@@ -82,6 +82,8 @@ class KobraXBridge:
" fan_speed " : 0 ,
" light_on " : False ,
" light_brightness " : 80 ,
" taskid " : " -1 " ,
" print_speed_mode " : 2 ,
}
self . _ams_slots : list [ dict ] = [ ]
self . _ams_loaded_slot : int = - 1
@@ -125,6 +127,11 @@ class KobraXBridge:
self . _state [ " curr_layer " ] = d [ " curr_layer " ]
if " total_layers " in d :
self . _state [ " total_layers " ] = d [ " total_layers " ]
if " taskid " in d :
self . _state [ " taskid " ] = str ( d [ " taskid " ] )
settings = d . get ( " settings " ) or { }
if " print_speed_mode " in settings :
self . _state [ " print_speed_mode " ] = int ( settings [ " print_speed_mode " ] )
self . _push_status_update ( )
def _on_info ( self , payload : dict ) :
@@ -149,6 +156,9 @@ class KobraXBridge:
fan = d . get ( " fan_speed_pct " )
if fan is not None :
self . _state [ " fan_speed " ] = int ( fan )
speed_mode = d . get ( " print_speed_mode " )
if speed_mode is not None :
self . _state [ " print_speed_mode " ] = int ( speed_mode )
self . _push_status_update ( )
def _on_file ( self , payload : dict ) :
@@ -634,6 +644,14 @@ main{flex:1;overflow-y:auto;padding:20px}
.btn-cancel { background:#2d0d0d;color:var(--err);border:1px solid var(--err)}
.btn-accent { background:var(--accent);color:#001a24}
.btn-sm { padding:7px 12px;font-size:12px}
.spd-btn { flex:1;border:1.5px solid var(--border);background:var(--raised);color:var(--txt);
border-radius:10px;padding:14px 8px;font-size:13px;font-weight:600;cursor:pointer;
transition:all .15s;display:flex;flex-direction:column;align-items:center;gap:4px}
.spd-btn:hover { border-color:var(--accent);color:var(--accent)}
.spd-btn.spd-active { border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
.spd-btn .spd-icon { font-size:22px}
.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}
/* ── TEMPS ── */
.temp-pair { display:grid;grid-template-columns:1fr 1fr;gap:12px}
@@ -884,9 +902,9 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<!-- Kamera -->
<div class= " card " style= " grid-column:1/-1 " >
<div style= " display:flex;align-items:center;justify-content:space-between;margin-bottom:10px " >
<div class= " card-title " style= " margin-bottom:0 " ><span>📷</span> Kamera </div>
<div class= " card-title " style= " margin-bottom:0 " ><span>📷</span> <span id= " d-card-cam " >Kamera</span> </div>
<div style= " display:flex;align-items:center;gap:10px " >
<span style= " font-size:12px;color:var(--txt2) " >💡 Licht</span>
<span id= " d-lbl-light " style= " font-size:12px;color:var(--txt2) " >💡 Licht</span>
<label class= " toggle " >
<input type= " checkbox " id= " d-light-toggle " onchange= " setLight() " >
<span class= " toggle-track " ></span>
@@ -944,7 +962,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
</div>
</div>
<div class= " temp-block " >
<div class= " temp-label " >Bett</div>
<div class= " temp-label " id= " d-lbl-bed " >Bett</div>
<div class= " temp-row " >
<div class= " temp-val " id= " d-bt " >– </div>
<div class= " temp-unit " >°C</div>
@@ -1002,6 +1020,28 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<div style= " text-align:center;margin-top:8px;font-size:12px;color:var(--txt2) " >Schrittweite: <span id= " step-display " >1</span> mm</div>
</div>
<!-- Print Speed -->
<div class= " card " >
<div class= " card-title " ><span>🏎</span> <span id= " d-card-speed " >Druckgeschwindigkeit</span></div>
<div style= " display:flex;gap:8px;margin-top:4px " >
<button class= " spd-btn " id= " d-spd-1 " onclick= " setSpeed(1) " >
<span class= " spd-icon " >🐢</span>
<span id= " d-spd-lbl-1 " >Leise</span>
</button>
<button class= " spd-btn spd-active " id= " d-spd-2 " onclick= " setSpeed(2) " >
<span class= " spd-icon " >⚡</span>
<span id= " d-spd-lbl-2 " >Normal</span>
</button>
<button class= " spd-btn " id= " d-spd-3 " onclick= " setSpeed(3) " >
<span class= " spd-icon " >🚀</span>
<span id= " d-spd-lbl-3 " >Sport</span>
</button>
</div>
<div class= " spd-bar " style= " margin-top:12px " >
<div class= " spd-bar-fill " id= " d-spd-bar " style= " width:50 % " ></div>
</div>
</div>
<!-- Lüfter -->
<div class= " card " >
<div class= " card-title " ><span>🌀</span> <span id= " d-card-lightfan " >Lüfter</span></div>
@@ -1033,8 +1073,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<span id= " ams-slot-label " style= " min-width:48px;font-size:13px;font-weight:600 " >Slot 1</span>
</div>
<div style= " display:flex;gap:10px " >
<button class= " btn " style= " flex:1 " onclick= " amsFeed(1) " >⬇ Einziehen</button>
<button id= " btn-unload " class= " btn " style= " flex:1 " onclick= " amsFeed(2) " >⬆ Ausziehen</button>
<button class= " btn " style= " flex:1 " onclick= " amsFeed(1) " >⬇ <span class= " lbl-feed " > Einziehen</span></ button>
<button id= " btn-unload " class= " btn " style= " flex:1 " onclick= " amsFeed(2) " >⬆ <span class= " lbl-unload " > Ausziehen</span></ button>
</div>
</div>
</div>
@@ -1061,7 +1101,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
var S= { nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
print_state: ' standby ' ,filename: ' ' ,progress:0,print_duration:0,
curr_layer:0,total_layers:0,printer_name: ' Kobra X ' ,firmware_version: ' – ' ,
camera_url: ' ' ,fan_speed:0,light_on:false,light_brightness:80,ams_slots:[]};
camera_url: ' ' ,fan_speed:0,print_speed_mode:2, light_on:false,light_brightness:80,ams_slots:[]};
var tempHistory= { n:[],b:[]};
var camOn=false;
var currentStep=1;
@@ -1080,7 +1120,9 @@ var LANG_DE={
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_finished: ' Abgeschlossen ' ,kobra_failed: ' Fehler ' ,kobra_canceled: ' Abgebrochen ' ,kobra_offline: ' Offline ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Druck ' ,nav_temps: ' Temperaturen ' ,nav_motion: ' Achsen ' ,nav_ams: ' AMS ' ,nav_extras: ' Licht / Lüfter ' ,nav_console: ' Konsole ' ,
card_progress: ' Fortschritt ' ,card_temps: ' Temperaturen ' ,card_light_fan: ' Lüfter ' ,
card_progress: ' Fortschritt ' ,card_temps: ' Temperaturen ' ,card_light_fan: ' Lüfter ' ,card_speed: ' Druckgeschwindigkeit ' ,card_cam: ' Kamera ' ,
speed_silent: ' 🐢 Leise ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
lbl_light: ' 💡 Licht ' ,lbl_feed: ' Einziehen ' ,lbl_unload: ' Ausziehen ' ,
cam_placeholder: ' 📷 Kamera nicht gestartet ' ,btn_cam_start: ' ▶ Kamera ' ,btn_cam_stop: ' ◼ Kamera ' ,
btn_pause: ' ⏸ Pause ' ,btn_resume: ' ▶ Weiter ' ,btn_cancel: ' ✕ Stopp ' ,
label_nozzle: ' Nozzle ' ,label_bed: ' Bett ' ,label_fan: ' 🌀 Lüfter ' ,label_light: ' 💡 Licht ' ,label_on_off: ' Ein / Aus ' ,label_speed: ' Geschwindigkeit ' ,
@@ -1103,7 +1145,9 @@ var LANG_EN={
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_finished: ' Finished ' ,kobra_failed: ' Error ' ,kobra_canceled: ' Cancelled ' ,kobra_offline: ' Offline ' ,
nav_dashboard: ' Dashboard ' ,nav_print: ' Print ' ,nav_temps: ' Temperatures ' ,nav_motion: ' Motion ' ,nav_ams: ' AMS ' ,nav_extras: ' Light / Fan ' ,nav_console: ' Console ' ,
card_progress: ' Progress ' ,card_temps: ' Temperatures ' ,card_light_fan: ' Fan ' ,
card_progress: ' Progress ' ,card_temps: ' Temperatures ' ,card_light_fan: ' Fan ' ,card_speed: ' Print Speed ' ,card_cam: ' Camera ' ,
speed_silent: ' 🐢 Silent ' ,speed_normal: ' ⚡ Normal ' ,speed_sport: ' 🚀 Sport ' ,
lbl_light: ' 💡 Light ' ,lbl_feed: ' Load ' ,lbl_unload: ' Unload ' ,
cam_placeholder: ' 📷 Camera not started ' ,btn_cam_start: ' ▶ Camera ' ,btn_cam_stop: ' ◼ Camera ' ,
btn_pause: ' ⏸ Pause ' ,btn_resume: ' ▶ Resume ' ,btn_cancel: ' ✕ Stop ' ,
label_nozzle: ' Nozzle ' ,label_bed: ' Bed ' ,label_fan: ' 🌀 Fan ' ,label_light: ' 💡 Light ' ,label_on_off: ' On / Off ' ,label_speed: ' Speed ' ,
@@ -1143,7 +1187,11 @@ function applyLang(){
setText( ' d-card-progress ' ,T.card_progress);
setText( ' d-card-temps ' ,T.card_temps);
setText( ' d-card-lightfan ' ,T.card_light_fan);
setText( ' d-card-speed ' ,T.card_speed);
setText( ' d-card-cam ' ,T.card_cam);
setText( ' d-card-ams ' ,T.panel_ams_title);
setText( ' d-lbl-light ' ,T.lbl_light);
setText( ' d-lbl-bed ' ,T.label_bed);
// Dashboard buttons
setText( ' d-btn-pause ' ,T.btn_pause);
setText( ' d-btn-resume ' ,T.btn_resume);
@@ -1178,6 +1226,13 @@ function applyLang(){
setText( ' lbl-mode-id ' ,T.settings_mode_id);
setText( ' lbl-update-check ' ,T.update_check);
setText( ' lbl-update-apply ' ,T.update_apply);
// Speed buttons
setText( ' d-spd-lbl-1 ' ,T.speed_silent.replace(/^ \ S+ \ s/, ' ' ));
setText( ' d-spd-lbl-2 ' ,T.speed_normal.replace(/^ \ S+ \ s/, ' ' ));
setText( ' d-spd-lbl-3 ' ,T.speed_sport.replace(/^ \ S+ \ s/, ' ' ));
// AMS feed/unload
document.querySelectorAll( ' .lbl-feed ' ).forEach(e=>e.textContent=T.lbl_feed);
document.querySelectorAll( ' .lbl-unload ' ).forEach(e=>e.textContent=T.lbl_unload);
}
function setText(id,txt) { var el=document.getElementById(id);if(el)el.textContent=txt;}
(function() {
@@ -1268,6 +1323,15 @@ function applyState(){
var dfan=document.getElementById( ' d-fan ' );if(dfan)dfan.value=s.fan_speed;
var dfanval=document.getElementById( ' d-fan-val ' );if(dfanval)dfanval.textContent=s.fan_speed;
// speed mode buttons
var spdWidths= { 1:25,2:55,3:90};
[1,2,3].forEach(function(m) {
var b=document.getElementById( ' d-spd- ' +m);
if(b) b.classList.toggle( ' spd-active ' , s.print_speed_mode===m);
});
var spdBar=document.getElementById( ' d-spd-bar ' );
if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+ ' % ' ;
// AMS
if(s.ams_slots&&s.ams_slots.length) {
var html= ' ' ;
@@ -1475,7 +1539,7 @@ function setNozzle(){
function setBed() {
var v=parseFloat(document.getElementById( ' p-bed-inp ' ).value||0);
post( ' /api/temperature ' , { nozzle:S.nozzle_target,bed:v})
.then(function() { clog( ' Bett → ' +v+ ' °C ' , ' msg-ok ' )})
.then(function() { clog(T.label_bed+ ' → ' +v+ ' °C ' , ' msg-ok ' )})
.catch(function(e) { clog( ' Temp-Fehler: ' +e, ' msg-err ' )});
}
@@ -1487,6 +1551,17 @@ function setLight(){
.catch(function(e) { clog( ' Licht-Fehler: ' +e, ' msg-err ' )});
}
// ── Print Speed ──
function setSpeed(mode) {
S.print_speed_mode=mode;
[1,2,3].forEach(function(m) {
var b=document.getElementById( ' d-spd- ' +m);
if(b) b.classList.toggle( ' spd-active ' ,m===mode);
});
post( ' /api/speed ' , { mode:mode})
.catch(function(e) { clog( ' Speed-Fehler: ' +e, ' msg-err ' )});
}
// ── Fan ──
function setFan() {
var v=parseInt(document.getElementById( ' d-fan ' ).value);
@@ -1507,7 +1582,7 @@ function quickFan(v){
function amsFeed(type) {
var slot=parseInt(document.getElementById( ' ams-slot-sel ' ).value);
post( ' /api/ams/feed ' , { slot_index:slot,type:type})
.then(function() { clog((type===1?' Einziehen ' : ' Ausziehen ' )+ ' Slot ' +(slot+1), ' msg-ok ' )})
.then(function() { clog((type===1?T.lbl_feed:T.lbl_unload )+ ' Slot ' +(slot+1), ' msg-ok ' )})
.catch(function(e) { clog( ' AMS-Fehler: ' +e, ' msg-err ' )});
}
@@ -1597,6 +1672,21 @@ function toggleCam(){if(camOn)camStop();else camStart()}
self . _state [ " fan_speed " ] = speed
return web . json_response ( { " result " : " ok " } )
async def handle_api_speed ( self , request ) :
try :
body = await request . json ( )
except Exception :
body = { }
mode = int ( body . get ( " mode " , 2 ) )
loop = asyncio . get_event_loop ( )
taskid = self . _state . get ( " taskid " , " -1 " )
await loop . run_in_executor ( None , lambda : self . client . publish_web (
" print " , " update " ,
{ " taskid " : taskid , " settings " : { " print_speed_mode " : mode } } ,
) )
self . _state [ " print_speed_mode " ] = mode
return web . json_response ( { " result " : " ok " } )
async def handle_api_ams_feed ( self , request ) :
try :
body = await request . json ( )
@@ -1642,18 +1732,29 @@ function toggleCam(){if(camOn)camStop();else camStart()}
nozzle = body . get ( " nozzle " )
bed = body . get ( " bed " )
loop = asyncio . get_event_loop ( )
if nozzle is not None :
n = int ( float ( nozzle ) )
printing = self . _state . get ( " print_state " ) == " printing "
if printing :
# During print: runtime update via web/printer topic, one setting at a time
taskid = self . _state . get ( " taskid " , " -1 " )
if nozzle is not None :
n = int ( float ( nozzle ) )
await loop . run_in_executor ( None , lambda : self . client . publish_web (
" print " , " update " ,
{ " taskid " : taskid , " settings " : { " target_nozzle_temp " : n } } ,
) )
if bed is not None :
b = int ( float ( bed ) )
await loop . run_in_executor ( None , lambda : self . client . publish_web (
" print " , " update " ,
{ " taskid " : taskid , " settings " : { " target_hotbed_temp " : b } } ,
) )
else :
# Idle: standard tempature/set with both values
n = int ( float ( nozzle ) ) if nozzle is not None else int ( self . _state [ " nozzle_target " ] )
b = int ( float ( bed ) ) if bed is not None else int ( self . _state [ " bed_target " ] )
await loop . run_in_executor ( None , lambda : self . client . publish (
" tempature " , " set " ,
{ " type " : 0 , " target_nozzle_temp" : n , " target_hotbed_temp " : 0 } ,
timeout = 0
) )
if bed is not None :
b = int ( float ( bed ) )
await loop . run_in_executor ( None , lambda : self . client . publish (
" tempature " , " set " ,
{ " type " : 1 , " target_hotbed_temp " : b , " target_nozzle_temp " : 0 } ,
{ " target_nozzle_temp " : n , " target_hotbed_temp " : b } ,
timeout = 0
) )
return web . json_response ( { " result " : " ok " } )
@@ -1785,6 +1886,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
" filename " : s [ " filename " ] ,
" camera_url " : s [ " camera_url " ] ,
" fan_speed " : s [ " fan_speed " ] ,
" print_speed_mode " : s [ " print_speed_mode " ] ,
" light_on " : s [ " light_on " ] ,
" light_brightness " : s [ " light_brightness " ] ,
" ams_slots " : self . _ams_slots ,
@@ -2241,6 +2343,7 @@ def build_app(bridge: KobraXBridge) -> web.Application:
# New API endpoints
r . add_post ( " /api/light " , bridge . handle_api_light )
r . add_post ( " /api/fan " , bridge . handle_api_fan )
r . add_post ( " /api/speed " , bridge . handle_api_speed )
r . add_post ( " /api/ams/feed " , bridge . handle_api_ams_feed )
r . add_post ( " /api/axis " , bridge . handle_api_axis )
r . add_post ( " /api/temperature " , bridge . handle_api_temperature )