@@ -1102,6 +1102,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 +1156,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 " >
@@ -1308,7 +1336,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 +1345,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>
@@ -1374,7 +1413,11 @@ 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 '
};
};
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 ' ,
@@ -1402,7 +1445,11 @@ 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 '
};
};
var currentLang= ' de ' ;
var currentLang= ' de ' ;
var T=LANG_DE;
var T=LANG_DE;
@@ -1479,6 +1526,12 @@ 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);
}
}
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 +1563,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,12 +1590,37 @@ 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;
});
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;
}
}
@@ -1657,6 +1737,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 +1745,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 +1850,60 @@ 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 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 +2221,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 ( )
@@ -2850,6 +3016,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 )