@@ -108,11 +108,17 @@ KLIPPER_VERSION = "v0.12.0-1"
def _parse_gcode_estimated_time ( data : bytes ) - > int :
def _parse_gcode_estimated_time ( data : bytes ) - > int :
""" Liest ' ; estimated printing time (normal mode) = Xh Ym Zs ' aus GCode-Head er.
""" Liest geschätzte Druckzeit aus GCode (OrcaSlicer + PrusaSlic er) .
Gibt Sekunden zurück, 0 wenn nicht gefunden. Sucht nur in den ersten 8KB. """
Gibt Sekunden zurück, 0 wenn nicht gefunden.
PrusaSlicer schreibt die Zeit ins Header (erste 16KB),
OrcaSlicer schreibt sie ans Ende der Datei (letzte 16KB). """
import re
import re
header = data [ : 8192 ] . decode ( " utf-8 " , errors = " ignore " )
# Anfang + Ende der Datei durchsuchen (OrcaSlicer schreibt Zeit am Ende )
m = re . search ( r " ; \ s*estimated printing time \ (normal mode \ ) \ s*= \ s*(.*) " , header )
search_text = ( data [ : 16384 ] + data [ - 65536 : ] ) . decode ( " utf-8 " , errors = " ignore " )
# OrcaSlicer: ; total estimated time: 9m 20s
# PrusaSlicer: ; estimated printing time (normal mode) = 1h 9m 20s
m = ( re . search ( r " ; \ s*total estimated time: \ s*(.*) " , search_text ) or
re . search ( r " ; \ s*estimated printing time \ (normal mode \ ) \ s*= \ s*(.*) " , search_text ) )
if not m :
if not m :
return 0
return 0
parts = re . findall ( r " ( \ d+) \ s*([hms]) " , m . group ( 1 ) )
parts = re . findall ( r " ( \ d+) \ s*([hms]) " , m . group ( 1 ) )
@@ -121,6 +127,8 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
if unit == " h " : secs + = int ( val ) * 3600
if unit == " h " : secs + = int ( val ) * 3600
elif unit == " m " : secs + = int ( val ) * 60
elif unit == " m " : secs + = int ( val ) * 60
elif unit == " s " : secs + = int ( val )
elif unit == " s " : secs + = int ( val )
if secs :
log . info ( f " Slicer-Schätzzeit: { secs } s ( { m . group ( 1 ) . strip ( ) } ) " )
return secs
return secs
@@ -154,6 +162,7 @@ class KobraXBridge:
" taskid " : " -1 " ,
" taskid " : " -1 " ,
" print_speed_mode " : 2 ,
" print_speed_mode " : 2 ,
" connection_error " : " " ,
" connection_error " : " " ,
" file_ready " : " " ,
}
}
self . _ams_slots : list [ dict ] = [ ]
self . _ams_slots : list [ dict ] = [ ]
self . _ams_loaded_slot : int = - 1
self . _ams_loaded_slot : int = - 1
@@ -503,6 +512,7 @@ class KobraXBridge:
ct = request . headers . get ( " Content-Type " , " " )
ct = request . headers . get ( " Content-Type " , " " )
if " multipart " not in ct :
if " multipart " not in ct :
return web . json_response ( { " error " : " expected multipart " } , status = 400 )
return web . json_response ( { " error " : " expected multipart " } , status = 400 )
auto_print = False
reader = await request . multipart ( )
reader = await request . multipart ( )
file_data = None
file_data = None
remote_filename = self . _last_uploaded_file or " upload.gcode "
remote_filename = self . _last_uploaded_file or " upload.gcode "
@@ -516,14 +526,17 @@ class KobraXBridge:
val = ( await part . read ( ) ) . decode ( " utf-8 " , errors = " replace " ) . strip ( )
val = ( await part . read ( ) ) . decode ( " utf-8 " , errors = " replace " ) . strip ( )
if val :
if val :
remote_filename = val
remote_filename = val
elif part . name == " print " :
val = ( await part . read ( ) ) . decode ( " utf-8 " , errors = " replace " ) . strip ( ) . lower ( )
auto_print = val == " true "
else :
else :
log . debug ( f " Unbekanntes Multipart-Feld: { part . name } " )
log . debug ( f " Unbekanntes Multipart-Feld: { part . name } " )
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 )
file_md5 = hashlib . md5 ( file_data ) . hexdigest ( )
file_md5 = hashlib . md5 ( file_data ) . hexdigest ( )
file_size = len ( file_data )
file_size = len ( file_data )
# Slicer-Zeitschätzung aus GCode-Header auslesen
# Slicer-Zeitschätzung aus GCode-Header auslesen
self . _state [ " slicer_time " ] = _parse_gcode_estimated_time ( file_data )
self . _state [ " slicer_time " ] = _parse_gcode_estimated_time ( file_data )
@@ -553,9 +566,24 @@ class KobraXBridge:
# Druck starten mit vollständigem Payload (inkl. serve-URL + md5 + size)
# Druck starten mit vollständigem Payload (inkl. serve-URL + md5 + size)
serve_url = f " http:// { request . host } /serve/ { remote_filename } "
serve_url = f " http:// { request . host } /serve/ { remote_filename } "
log . info ( f " Starte Druck automatisch: { remote_filename } " )
loop = asyncio . get_event_loop ( )
# print=true im Multipart-Formular (Moonraker) oder Query-String → Druck starten
loop . run_in_executor ( None , lambda : self . _start_print ( remote_filename , serve_url , file_md5 , file_size ) )
# print=false oder fehlt → nur hochladen
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 )
if auto_print :
log . info ( f " Upload+Print (print=true): { remote_filename } " )
self . _state [ " file_ready " ] = " "
loop = asyncio . get_event_loop ( )
loop . run_in_executor ( None , lambda : self . _start_print ( remote_filename , serve_url , file_md5 , file_size ) )
else :
log . info ( f " Nur hochgeladen (print=false): { remote_filename } " )
self . _state [ " file_ready " ] = remote_filename
# OctoPrint-kompatibler Response (OrcaSlicer wertet refs aus)
# OctoPrint-kompatibler Response (OrcaSlicer wertet refs aus)
return web . json_response ( {
return web . json_response ( {
@@ -578,6 +606,7 @@ class KobraXBridge:
} , status = 201 )
} , status = 201 )
def _start_print ( self , filename : str , url : str = " " , md5 : str = " " , filesize : int = 0 ) :
def _start_print ( self , filename : str , url : str = " " , md5 : str = " " , filesize : int = 0 ) :
self . _state [ " file_ready " ] = " "
default_slot = getattr ( self . _args , " default_ams_slot " , " auto " )
default_slot = getattr ( self . _args , " default_ams_slot " , " auto " )
all_loaded = [ ( i , s ) for i , s in enumerate ( self . _ams_slots ) if s . get ( " status " ) == 5 ]
all_loaded = [ ( i , s ) for i , s in enumerate ( self . _ams_slots ) if s . get ( " status " ) == 5 ]
if default_slot != " auto " :
if default_slot != " auto " :
@@ -628,11 +657,6 @@ class KobraXBridge:
" model_objects_skip_parts " : [ ] ,
" model_objects_skip_parts " : [ ] ,
} ,
} ,
}
}
# Thumbnail vorab anfordern (Drucker antwortet async auf file/report)
self . _thumbnail_b64 = " "
self . client . publish ( " file " , " fileDetails " ,
{ " root " : " local " , " filename " : filename } , timeout = 0 )
log . info ( f " print/start → { filename } url= { url } ams= { len ( self . _ams_slots ) } slots " )
log . info ( f " print/start → { filename } url= { url } ams= { len ( self . _ams_slots ) } slots " )
result = self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
result = self . client . publish ( " print " , " start " , payload , timeout = 15.0 )
if result :
if result :
@@ -645,7 +669,9 @@ class KobraXBridge:
body = await request . json ( )
body = await request . json ( )
except Exception :
except Exception :
body = { }
body = { }
filename = bod y. get ( " filename " ) or self . _last_uploaded_file
filename = ( request . rel_url . quer y. get ( " filename " )
or body . get ( " filename " )
or self . _last_uploaded_file )
if not filename :
if not filename :
return web . json_response ( { " error " : " no filename " } , status = 400 )
return web . json_response ( { " error " : " no filename " } , status = 400 )
@@ -718,6 +744,12 @@ class KobraXBridge:
await loop . run_in_executor ( None , lambda : self . client . stop_print ( taskid ) )
await loop . run_in_executor ( None , lambda : self . client . stop_print ( taskid ) )
return web . json_response ( { " result " : " ok " } )
return web . json_response ( { " result " : " ok " } )
async def handle_api_file_ready_clear ( self , request ) :
self . _state [ " file_ready " ] = " "
self . _thumbnail_b64 = " "
self . _push_status_update ( )
return web . json_response ( { " result " : " ok " } )
async def handle_octoprint_version ( self , request ) :
async def handle_octoprint_version ( self , request ) :
return web . json_response ( {
return web . json_response ( {
" api " : " 0.1 " ,
" api " : " 0.1 " ,
@@ -845,6 +877,11 @@ main{flex:1;overflow-y:auto;padding:20px}
.spd-bar { height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden}
.spd-bar { height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden}
.spd-bar-fill { height:100 % ;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s}
.spd-bar-fill { height:100 % ;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s}
/* ── TIME CARDS ── */
.time-grid { display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px}
.time-block { background:var(--raised);border-radius:10px;padding:10px 12px}
.time-label { font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:4px}
.time-val { font-size:20px;font-weight:700;font-family:var(--mono);color:var(--txt)}
/* ── TEMPS ── */
/* ── TEMPS ── */
.temp-pair { display:grid;grid-template-columns:1fr 1fr;gap:12px}
.temp-pair { display:grid;grid-template-columns:1fr 1fr;gap:12px}
.temp-card-inner { display:grid;grid-template-columns:1fr 1fr;gap:12px}
.temp-card-inner { display:grid;grid-template-columns:1fr 1fr;gap:12px}
@@ -1010,6 +1047,13 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<body>
<body>
<div id= " conn-error-banner " style= " display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999; " ></div>
<div id= " conn-error-banner " style= " display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999; " ></div>
<div id= " file-ready-banner " style= " display:none;background:#1a6e3c;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:998;display:none;align-items:center;justify-content:center;gap:12px " >
<span>📄 <span id= " file-ready-name " ></span></span>
<button id= " file-ready-btn " onclick= " startReadyFile() "
style= " padding:5px 16px;background:#fff;color:#1a6e3c;border:none;border-radius:6px;font-weight:700;cursor:pointer;font-size:13px " ></button>
<button id= " file-cancel-btn " onclick= " cancelReadyFile() "
style= " padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px " ></button>
</div>
<header>
<header>
<div class= " logo " >⬡ KX-Bridge</div>
<div class= " logo " >⬡ KX-Bridge</div>
@@ -1102,6 +1146,34 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
</div>
</div>
</div>
</div>
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
<div class= " modal-overlay " id= " slot-edit-modal " onclick= " if(event.target===this)closeSlotEdit() " >
<div class= " modal-box " style= " max-width:340px " >
<div style= " display:flex;justify-content:space-between;align-items:center;margin-bottom:16px " >
<span class= " modal-title " id= " slot-edit-title " ></span>
<button onclick= " closeSlotEdit() " style= " background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1 " >✕</button>
</div>
<div style= " display:flex;align-items:center;gap:16px;margin-bottom:20px " >
<div id= " slot-edit-preview " style= " width:56px;height:56px;border-radius:50 % ;border:3px solid rgba(255,255,255,.2);flex-shrink:0 " ></div>
<div style= " flex:1 " >
<div style= " font-size:11px;color:var(--txt2);margin-bottom:4px " id= " lbl-slot-color " ></div>
<input type= " color " id= " slot-edit-color "
oninput= " document.getElementById( ' slot-edit-preview ' ).style.background=this.value "
style= " width:100 % ;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px " >
</div>
</div>
<div style= " margin-bottom:20px " >
<div style= " font-size:11px;color:var(--txt2);margin-bottom:6px " id= " lbl-slot-material " ></div>
<div style= " display:flex;flex-wrap:wrap;gap:6px " id= " slot-mat-btns " >
</div>
<input type= " text " id= " slot-edit-mat "
oninput= " highlightMatBtn(this.value) "
style= " margin-top:8px;width:100 % ;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box " >
</div>
<button class= " modal-save " id= " btn-slot-edit-save " onclick= " saveSlotEdit() " ></button>
</div>
</div>
<div class= " layout " >
<div class= " layout " >
<nav class= " sidebar " >
<nav class= " sidebar " >
<button class= " nav-btn active " onclick= " showPanel( ' dashboard ' ) " id= " nb-dashboard " >
<button class= " nav-btn active " onclick= " showPanel( ' dashboard ' ) " id= " nb-dashboard " >
@@ -1128,7 +1200,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
</div>
</div>
</div>
</div>
<div class= " cam-wrap " id= " cam-wrap " >
<div class= " cam-wrap " id= " cam-wrap " >
<div class= " cam-placeholder " id= " cam-placeholder " >📷 Kamera nicht gestartet</div>
<div class= " cam-placeholder " id= " cam-placeholder " ><span id= " cam-placeholder-txt " > 📷 Kamera nicht gestartet</span></ div>
<div class= " cam-spinner " id= " cam-spinner " ></div>
<div class= " cam-spinner " id= " cam-spinner " ></div>
<img id= " cam-img " style= " display:none;width:100 % ;height:auto " alt= " Kamera " >
<img id= " cam-img " style= " display:none;width:100 % ;height:auto " alt= " Kamera " >
<div class= " cam-overlay " id= " cam-overlay " style= " display:none " >
<div class= " cam-overlay " id= " cam-overlay " style= " display:none " >
@@ -1143,14 +1215,26 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<div class= " card-title " ><span>◉</span> <span id= " d-card-progress " >Fortschritt</span></div>
<div class= " card-title " ><span>◉</span> <span id= " d-card-progress " >Fortschritt</span></div>
<img id= " d-thumbnail " src= " " alt= " " style= " display:none;width:100 % ;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px " >
<img id= " d-thumbnail " src= " " alt= " " style= " display:none;width:100 % ;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px " >
<div class= " pct-big " ><span id= " d-pct " >0</span><small> % </small></div>
<div class= " pct-big " ><span id= " d-pct " >0</span><small> % </small></div>
<div class= " progress-bar " style= " margin:8px 0 " ><div class= " progress-fill " id= " d-pbar " style= " width:0 % " ></div></div >
<div style= " display:flex;align-items:center;gap:10px;margin:8px 0 " >
<div class= " meta-row " style= " margin-top:6px " >
<div class= " progress-bar " style= " flex:1;margin:0 " ><div class= " progress-fill " id= " d-pbar " style= " width:0 % " ></div></div >
<span id= " d-elapsed " >– </span >
<div class= " time-block " style= " padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0 " >
<span id= " d-remain " style = " color:var(--acc) " >– </span >
<div class= " time-label " id = " d-lbl-layers " ></div >
<span id= " d-layers " class = " layer-badge " >– </span >
<div class= " time-val " style= " font-size:16px " id = " d- layers " >– </div >
</div>
</div>
</div>
<div class= " meta-row " style= " margin-top:4px;font-size:0.82em;opacity:0.7 " id= " d-slicer-row " >
<div class= " time-grid " >
<span id= " d-slicer-label " ></span><span id= " d-slicer-time " style= " margin-left:4px " >– </span >
<div class= " time-block " >
<div class= " time-label " id= " d-lbl-elapsed " ></div>
<div class= " time-val " id= " d-elapsed " >– </div>
</div>
<div class= " time-block " id= " d-slicer-row " style= " display:none " >
<div class= " time-label " id= " d-slicer-label " ></div>
<div class= " time-val " id= " d-slicer-time " >– </div>
</div>
<div class= " time-block " style= " color:var(--acc) " >
<div class= " time-label " id= " d-lbl-remain " ></div>
<div class= " time-val " id= " d-remain " style= " color:var(--acc) " >– </div>
</div>
</div>
</div>
<div class= " fname " id= " d-fname " title= " " style= " margin-top:6px " >– </div>
<div class= " fname " id= " d-fname " title= " " style= " margin-top:6px " >– </div>
<div class= " ctrl-btns " id= " d-ctrl-btns " style= " margin-top:12px " >
<div class= " ctrl-btns " id= " d-ctrl-btns " style= " margin-top:12px " >
@@ -1308,7 +1392,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<a id= " btn-log-dl " href= " /api/log/download " download= " kx-bridge.log "
<a id= " btn-log-dl " href= " /api/log/download " download= " kx-bridge.log "
style= " font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none " >⬇ Download</a>
style= " font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none " >⬇ Download</a>
</div>
</div>
<div style= " display:flex;gap:8 px;margin-bottom:8 px;flex-wrap:wrap;align-items:center " >
<div style= " display:flex;gap:6 px;margin-bottom:6 px;flex-wrap:wrap;align-items:center " >
<input id= " log-filter " type= " text " placeholder= " Filter… "
<input id= " log-filter " type= " text " placeholder= " Filter… "
oninput= " renderLog() "
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) " >
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) " >
@@ -1317,7 +1401,18 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<button onclick= " consoleLogs=[];renderLog() "
<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(--raised);color:var(--txt2);cursor:pointer " >✕ Clear</button>
</div>
</div>
<div class= " conso le " id= " console-log " style= " height:calc(100vh - 220 px) ;min-height:200px " onscroll= " onLogScroll() " ></div >
<div sty le= " display:flex;gap:5 px;margin-bottom:8px;flex-wrap:wrap " >
<span style= " font-size:11px;color:var(--txt2);align-self:center;margin-right:2px " >Dir:</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 " >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>
</div>
<div class= " console " id= " console-log " style= " height:calc(100vh - 260px);min-height:160px " onscroll= " onLogScroll() " ></div>
</div>
</div>
</div>
</div>
</main>
</main>
@@ -1352,7 +1447,7 @@ var LANG_DE={
header_status_standby: ' Bereit ' ,header_status_printing: ' Druckt ' ,header_status_complete: ' Fertig ' ,header_status_error: ' Fehler ' ,
header_status_standby: ' Bereit ' ,header_status_printing: ' Druckt ' ,header_status_complete: ' Fertig ' ,header_status_error: ' Fehler ' ,
kobra_free: ' Bereit ' ,kobra_busy: ' Beschäftigt ' ,kobra_printing: ' Druckt ' ,kobra_preheating: ' Aufheizen ' ,kobra_auto_leveling: ' Nivellierung ' ,kobra_checking: ' Prüfung ' ,kobra_updated: ' Aktualisierung ' ,kobra_init: ' Initialisierung ' ,kobra_pausing: ' Pausiert... ' ,kobra_paused: ' Pausiert ' ,kobra_resuming: ' Fortsetzen... ' ,kobra_resumed: ' Fortgesetzt ' ,kobra_stopping: ' Stoppt... ' ,kobra_stoped: ' Gestoppt ' ,kobra_finished: ' Abgeschlossen ' ,kobra_failed: ' Fehler ' ,kobra_canceled: ' Abgebrochen ' ,kobra_offline: ' Offline ' ,
kobra_free: ' Bereit ' ,kobra_busy: ' Beschäftigt ' ,kobra_printing: ' Druckt ' ,kobra_preheating: ' Aufheizen ' ,kobra_auto_leveling: ' Nivellierung ' ,kobra_checking: ' Prüfung ' ,kobra_updated: ' Aktualisierung ' ,kobra_init: ' Initialisierung ' ,kobra_pausing: ' Pausiert... ' ,kobra_paused: ' Pausiert ' ,kobra_resuming: ' Fortsetzen... ' ,kobra_resumed: ' Fortgesetzt ' ,kobra_stopping: ' Stoppt... ' ,kobra_stoped: ' Gestoppt ' ,kobra_finished: ' Abgeschlossen ' ,kobra_failed: ' Fehler ' ,kobra_canceled: ' Abgebrochen ' ,kobra_offline: ' Offline ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Druck ' ,nav_temps: ' Temperaturen ' ,nav_motion: ' Achsen ' ,nav_ams: ' AMS ' ,nav_extras: ' Licht / Lüfter ' ,nav_console: ' Konsole ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Druck ' ,nav_temps: ' Temperaturen ' ,nav_motion: ' Achsen ' ,nav_ams: ' AMS ' ,nav_extras: ' Licht / Lüfter ' ,nav_console: ' Konsole ' ,
card_progress: ' Fortschritt ' ,card_temps: ' Temperaturen ' ,card_light_fan: ' Lüfter ' ,card_speed: ' Druckgeschwindigkeit ' ,card_cam: ' Kamera ' ,lbl_elapsed: ' Verstrichen ' ,lbl_remaining: ' verbleibend ' ,lbl_slicer_time: ' Slicer-Schätzung: ' ,
card_progress: ' Fortschritt ' ,card_temps: ' Temperaturen ' ,card_light_fan: ' Lüfter ' ,card_speed: ' Druckgeschwindigkeit ' ,card_cam: ' Kamera ' ,lbl_elapsed: ' Verstrichen: ' ,lbl_remaining: ' Restzeit: ' ,lbl_slicer_time: ' Slicer-Schätzung: ' ,lbl_layers: ' Layer ' ,
speed_silent: ' 🐢 Leise ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
speed_silent: ' 🐢 Leise ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
lbl_light: ' 💡 Licht ' ,lbl_feed: ' Einziehen ' ,lbl_unload: ' Ausziehen ' ,
lbl_light: ' 💡 Licht ' ,lbl_feed: ' Einziehen ' ,lbl_unload: ' Ausziehen ' ,
cam_placeholder: ' 📷 Kamera nicht gestartet ' ,btn_cam_start: ' ▶ Kamera ' ,btn_cam_stop: ' ◼ Kamera ' ,
cam_placeholder: ' 📷 Kamera nicht gestartet ' ,btn_cam_start: ' ▶ Kamera ' ,btn_cam_stop: ' ◼ Kamera ' ,
@@ -1374,13 +1469,19 @@ var LANG_DE={
update_check: ' Auf Updates prüfen ' ,update_checking: ' Prüfe... ' ,update_available: ' verfügbar ' ,update_none: ' Bereits aktuell ' ,
update_check: ' Auf Updates prüfen ' ,update_checking: ' Prüfe... ' ,update_available: ' verfügbar ' ,update_none: ' Bereits aktuell ' ,
update_apply: ' Jetzt installieren ' ,update_applying: ' Lade herunter... ' ,update_restarting: ' Starte neu... ' ,update_error: ' Fehler ' ,
update_apply: ' Jetzt installieren ' ,update_applying: ' Lade herunter... ' ,update_restarting: ' Starte neu... ' ,update_error: ' Fehler ' ,
btn_connect: ' ⚡ Verbinden ' ,btn_disconnect: ' ✕ Trennen ' ,
btn_connect: ' ⚡ Verbinden ' ,btn_disconnect: ' ✕ Trennen ' ,
lbl_conn_error: ' Verbindungsfehler: '
lbl_conn_error: ' Verbindungsfehler: ' ,
slot_edit_title: ' Slot bearbeiten ' ,slot_edit_color: ' Farbe ' ,slot_edit_material: ' Material ' ,
slot_edit_save: ' 💾 Speichern ' ,slot_edit_custom: ' z.B. PLA, PETG, ABS… ' ,
slot_edit_ok: ' AMS Slot ' ,
log_dir_all: ' Alle ' ,
file_ready_btn: ' ▶ Druck starten ' ,
file_cancel_btn: ' ✕ Abbrechen '
};
};
var LANG_EN= {
var LANG_EN= {
header_status_standby: ' Ready ' ,header_status_printing: ' Printing ' ,header_status_complete: ' Complete ' ,header_status_error: ' Error ' ,
header_status_standby: ' Ready ' ,header_status_printing: ' Printing ' ,header_status_complete: ' Complete ' ,header_status_error: ' Error ' ,
kobra_free: ' Ready ' ,kobra_busy: ' Busy ' ,kobra_printing: ' Printing ' ,kobra_preheating: ' Preheating ' ,kobra_auto_leveling: ' Auto Leveling ' ,kobra_checking: ' Checking ' ,kobra_updated: ' Updating ' ,kobra_init: ' Initializing ' ,kobra_pausing: ' Pausing... ' ,kobra_paused: ' Paused ' ,kobra_resuming: ' Resuming... ' ,kobra_resumed: ' Resumed ' ,kobra_stopping: ' Stopping... ' ,kobra_stoped: ' Stopped ' ,kobra_finished: ' Finished ' ,kobra_failed: ' Error ' ,kobra_canceled: ' Cancelled ' ,kobra_offline: ' Offline ' ,
kobra_free: ' Ready ' ,kobra_busy: ' Busy ' ,kobra_printing: ' Printing ' ,kobra_preheating: ' Preheating ' ,kobra_auto_leveling: ' Auto Leveling ' ,kobra_checking: ' Checking ' ,kobra_updated: ' Updating ' ,kobra_init: ' Initializing ' ,kobra_pausing: ' Pausing... ' ,kobra_paused: ' Paused ' ,kobra_resuming: ' Resuming... ' ,kobra_resumed: ' Resumed ' ,kobra_stopping: ' Stopping... ' ,kobra_stoped: ' Stopped ' ,kobra_finished: ' Finished ' ,kobra_failed: ' Error ' ,kobra_canceled: ' Cancelled ' ,kobra_offline: ' Offline ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Print ' ,nav_temps: ' Temperatures ' ,nav_motion: ' Motion ' ,nav_ams: ' AMS ' ,nav_extras: ' Light / Fan ' ,nav_console: ' Console ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Print ' ,nav_temps: ' Temperatures ' ,nav_motion: ' Motion ' ,nav_ams: ' AMS ' ,nav_extras: ' Light / Fan ' ,nav_console: ' Console ' ,
card_progress: ' Progress ' ,card_temps: ' Temperatures ' ,card_light_fan: ' Fan ' ,card_speed: ' Print Speed ' ,card_cam: ' Camera ' ,lbl_elapsed: ' Elapsed ' ,lbl_remaining: ' r emaining' ,lbl_slicer_time: ' Slicer estimate: ' ,
card_progress: ' Progress ' ,card_temps: ' Temperatures ' ,card_light_fan: ' Fan ' ,card_speed: ' Print Speed ' ,card_cam: ' Camera ' ,lbl_elapsed: ' Elapsed: ' ,lbl_remaining: ' R emaining: ' ,lbl_slicer_time: ' Slicer estimate: ' ,lbl_layers: ' Layer ' ,
speed_silent: ' 🐢 Silent ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
speed_silent: ' 🐢 Silent ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
lbl_light: ' 💡 Light ' ,lbl_feed: ' Load ' ,lbl_unload: ' Unload ' ,
lbl_light: ' 💡 Light ' ,lbl_feed: ' Load ' ,lbl_unload: ' Unload ' ,
cam_placeholder: ' 📷 Camera not started ' ,btn_cam_start: ' ▶ Camera ' ,btn_cam_stop: ' ◼ Camera ' ,
cam_placeholder: ' 📷 Camera not started ' ,btn_cam_start: ' ▶ Camera ' ,btn_cam_stop: ' ◼ Camera ' ,
@@ -1402,7 +1503,13 @@ var LANG_EN={
update_check: ' Check for Updates ' ,update_checking: ' Checking... ' ,update_available: ' available ' ,update_none: ' Already up to date ' ,
update_check: ' Check for Updates ' ,update_checking: ' Checking... ' ,update_available: ' available ' ,update_none: ' Already up to date ' ,
update_apply: ' Install Now ' ,update_applying: ' Downloading... ' ,update_restarting: ' Restarting... ' ,update_error: ' Error ' ,
update_apply: ' Install Now ' ,update_applying: ' Downloading... ' ,update_restarting: ' Restarting... ' ,update_error: ' Error ' ,
btn_connect: ' ⚡ Connect ' ,btn_disconnect: ' ✕ Disconnect ' ,
btn_connect: ' ⚡ Connect ' ,btn_disconnect: ' ✕ Disconnect ' ,
lbl_conn_error: ' Connection error: '
lbl_conn_error: ' Connection error: ' ,
slot_edit_title: ' Edit Slot ' ,slot_edit_color: ' Color ' ,slot_edit_material: ' Material ' ,
slot_edit_save: ' 💾 Save ' ,slot_edit_custom: ' e.g. PLA, PETG, ABS… ' ,
slot_edit_ok: ' AMS Slot ' ,
log_dir_all: ' All ' ,
file_ready_btn: ' ▶ Start Print ' ,
file_cancel_btn: ' ✕ Cancel '
};
};
var currentLang= ' de ' ;
var currentLang= ' de ' ;
var T=LANG_DE;
var T=LANG_DE;
@@ -1428,6 +1535,10 @@ function applyLang(){
setText( ' d-card-speed ' ,T.card_speed);
setText( ' d-card-speed ' ,T.card_speed);
setText( ' d-card-cam ' ,T.card_cam);
setText( ' d-card-cam ' ,T.card_cam);
setText( ' d-card-ams ' ,T.panel_ams_title);
setText( ' d-card-ams ' ,T.panel_ams_title);
setText( ' d-lbl-elapsed ' ,T.lbl_elapsed);
setText( ' d-lbl-remain ' ,T.lbl_remaining);
setText( ' d-slicer-label ' ,T.lbl_slicer_time);
setText( ' d-lbl-layers ' ,T.lbl_layers);
setText( ' d-lbl-light ' ,T.lbl_light);
setText( ' d-lbl-light ' ,T.lbl_light);
setText( ' d-lbl-bed ' ,T.label_bed);
setText( ' d-lbl-bed ' ,T.label_bed);
// Dashboard buttons
// Dashboard buttons
@@ -1479,6 +1590,14 @@ function applyLang(){
document.querySelectorAll( ' .lbl-unload ' ).forEach(e=>e.textContent=T.lbl_unload);
document.querySelectorAll( ' .lbl-unload ' ).forEach(e=>e.textContent=T.lbl_unload);
// conn-btn text (nur wenn nicht im Übergangszustand)
// conn-btn text (nur wenn nicht im Übergangszustand)
updateConnBtn();
updateConnBtn();
// Slot-Edit-Dialog
setText( ' lbl-slot-color ' ,T.slot_edit_color);
setText( ' lbl-slot-material ' ,T.slot_edit_material);
setText( ' btn-slot-edit-save ' ,T.slot_edit_save);
var mi=document.getElementById( ' slot-edit-mat ' );if(mi)mi.setAttribute( ' placeholder ' ,T.slot_edit_custom);
setText( ' logdir-all ' ,T.log_dir_all);
setText( ' file-ready-btn ' ,T.file_ready_btn);
setText( ' file-cancel-btn ' ,T.file_cancel_btn);
}
}
function setText(id,txt) { var el=document.getElementById(id);if(el)el.textContent=txt;}
function setText(id,txt) { var el=document.getElementById(id);if(el)el.textContent=txt;}
(function() {
(function() {
@@ -1510,6 +1629,8 @@ function showPanel(id){
var consoleLogs=[];
var consoleLogs=[];
var logAutoScroll=true;
var logAutoScroll=true;
var logBadgeCount=0;
var logBadgeCount=0;
var logDirFilter= ' all ' ; // ' all ' | ' rx ' | ' tx '
var logTopicFilter= ' ' ; // ' ' = no topic filter
function clog(msg,cls) {
function clog(msg,cls) {
cls=cls|| ' msg-info ' ;
cls=cls|| ' msg-info ' ;
@@ -1535,14 +1656,41 @@ function _appendLog(entry,forceCls){
}
}
renderLog();
renderLog();
}
}
function setLogDir(dir) {
logDirFilter=dir;
document.querySelectorAll( ' .log-dir-btn ' ).forEach(function(b) {
b.style.background=b.id=== ' logdir- ' +dir? ' var(--accent) ' : ' var(--raised) ' ;
b.style.color=b.id=== ' logdir- ' +dir? ' #fff ' : ' var(--txt2) ' ;
});
renderLog();
}
function setLogTopic(topic) {
var inp=document.getElementById( ' log-filter ' );
var active=inp.value===topic;
inp.value=active? ' ' :topic;
document.querySelectorAll( ' .log-topic-btn ' ).forEach(function(b) {
var on=!active&&b.getAttribute( ' data-topic ' )===topic;
b.style.background=on? ' var(--accent) ' : ' var(--raised) ' ;
b.style.color=on? ' #fff ' : ' var(--txt2) ' ;
});
renderLog();
}
function renderLog() {
function renderLog() {
var el=document.getElementById( ' console-log ' );
var el=document.getElementById( ' console-log ' );
if(!el)return;
if(!el)return;
var filter=(document.getElementById( ' log-filter ' )|| {} ).value|| ' ' ;
var filter=(document.getElementById( ' log-filter ' )|| {} ).value|| ' ' ;
var fl=filter.toLowerCase();
var fl=filter.toLowerCase();
var rows=fl? consoleLogs.filter(l=>l.msg.toLowerCase().includes(fl)):consoleLogs;
var rows=consoleLogs.filter(function(l) {
var m=l.msg;
if(logDirFilter=== ' rx ' &&!/ RX[ (]/.test(m))return false;
if(logDirFilter=== ' tx ' &&!/ TX[ (]/.test(m))return false;
if(fl&&!m.toLowerCase().includes(fl))return false;
return true;
});
var savedScroll=logAutoScroll?null:el.scrollTop;
el.innerHTML=rows.map(l=>`<div><span class= " ts " >$ {l.ts} </span><span class= " $ {l.cls} " >$ { escHtml(l.msg)}</span></div>`).join( ' ' );
el.innerHTML=rows.map(l=>`<div><span class= " ts " >$ {l.ts} </span><span class= " $ {l.cls} " >$ { escHtml(l.msg)}</span></div>`).join( ' ' );
if(logAutoScroll)el.scrollTop=el.scrollHeight;
if(logAutoScroll)el.scrollTop=el.scrollHeight;
else if(savedScroll!==null)el.scrollTop=savedScroll;
}
}
function onLogScroll() {
function onLogScroll() {
var el=document.getElementById( ' console-log ' );
var el=document.getElementById( ' console-log ' );
@@ -1585,6 +1733,13 @@ function applyState(){
// connection error banner
// connection error banner
var banner=document.getElementById( ' conn-error-banner ' );
var banner=document.getElementById( ' conn-error-banner ' );
if(banner) { if(s.connection_error) { banner.textContent= ' ⚠ ' +(T.lbl_conn_error|| ' Connection error: ' )+ ' ' +s.connection_error;banner.style.display= ' block ' ;}else { banner.style.display= ' none ' ;}}
if(banner) { if(s.connection_error) { banner.textContent= ' ⚠ ' +(T.lbl_conn_error|| ' Connection error: ' )+ ' ' +s.connection_error;banner.style.display= ' block ' ;}else { banner.style.display= ' none ' ;}}
var frb=document.getElementById( ' file-ready-banner ' );
if(frb) {
if(s.file_ready&&s.print_state=== ' standby ' ) {
document.getElementById( ' file-ready-name ' ).textContent=s.file_ready;
frb.style.display= ' flex ' ;
}else { frb.style.display= ' none ' ;}
}
// header
// header
var b=document.getElementById( ' h-badge ' );
var b=document.getElementById( ' h-badge ' );
b.className= ' hbadge ' +s.print_state;
b.className= ' hbadge ' +s.print_state;
@@ -1611,16 +1766,12 @@ function applyState(){
var layers=s.curr_layer&&s.total_layers? ' L ' +s.curr_layer+ ' / ' +s.total_layers: ' – ' ;
var layers=s.curr_layer&&s.total_layers? ' L ' +s.curr_layer+ ' / ' +s.total_layers: ' – ' ;
var dlayers=document.getElementById( ' d-layers ' );if(dlayers)dlayers.textContent=layers;
var dlayers=document.getElementById( ' d-layers ' );if(dlayers)dlayers.textContent=layers;
var elapsed=fmtTime(s.print_duration);
var d elapsed=document.getElementById( ' d-elapsed ' );if(delapsed)delapsed.textContent= fmtTime(s.print_duration);
var delapsed =document.getElementById( ' d-elapsed ' );if(delapsed)delapsed.textContent=elapsed ;
var dremain =document.getElementById( ' d-remain ' );if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time): ' – ' ;
var remain=s.remain_time>0? ' ≈ ' +fmtTime(s.remain_time)+ ' ' +T.lbl_remaining: ' ' ;
var dremain=document.getElementById( ' d-remain ' );if(dremain)dremain.textContent=remain;
var dslrow=document.getElementById( ' d-slicer-row ' );
var dslrow=document.getElementById( ' d-slicer-row ' );
var dsltime=document.getElementById( ' d-slicer-time ' );
var dsltime=document.getElementById( ' d-slicer-time ' );
var dsllbl=document.getElementById( ' d-slicer-label ' );
if(dslrow&&dsltime) {
if(dslrow&&dsltime) {
if(s.slicer_time>0) { dslrow.style.display= ' ' ;if(dsllbl)dsllbl.textContent=T.lbl_slicer_time; dsltime.textContent=fmtTime(s.slicer_time);}
if(s.slicer_time>0) { dslrow.style.display= ' ' ;dsltime.textContent=fmtTime(s.slicer_time);}
else { dslrow.style.display= ' none ' ;}
else { dslrow.style.display= ' none ' ;}
}
}
@@ -1657,6 +1808,7 @@ function applyState(){
// AMS
// AMS
if(s.ams_slots&&s.ams_slots.length) {
if(s.ams_slots&&s.ams_slots.length) {
window._amsSlots=s.ams_slots;
var html= ' ' ;
var html= ' ' ;
s.ams_slots.forEach(function(slot,i) {
s.ams_slots.forEach(function(slot,i) {
var empty=slot.status!==5;
var empty=slot.status!==5;
@@ -1664,11 +1816,13 @@ function applyState(){
var col= ' rgb( ' +rgb[0]+ ' , ' +rgb[1]+ ' , ' +rgb[2]+ ' ) ' ;
var col= ' rgb( ' +rgb[0]+ ' , ' +rgb[1]+ ' , ' +rgb[2]+ ' ) ' ;
var active=slot.status===1||slot.active;
var active=slot.status===1||slot.active;
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+ ' % ' : ' – ' );
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+ ' % ' : ' – ' );
html+= ' <div class= " ams-slot ' +(active? ' active ' : ' ' )+(empty? ' empty ' : ' ' )+ ' " style= " --slot-color: ' +col+ ' ;opacity: ' +(empty?0.4:1)+ ' " > '
var idx=slot.index!=null?slot.index:i;
html+= ' <div class= " ams-slot ' +(active? ' active ' : ' ' )+(empty? ' empty ' : ' ' )+ ' " style= " --slot-color: ' +col+ ' ;opacity: ' +(empty?0.4:1)+ ' ;cursor:pointer " onclick= " openSlotEdit( ' +i+ ' ) " > '
+ ' <div class= " slot-circle " style= " background: ' +col+ ' " ></div> '
+ ' <div class= " slot-circle " style= " background: ' +col+ ' " ></div> '
+ ' <div class= " slot-material " > ' +(empty? ' – ' :(slot.type||slot.material_type|| ' – ' ))+ ' </div> '
+ ' <div class= " slot-material " > ' +(empty? ' – ' :(slot.type||slot.material_type|| ' – ' ))+ ' </div> '
+ ' <div class= " slot-label " >Slot ' +(slot.index!=null?slot.index+1:i +1)+ ' </div> '
+ ' <div class= " slot-label " >Slot ' +(idx +1)+ ' </div> '
+ ' <div class= " slot-label " style= " font-size:10px;color:var(--txt2) " > ' +pct+ ' </div> '
+ ' <div class= " slot-label " style= " font-size:10px;color:var(--txt2) " > ' +pct+ ' </div> '
+ ' <div style= " font-size:9px;color:var(--txt2);margin-top:2px " >✏</div> '
+ ' </div> ' ;
+ ' </div> ' ;
});
});
document.getElementById( ' ams-slots ' ).innerHTML=html;
document.getElementById( ' ams-slots ' ).innerHTML=html;
@@ -1767,6 +1921,78 @@ function openSettings(){
function closeSettings() {
function closeSettings() {
document.getElementById( ' settings-modal ' ).classList.remove( ' open ' );
document.getElementById( ' settings-modal ' ).classList.remove( ' open ' );
}
}
// ── AMS Slot Edit ──
var _slotEditIndex=-1;
var _MAT_PRESETS=[ ' PLA ' , ' PETG ' , ' ABS ' , ' ASA ' , ' TPU ' , ' PA ' , ' PC ' , ' HIPS ' ];
function openSlotEdit(i) {
var slot=(window._amsSlots||[])[i]|| {} ;
var index=slot.index!=null?slot.index:i;
_slotEditIndex=index;
document.getElementById( ' slot-edit-title ' ).textContent=T.slot_edit_title+ ' ' +(index+1);
var rgb=Array.isArray(slot.color)?slot.color:[128,128,128];
var hex= ' # ' +rgb.map(function(v) { return( ' 0 ' +Math.min(255,v).toString(16)).slice(-2)}).join( ' ' );
var ci=document.getElementById( ' slot-edit-color ' );
ci.value=hex;
document.getElementById( ' slot-edit-preview ' ).style.background=hex;
var mat=(slot.type|| ' PLA ' ).toUpperCase();
document.getElementById( ' slot-edit-mat ' ).value=mat;
var btns=document.getElementById( ' slot-mat-btns ' );
btns.innerHTML=_MAT_PRESETS.map(function(m) {
return ' <button class= " mat-preset-btn " data-mat= " ' +m+ ' " onclick= " selectMatPreset( \ ' ' +m+ ' \ ' ) " '
+ ' style= " padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px; '
+(m===mat? ' background:var(--accent);color:#fff ' : ' background:var(--raised);color:var(--txt2) ' )+ ' " > ' +m+ ' </button> ' ;
}).join( ' ' );
document.getElementById( ' slot-edit-modal ' ).classList.add( ' open ' );
}
function closeSlotEdit() {
document.getElementById( ' slot-edit-modal ' ).classList.remove( ' open ' );
}
function startReadyFile() {
var btn=document.getElementById( ' file-ready-btn ' );
if(btn) { btn.disabled=true;btn.textContent= ' … ' ;}
post( ' /printer/print/start ' , { filename:S.file_ready})
.then(function(r) { return r.json();})
.then(function(r) {
document.getElementById( ' file-ready-banner ' ).style.display= ' none ' ;
if(btn) { btn.disabled=false;setText( ' file-ready-btn ' ,T.file_ready_btn);}
})
.catch(function(e) {
clog((T.log_error|| ' Error: ' )+ ' ' +e, ' msg-err ' );
if(btn) { btn.disabled=false;setText( ' file-ready-btn ' ,T.file_ready_btn);}
});
}
function cancelReadyFile() {
post( ' /api/file_ready/clear ' , {} )
.then(function() { document.getElementById( ' file-ready-banner ' ).style.display= ' none ' ;});
}
function selectMatPreset(m) {
document.getElementById( ' slot-edit-mat ' ).value=m;
highlightMatBtn(m);
}
function highlightMatBtn(val) {
document.querySelectorAll( ' .mat-preset-btn ' ).forEach(function(b) {
var on=b.getAttribute( ' data-mat ' )===val.toUpperCase();
b.style.background=on? ' var(--accent) ' : ' var(--raised) ' ;
b.style.color=on? ' #fff ' : ' var(--txt2) ' ;
});
}
function hexToRgb(hex) {
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
return[r,g,b];
}
function saveSlotEdit() {
var hex=document.getElementById( ' slot-edit-color ' ).value;
var mat=document.getElementById( ' slot-edit-mat ' ).value.trim().toUpperCase()|| ' PLA ' ;
var color=hexToRgb(hex);
post( ' /api/ams/set_slot ' , { index:_slotEditIndex,type:mat,color:color})
.then(function(r) { return r.json();})
.then(function(r) {
closeSlotEdit();
clog((T.slot_edit_ok|| ' AMS Slot ' )+ ' ' +(_slotEditIndex+1)+ ' : ' +mat+ ' ' +hex, ' msg-ok ' );
})
.catch(function(e) { clog( ' Fehler: ' +e, ' msg-err ' );});
}
document.addEventListener( ' DOMContentLoaded ' ,function() {
document.addEventListener( ' DOMContentLoaded ' ,function() {
document.getElementById( ' s-printer-ip ' ).addEventListener( ' input ' ,function() {
document.getElementById( ' s-printer-ip ' ).addEventListener( ' input ' ,function() {
var hint=document.getElementById( ' lbl-ip-hint ' );
var hint=document.getElementById( ' lbl-ip-hint ' );
@@ -2084,6 +2310,35 @@ function toggleCam(){if(camOn)camStop();else camStart()}
self . _state [ " print_speed_mode " ] = mode
self . _state [ " print_speed_mode " ] = mode
return web . json_response ( { " result " : " ok " } )
return web . json_response ( { " result " : " ok " } )
async def handle_api_ams_set_slot ( self , request ) :
try :
body = await request . json ( )
except Exception :
body = { }
index = int ( body . get ( " index " , 0 ) )
mat = str ( body . get ( " type " , " PLA " ) ) . upper ( )
color = body . get ( " color " , [ 255 , 255 , 255 ] )
if not ( isinstance ( color , list ) and len ( color ) == 3 ) :
return web . json_response ( { " error " : " color must be [r,g,b] " } , status = 400 )
loop = asyncio . get_event_loop ( )
def _send ( ) :
resp = self . client . publish (
" multiColorBox " , " setInfo " ,
{ " multi_color_box " : [ { " id " : - 1 , " slots " : [ { " index " : index , " type " : mat , " color " : color } ] } ] } ,
timeout = 5
)
log . info ( f " setInfo slot= { index } type= { mat } color= { color } → { resp } " )
return resp
resp = await loop . run_in_executor ( None , _send )
if resp and resp . get ( " code " ) == 200 :
# Update cached slot immediately
for s in self . _ams_slots :
if s . get ( " index " ) == index :
s [ " type " ] = mat
s [ " color " ] = color
break
return web . json_response ( { " result " : " ok " } )
async def handle_api_ams_feed ( self , request ) :
async def handle_api_ams_feed ( self , request ) :
try :
try :
body = await request . json ( )
body = await request . json ( )
@@ -2336,6 +2591,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
" ams_loaded_slot " : self . _ams_loaded_slot ,
" ams_loaded_slot " : self . _ams_loaded_slot ,
" thumbnail " : self . _thumbnail_b64 ,
" thumbnail " : self . _thumbnail_b64 ,
" connection_error " : s [ " connection_error " ] ,
" connection_error " : s [ " connection_error " ] ,
" file_ready " : s [ " file_ready " ] ,
" version " : self . _read_version ( ) ,
" version " : self . _read_version ( ) ,
} )
} )
@@ -2850,6 +3106,7 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r . add_post ( " /api/disconnect " , bridge . handle_api_disconnect )
r . add_post ( " /api/disconnect " , bridge . handle_api_disconnect )
r . add_post ( " /api/speed " , bridge . handle_api_speed )
r . add_post ( " /api/speed " , bridge . handle_api_speed )
r . add_post ( " /api/ams/feed " , bridge . handle_api_ams_feed )
r . add_post ( " /api/ams/feed " , bridge . handle_api_ams_feed )
r . add_post ( " /api/ams/set_slot " , bridge . handle_api_ams_set_slot )
r . add_post ( " /api/axis " , bridge . handle_api_axis )
r . add_post ( " /api/axis " , bridge . handle_api_axis )
r . add_post ( " /api/temperature " , bridge . handle_api_temperature )
r . add_post ( " /api/temperature " , bridge . handle_api_temperature )
r . add_get ( " /api/camera " , bridge . handle_api_camera )
r . add_get ( " /api/camera " , bridge . handle_api_camera )
@@ -2862,6 +3119,7 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r . add_post ( " /api/settings " , bridge . handle_api_settings_post )
r . add_post ( " /api/settings " , bridge . handle_api_settings_post )
r . add_get ( " /api/update/check " , bridge . handle_api_update_check )
r . add_get ( " /api/update/check " , bridge . handle_api_update_check )
r . add_post ( " /api/update/apply " , bridge . handle_api_update_apply )
r . add_post ( " /api/update/apply " , bridge . handle_api_update_apply )
r . add_post ( " /api/file_ready/clear " , bridge . handle_api_file_ready_clear )
r . add_get ( " /api/log/stream " , bridge . handle_api_log_stream )
r . add_get ( " /api/log/stream " , bridge . handle_api_log_stream )
r . add_get ( " /api/log/download " , bridge . handle_api_log_download )
r . add_get ( " /api/log/download " , bridge . handle_api_log_download )
r . add_get ( " /serve/ {filename} " , bridge . handle_serve_file )
r . add_get ( " /serve/ {filename} " , bridge . handle_serve_file )