From e28a91b64d2382eb788a4064844623e44b16f930 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 17:39:46 -1000 Subject: [PATCH 01/13] feat: ACE support, filament section visual tweaks --- .gitignore | 2 + kobrax_moonraker_bridge.py | 374 ++++++++++++++++++++++++++++++------- 2 files changed, 309 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index d86da6a..7beb455 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ dist/ releases/*/kx-bridge releases/*/extract_credentials releases/*/extract_credentials.exe +config/config.ini +data \ No newline at end of file diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 3a9c73a..dd6eff5 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -446,9 +446,13 @@ class KobraXBridge: "print_speed_mode": 2, "connection_error": "", "file_ready": "", + "filament_mode": "toolhead", } - self._ams_slots: list[dict] = [] - self._ams_loaded_slot: int = -1 + self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id + self._ams_loaded_slot: int = -1 # global slot index of currently loaded slot + self._pending_load_slot: int = -1 # global slot index requested via /api/ams/feed type=1 + self._head_tools_model: int = -1 + self._filament_mode: str = "toolhead" self._last_uploaded_file: str = "" self._store = store if store is not None else GCodeStore(args.data_dir) self._serve_dir_path: str = self._store._gcode_dir @@ -601,34 +605,179 @@ class KobraXBridge: log.warning(f"update_file_objects fehlgeschlagen: {e}") self._push_status_update() + @staticmethod + def _detect_filament_mode(boxes: list, head_tools_model: int = -1) -> str: + """Detect active filament topology mode. + + Modes: + - toolhead: only toolhead slots + - ace_direct: ACE channels directly mapped (no toolhead box present) + - ace_hub: toolhead + ACE via hub (slot 4 as hub path) + """ + toolhead = any(b.get("id") == -1 for b in boxes) + ace = any(b.get("id", -1) >= 0 for b in boxes) + if ace and toolhead: + return "ace_hub" + if ace: + return "ace_direct" + return "toolhead" + + @staticmethod + def _aggregate_slots(boxes: list, mode: str = "toolhead") -> tuple: + """Aggregate multi_color_box list into a flat global slot list.""" + toolhead = next((b for b in boxes if b.get("id") == -1), None) + ace_boxes = sorted( + [b for b in boxes if b.get("id", -1) >= 0], + key=lambda b: b["id"] + ) + + global_slots: list = [] + global_loaded: int = -1 + + if mode == "toolhead": + if toolhead: + for local_idx, s in enumerate(toolhead.get("slots") or []): + s = dict(s) + s["global_index"] = local_idx + s["box_id"] = -1 + global_slots.append(s) + loaded = toolhead.get("loaded_slot", -1) + if loaded >= 0: + global_loaded = loaded + return global_slots, global_loaded + + if mode == "ace_direct": + for ace in ace_boxes: + ace_id = ace["id"] + base = ace_id * 4 + for local_idx, s in enumerate(ace.get("slots") or []): + s = dict(s) + s["global_index"] = base + local_idx + s["box_id"] = ace_id + global_slots.append(s) + ace_loaded = ace.get("loaded_slot", -1) + if ace_loaded >= 0: + global_loaded = base + ace_loaded + return global_slots, global_loaded + + # ace_hub + if toolhead: + for local_idx, s in enumerate((toolhead.get("slots") or [])[:3]): + s = dict(s) + s["global_index"] = local_idx + s["box_id"] = -1 + global_slots.append(s) + th_loaded = toolhead.get("loaded_slot", -1) + if 0 <= th_loaded <= 2: + global_loaded = th_loaded + + for ace in ace_boxes: + ace_id = ace["id"] + base = 3 + ace_id * 4 + for local_idx, s in enumerate(ace.get("slots") or []): + s = dict(s) + s["global_index"] = base + local_idx + s["box_id"] = ace_id + global_slots.append(s) + ace_loaded = ace.get("loaded_slot", -1) + if ace_loaded >= 0: + global_loaded = base + ace_loaded + + return global_slots, global_loaded + + def _global_to_box_slot(self, global_index: int) -> tuple: + """Convert a global slot index to (box_id, local_slot_index).""" + for s in self._ams_slots: + if s.get("global_index") == global_index: + return s.get("box_id", -1), s.get("index", global_index) + + ace_present = any(s.get("box_id", -1) >= 0 for s in self._ams_slots) + if self._filament_mode == "ace_direct" and ace_present: + return global_index // 4, global_index % 4 + if not ace_present or global_index < 3: + return -1, global_index + offset = global_index - 3 + return offset // 4, offset % 4 + + def _box_local_to_global(self, box_id: int, local_slot: int, boxes: list) -> int: + """Convert (box_id, local slot) to global slot index for current topology.""" + if box_id == -1: + return local_slot + if self._filament_mode == "ace_direct": + return box_id * 4 + local_slot + return 3 + box_id * 4 + local_slot + + def _slot_activity_map(self, boxes: list, global_loaded: int = -1) -> dict: + """Build {global_slot_index: loading|unloading} from feed_status data.""" + activity: dict = {} + for box in boxes: + fs = box.get("feed_status") or {} + current_status = int(fs.get("current_status", -1)) + local_slot = int(fs.get("slot_index", -1)) + feed_type = int(fs.get("type", -1)) + if current_status in (-1, 10, 11) or local_slot < 0: + continue + box_slots = box.get("slots") or [] + if local_slot >= len(box_slots) or (box_slots[local_slot] or {}).get("status") != 5: + continue + if feed_type == 1: + act = "loading" + elif feed_type == 2: + act = "unloading" + else: + continue + global_slot = self._box_local_to_global(int(box.get("id", -1)), local_slot, boxes) + if feed_type == 1 and self._pending_load_slot >= 0 and global_slot != self._pending_load_slot: + # Ignore transient firmware-reported loading slots that differ from the requested target. + if global_loaded >= 0 and global_loaded != self._pending_load_slot: + activity[global_loaded] = "unloading" + continue + if feed_type == 1 and global_loaded >= 0 and global_slot != global_loaded: + # During a slot swap the firmware reports the target slot immediately, + # while the previously loaded slot is still being unloaded first. + activity[global_loaded] = "unloading" + activity[global_slot] = act + return activity + def _on_multicolor_box(self, payload: dict): - boxes = (payload.get("data") or {}).get("multi_color_box") or [] + data = payload.get("data") or {} + boxes = data.get("multi_color_box") or [] if not boxes: return - box = boxes[0] - slots = box.get("slots") or [] - loaded = box.get("loaded_slot", -1) - if loaded != -1: - self._ams_loaded_slot = loaded + self._head_tools_model = int(data.get("head_tools_model", self._head_tools_model)) + self._filament_mode = self._detect_filament_mode(boxes, self._head_tools_model) + self._state["filament_mode"] = self._filament_mode + + global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) + self._ams_loaded_slot = global_loaded + if self._pending_load_slot >= 0 and global_loaded == self._pending_load_slot: + self._pending_load_slot = -1 + activity_map = self._slot_activity_map(boxes, global_loaded) + for s in global_slots: + s["activity"] = activity_map.get(s.get("global_index"), "") + # Tip-Forming: nach Einziehen (status=10) oder Ausziehen (status=11) - # schickt der originale Slicer automatisch type=3 (Extruder-Rückzug) - fs = box.get("feed_status") or {} - current_status = fs.get("current_status") - slot_index = fs.get("slot_index", 0) - if current_status in (10, 11): - import threading - def _tip_form(): - import time; time.sleep(2) - self.client.publish( - "multiColorBox", "feedFilament", - {"multi_color_box": [{"id": -1, "feed_status": {"slot_index": slot_index, "type": 3}}]}, - timeout=0 - ) - log.info(f"Tip-Forming (type=3) nach status={current_status} slot={slot_index}") - threading.Thread(target=_tip_form, daemon=True).start() - if slots: - self._ams_slots = slots - log.info(f"AMS-Slots empfangen: {len(slots)}, loaded_slot={self._ams_loaded_slot}") + # schickt der originale Slicer automatisch type=3 (Extruder-Rückzug). + # Check ALL boxes so ACE-triggered events are handled correctly. + for box in boxes: + fs = box.get("feed_status") or {} + current_status = fs.get("current_status") + slot_index = fs.get("slot_index", 0) + box_id = box.get("id", -1) + if current_status in (10, 11): + def _tip_form(bi=box_id, si=slot_index, cs=current_status): + import time; time.sleep(2) + self.client.publish( + "multiColorBox", "feedFilament", + {"multi_color_box": [{"id": bi, "feed_status": {"slot_index": si, "type": 3}}]}, + timeout=0 + ) + log.info(f"Tip-Forming (type=3) nach status={cs} box={bi} slot={si}") + threading.Thread(target=_tip_form, daemon=True).start() + + if global_slots: + self._ams_slots = global_slots + log.info(f"AMS-Slots empfangen: {len(global_slots)}, loaded_slot={self._ams_loaded_slot}") self._push_status_update() def _on_light(self, payload: dict): @@ -758,8 +907,8 @@ class KobraXBridge: "gate_filament_name": [""] * num_gates, "gate_spool_id": [-1] * num_gates, "ttg_map": list(range(num_gates)), - "tool": max(self._ams_loaded_slot, 0), - "gate": max(self._ams_loaded_slot, 0), + "tool": self._ams_loaded_slot, + "gate": self._ams_loaded_slot, } def _build_printer_objects(self) -> dict: @@ -1620,11 +1769,26 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background: .home-btns{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;justify-content:center} /* ── AMS ── */ -.ams-slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} +.ams-slots{display:flex;flex-direction:column;gap:12px} +.ams-box-group{} +.ams-box-label{font-size:11px;font-weight:700;color:var(--txt2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;padding-left:2px} +.ams-box-slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} .ams-slot{background:var(--raised);border-radius:10px;padding:10px;text-align:center; border:2px solid transparent;transition:.2s;position:relative} .ams-slot.active{border-color:var(--slot-color,var(--accent)); box-shadow:0 0 12px rgba(var(--slot-rgb,0,200,255),.3)} +.ams-slot.loaded{border-color:var(--ok)!important; + box-shadow:0 0 0 2px rgba(64,220,120,.35),0 0 14px rgba(64,220,120,.35)} +.ams-slot.loading{border-color:var(--ok)!important;animation:amsPulseGreen 1s ease-in-out infinite} +.ams-slot.unloading{border-color:var(--err)!important;animation:amsPulseRed 1s ease-in-out infinite} +@keyframes amsPulseGreen{0%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}50%{box-shadow:0 0 0 4px rgba(64,220,120,.25),0 0 18px rgba(64,220,120,.45)}100%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}} +@keyframes amsPulseRed{0%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}50%{box-shadow:0 0 0 4px rgba(230,80,80,.25),0 0 18px rgba(230,80,80,.45)}100%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}} +.ams-slot-bridge{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px; + border:1px dashed var(--border);background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.01)); + color:var(--txt2);min-height:106px} +.ams-slot-bridge .bridge-chip{width:58px;height:58px;border:1px solid rgba(255,255,255,.14);border-radius:50%; + display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:var(--txt2); + font-size:13px;font-weight:700;letter-spacing:.04em} .slot-circle{width:36px;height:36px;border-radius:50%;margin:0 auto 6px;border:2px solid rgba(255,255,255,.15)} .slot-label{font-size:11px;color:var(--txt2);font-family:var(--mono)} .slot-material{font-size:12px;font-weight:600;margin-bottom:2px} @@ -1731,8 +1895,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; .temp-pair{grid-template-columns:1fr} .temp-card-inner{grid-template-columns:1fr} - /* AMS: 2 Spalten */ - .ams-slots{grid-template-columns:repeat(2,1fr)} + /* AMS: 2 Spalten auf kleinen Screens */ + .ams-box-slots{grid-template-columns:repeat(2,1fr)} /* Joypad etwas kleiner */ .joypad{grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:5px} @@ -2097,7 +2261,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
+ oninput="(function(v){var s=(window._amsSlots||[])[parseInt(v)]||{};var g=s.global_index!=null?s.global_index:parseInt(v);document.getElementById('ams-slot-label').textContent='Slot '+(g+1);})(this.value)"> Slot 1
@@ -2199,7 +2363,8 @@ 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,remain_time:0, curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', - camera_url:'',fan_speed:0,print_speed_mode:2,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:[],filament_mode:'toolhead'}; var tempHistory={n:[],b:[]}; var camOn=false; var currentStep=1; @@ -2721,26 +2886,77 @@ function applyState(){ var spdBar=document.getElementById('d-spd-bar'); if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%'; + var amsTitle=document.getElementById('d-card-ams'); + if(amsTitle){ + var baseTitle=T.card_ams||'AMS / Filamentbox'; + var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'}; + var modeTxt=modeMap[s.filament_mode]||''; + amsTitle.textContent=modeTxt?(baseTitle+' · '+modeTxt):baseTitle; + } + // AMS if(s.ams_slots&&s.ams_slots.length){ window._amsSlots=s.ams_slots; - var html=''; + // Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...) + var boxMap={}; s.ams_slots.forEach(function(slot,i){ - var empty=slot.status!==5; - var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]); - var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')'; - var active=slot.status===1||slot.active; - var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); - var idx=slot.index!=null?slot.index:i; - html+='
' - +'
' - +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' - +'
Slot '+(idx+1)+'
' - +'
'+pct+'
' - +'
' - +'
'; + var bid=slot.box_id!=null?slot.box_id:-1; + if(!boxMap[bid])boxMap[bid]=[]; + boxMap[bid].push({slot:slot,arrIdx:i}); + }); + var boxIds=Object.keys(boxMap).map(Number).sort(function(a,b){return a-b}); + var acePresent=boxIds.some(function(b){return b>=0;}); + var html=''; + boxIds.forEach(function(bid){ + var entries=boxMap[bid]; + var label=bid===-1 + ?(acePresent?'Toolhead (Slots 1–3)':'Toolhead') + :('ACE '+(bid+1)); + html+='
' + +'
'+label+'
' + +'
'; + entries.forEach(function(e){ + var slot=e.slot;var i=e.arrIdx; + var empty=slot.status!==5; + var rgb=empty?[80,80,80]:(Array.isArray(slot.color)?slot.color:[128,128,128]); + var col='rgb('+rgb[0]+','+rgb[1]+','+rgb[2]+')'; + var globalIdx=slot.global_index!=null?slot.global_index:i; + var active=slot.status===1||slot.active; + var loaded=(s.ams_loaded_slot!=null&&s.ams_loaded_slot>=0&&globalIdx===s.ams_loaded_slot); + var activity=(slot.activity||''); + var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); + var slotLabel='Slot '+(globalIdx+1); + if(bid>=0){ + slotLabel='ACE '+(bid+1)+'.'+(slot.index!=null?slot.index+1:(i-2)); + } + html+='
' + +'
' + +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' + +'
'+slotLabel+'
' + +'
'+pct+'
' + +'
' + +'
'; + }); + if(bid===-1&&acePresent){ + html+='
' + +'
ACE
' + +'
'; + } + html+='
'; }); document.getElementById('ams-slots').innerHTML=html; + // Update feed/unload slot slider range + var sel=document.getElementById('ams-slot-sel'); + if(sel){ + var maxSlot=s.ams_slots.length-1; + sel.max=maxSlot; + if(parseInt(sel.value)>maxSlot){ + sel.value=0; + var lbl=document.getElementById('ams-slot-label'); + if(lbl)lbl.textContent='Slot 1'; + } + } } // camera overlay @@ -2847,9 +3063,9 @@ 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 globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i); + _slotEditIndex=globalIdx; + document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+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'); @@ -3103,9 +3319,11 @@ function quickFan(v){ // ── AMS ── 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?T.lbl_feed:T.lbl_unload)+' Slot '+(slot+1),'msg-ok')}) + var i=parseInt(document.getElementById('ams-slot-sel').value); + var slot=(window._amsSlots||[])[i]||{}; + var globalIdx=slot.global_index!=null?slot.global_index:i; + post('/api/ams/feed',{slot_index:globalIdx,type:type}) + .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')}) .catch(function(e){clog('AMS-Fehler: '+e,'msg-err')}); } @@ -3830,25 +4048,26 @@ function loadPrinterTab(){ body = await request.json() except Exception: body = {} - index = int(body.get("index", 0)) + index = int(body.get("index", 0)) # global slot index 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) + box_id, local_slot = self._global_to_box_slot(index) 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}]}]}, + {"multi_color_box": [{"id": box_id, "slots": [{"index": local_slot, "type": mat, "color": color}]}]}, timeout=5 ) - log.info(f"setInfo slot={index} type={mat} color={color} → {resp}") + log.info(f"setInfo global={index} box={box_id} local_slot={local_slot} 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: + if s.get("global_index") == index: s["type"] = mat s["color"] = color break @@ -3861,17 +4080,20 @@ function loadPrinterTab(){ body = {} slot_index = int(body.get("slot_index", 0)) feed_type = int(body.get("type", 1)) + if feed_type == 1: + self._pending_load_slot = slot_index # Ausziehen (type=2): wenn kein Slot explizit gewählt, den zuletzt geladenen nehmen if feed_type == 2 and self._ams_loaded_slot >= 0: slot_index = self._ams_loaded_slot + box_id, local_slot = self._global_to_box_slot(slot_index) loop = asyncio.get_event_loop() def _send(): resp = self.client.publish( "multiColorBox", "feedFilament", - {"multi_color_box": [{"id": -1, "feed_status": {"slot_index": slot_index, "type": feed_type}}]}, + {"multi_color_box": [{"id": box_id, "feed_status": {"slot_index": local_slot, "type": feed_type}}]}, timeout=5 ) - log.info(f"feedFilament type={feed_type} slot={slot_index} loaded_slot={self._ams_loaded_slot} → {resp}") + log.info(f"feedFilament type={feed_type} global_slot={slot_index} box={box_id} local_slot={local_slot} loaded_slot={self._ams_loaded_slot} → {resp}") await loop.run_in_executor(None, _send) return web.json_response({"result": "ok"}) @@ -4104,6 +4326,7 @@ function loadPrinterTab(){ "light_brightness": s["light_brightness"], "ams_slots": self._ams_slots, "ams_loaded_slot": self._ams_loaded_slot, + "filament_mode": s.get("filament_mode", self._filament_mode), "thumbnail": self._thumbnail_b64, "connection_error": s["connection_error"], "file_ready": s["file_ready"], @@ -4145,11 +4368,19 @@ function loadPrinterTab(){ """Frische Slot-Daten per getInfo holen, Fallback auf gecachte.""" resp = self.client.publish("multiColorBox", "getInfo", None, timeout=5) if resp and resp.get("data"): - boxes = resp["data"].get("multi_color_box") or [] + data = resp["data"] + self._head_tools_model = int(data.get("head_tools_model", self._head_tools_model)) + boxes = data.get("multi_color_box") or [] if boxes: - slots = boxes[0].get("slots") or [] - if slots: - self._ams_slots = slots + self._filament_mode = self._detect_filament_mode(boxes, self._head_tools_model) + self._state["filament_mode"] = self._filament_mode + global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) + activity_map = self._slot_activity_map(boxes, global_loaded) + for s in global_slots: + s["activity"] = activity_map.get(s.get("global_index"), "") + if global_slots: + self._ams_slots = global_slots + self._ams_loaded_slot = global_loaded return self._ams_slots # ─── Settings ──────────────────────────────────────────────────────────── @@ -4726,10 +4957,19 @@ function loadPrinterTab(){ self._on_print(print_r) box = self.client.query_multicolor_box() if box: - boxes = (box.get("data") or {}).get("multi_color_box") or [] - slots = boxes[0].get("slots") or [] if boxes else [] - if slots: - self._ams_slots = slots + data = box.get("data") or {} + self._head_tools_model = int(data.get("head_tools_model", self._head_tools_model)) + boxes = data.get("multi_color_box") or [] + if boxes: + self._filament_mode = self._detect_filament_mode(boxes, self._head_tools_model) + self._state["filament_mode"] = self._filament_mode + global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) + activity_map = self._slot_activity_map(boxes, global_loaded) + for s in global_slots: + s["activity"] = activity_map.get(s.get("global_index"), "") + if global_slots: + self._ams_slots = global_slots + self._ams_loaded_slot = global_loaded except Exception as e: log.warning(f"Poll-Fehler: {e}") # Prüfen ob Drucker wirklich weg ist -- 2.49.1 From c6cc4ac446ec0fadfebd53e69a9ca303096f42bc Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 18:08:09 -1000 Subject: [PATCH 02/13] feat: update AMS section UI and functionality for slot editing --- kobrax_moonraker_bridge.py | 80 +++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index dd6eff5..f3c9ebc 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -2054,6 +2054,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; 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">
+ @@ -2252,23 +2253,10 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
-
AMS / Filamentbox
+
Filament
Keine AMS-Daten empfangen
-
-
Slot auswählen
-
- - Slot 1 -
-
- - -
-
@@ -2393,7 +2381,7 @@ var LANG_DE={ label_set:'Setzen',label_off:'Aus', panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:', panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus', - panel_ams_title:'AMS / Filamentbox',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer', + panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer', panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', panel_console_title:'Ereignis-Log', log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:', @@ -2407,6 +2395,7 @@ var LANG_DE={ btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen', lbl_conn_error:'Verbindungsfehler:', slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material', + slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen', slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…', slot_edit_ok:'AMS Slot', log_dir_all:'Alle', @@ -2454,7 +2443,7 @@ var LANG_EN={ label_set:'Set',label_off:'Off', panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:', panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off', - panel_ams_title:'AMS / Filament Box',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty', + panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty', panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop', panel_console_title:'Event Log', log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:', @@ -2468,6 +2457,7 @@ var LANG_EN={ btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect', lbl_conn_error:'Connection error:', slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material', + slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload', slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…', slot_edit_ok:'AMS Slot', log_dir_all:'All', @@ -2663,6 +2653,7 @@ function applyLang(){ 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); + updateSlotEditFeedButton(); 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); @@ -2888,10 +2879,10 @@ function applyState(){ var amsTitle=document.getElementById('d-card-ams'); if(amsTitle){ - var baseTitle=T.card_ams||'AMS / Filamentbox'; + var baseTitle=T.card_ams||'Filament'; var modeMap={toolhead:'Toolhead',ace_direct:'ACE Direct',ace_hub:'ACE Hub'}; var modeTxt=modeMap[s.filament_mode]||''; - amsTitle.textContent=modeTxt?(baseTitle+' · '+modeTxt):baseTitle; + amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle; } // AMS @@ -2946,17 +2937,6 @@ function applyState(){ html+=''; }); document.getElementById('ams-slots').innerHTML=html; - // Update feed/unload slot slider range - var sel=document.getElementById('ams-slot-sel'); - if(sel){ - var maxSlot=s.ams_slots.length-1; - sel.max=maxSlot; - if(parseInt(sel.value)>maxSlot){ - sel.value=0; - var lbl=document.getElementById('ams-slot-label'); - if(lbl)lbl.textContent='Slot 1'; - } - } } // camera overlay @@ -3060,11 +3040,23 @@ function closeSettings(){ // ── AMS Slot Edit ── var _slotEditIndex=-1; +var _slotEditLoaded=false; var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; +function updateSlotEditFeedButton(){ + var btn=document.getElementById('btn-slot-edit-feed'); + if(!btn)return; + if(_slotEditIndex<0){ + btn.style.display='none'; + return; + } + btn.style.display=''; + btn.textContent=_slotEditLoaded?(T.slot_edit_unload||'⬆ Unload'):(T.slot_edit_load||'⬇ Load'); +} function openSlotEdit(i){ var slot=(window._amsSlots||[])[i]||{}; var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i); _slotEditIndex=globalIdx; + _slotEditLoaded=(S.ams_loaded_slot!=null&&S.ams_loaded_slot===globalIdx); document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+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(''); @@ -3079,11 +3071,24 @@ function openSlotEdit(i){ +'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+''; }).join(''); + updateSlotEditFeedButton(); document.getElementById('slot-edit-modal').classList.add('open'); } function closeSlotEdit(){ + _slotEditIndex=-1; document.getElementById('slot-edit-modal').classList.remove('open'); } +function slotEditFeed(){ + if(_slotEditIndex<0)return; + var type=_slotEditLoaded?2:1; + amsFeed(type,_slotEditIndex) + .then(function(){ + _slotEditLoaded=!_slotEditLoaded; + updateSlotEditFeedButton(); + poll(); + }) + .catch(function(){}); +} function startReadyFile(){ var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} @@ -3318,13 +3323,18 @@ function quickFan(v){ } // ── AMS ── -function amsFeed(type){ - var i=parseInt(document.getElementById('ams-slot-sel').value); - var slot=(window._amsSlots||[])[i]||{}; - var globalIdx=slot.global_index!=null?slot.global_index:i; - post('/api/ams/feed',{slot_index:globalIdx,type:type}) +function amsFeed(type,slotIndex){ + var globalIdx; + if(typeof slotIndex==='number'&&slotIndex>=0){ + globalIdx=slotIndex; + }else{ + var i=parseInt(document.getElementById('ams-slot-sel').value); + var slot=(window._amsSlots||[])[i]||{}; + globalIdx=slot.global_index!=null?slot.global_index:i; + } + return post('/api/ams/feed',{slot_index:globalIdx,type:type}) .then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(globalIdx+1),'msg-ok')}) - .catch(function(e){clog('AMS-Fehler: '+e,'msg-err')}); + .catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;}); } // ── Camera ── -- 2.49.1 From aea115faa001f9b39f442f814291f8faf46c11a3 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 18:46:56 -1000 Subject: [PATCH 03/13] ace dryer, start, stop, temp set/current, time set/remaning, status, humidity monitor. groundwork --- kobrax_moonraker_bridge.py | 236 ++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 5 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index f3c9ebc..96bbac6 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -447,10 +447,12 @@ class KobraXBridge: "connection_error": "", "file_ready": "", "filament_mode": "toolhead", + "ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None}, } self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id self._ams_loaded_slot: int = -1 # global slot index of currently loaded slot self._pending_load_slot: int = -1 # global slot index requested via /api/ams/feed type=1 + self._ace_box_ids: list[int] = [] # detected ACE unit IDs (0..3) self._head_tools_model: int = -1 self._filament_mode: str = "toolhead" self._last_uploaded_file: str = "" @@ -750,6 +752,7 @@ class KobraXBridge: global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) self._ams_loaded_slot = global_loaded + self._update_ace_drying_state(data, boxes) if self._pending_load_slot >= 0 and global_loaded == self._pending_load_slot: self._pending_load_slot = -1 activity_map = self._slot_activity_map(boxes, global_loaded) @@ -780,6 +783,69 @@ class KobraXBridge: log.info(f"AMS-Slots empfangen: {len(global_slots)}, loaded_slot={self._ams_loaded_slot}") self._push_status_update() + def _update_ace_drying_state(self, data: dict, boxes: list): + """Extract ACE drying state from multiColorBox report/getInfo payloads.""" + ace_ids = sorted({int(b.get("id", -1)) for b in boxes if int(b.get("id", -1)) >= 0}) + self._ace_box_ids = [i for i in ace_ids if 0 <= i <= 3] + + def _humidity_from(src: dict, default=None): + for k in ("humidity", "current_humidity", "cur_humidity", "relative_humidity", "humidity_value"): + v = src.get(k) + if v is not None: + try: + return float(v) + except Exception: + return default + return default + + per_unit: list[dict] = [] + for box in boxes: + bid = int(box.get("id", -1)) + if bid < 0: + continue + bs = box.get("drying_status") or box.get("drying_settings") + if isinstance(bs, dict): + per_unit.append({ + "id": bid, + "status": int(bs.get("status", 0)), + "target_temp": int(bs.get("target_temp", 0)), + "duration": int(bs.get("duration", 0)), + "remain_time": int(bs.get("remain_time", 0)), + "humidity": _humidity_from(bs), + }) + + src = data.get("drying_status") or data.get("drying_settings") + if not isinstance(src, dict): + for box in boxes: + if int(box.get("id", -1)) < 0: + continue + cand = box.get("drying_status") or box.get("drying_settings") + if isinstance(cand, dict): + src = cand + break + + if isinstance(src, dict): + cur = self._state.get("ace_drying") or {} + self._state["ace_drying"] = { + "status": int(src.get("status", cur.get("status", 0))), + "target_temp": int(src.get("target_temp", cur.get("target_temp", 0))), + "duration": int(src.get("duration", cur.get("duration", 0))), + "remain_time": int(src.get("remain_time", cur.get("remain_time", 0))), + "humidity": _humidity_from(src, cur.get("humidity")), + "units": per_unit, + } + elif per_unit: + active = [u for u in per_unit if u.get("status", 0)] + primary = active[0] if active else per_unit[0] + self._state["ace_drying"] = { + "status": int(primary.get("status", 0)), + "target_temp": int(primary.get("target_temp", 0)), + "duration": int(primary.get("duration", 0)), + "remain_time": int(primary.get("remain_time", 0)), + "humidity": primary.get("humidity"), + "units": per_unit, + } + def _on_light(self, payload: dict): d = payload.get("data") or {} self._state["light_on"] = bool(d.get("status", 0)) @@ -2251,6 +2317,25 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; + +
Filament
@@ -2352,7 +2437,7 @@ var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80, - ams_slots:[],filament_mode:'toolhead'}; + ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,units:[]}}; var tempHistory={n:[],b:[]}; var camOn=false; var currentStep=1; @@ -2374,6 +2459,7 @@ var LANG_DE={ 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', lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', + card_ace_dry:'ACE Trocknung',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', 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', @@ -2436,6 +2522,7 @@ var LANG_EN={ card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer', speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', + card_ace_dry:'ACE Drying',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', 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', @@ -2590,6 +2677,7 @@ 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-ace-dry',T.card_ace_dry); setText('d-card-speed',T.card_speed); setText('d-card-cam',T.card_cam); setText('d-card-ams',T.panel_ams_title); @@ -2647,6 +2735,11 @@ function applyLang(){ // 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); + setText('ace-dry-start',T.ace_dry_start); + setText('ace-dry-stop',T.ace_dry_stop); + setText('d-ace-dry-humidity-label',(T.ace_dry_humidity||'Humidity')+':'); + var adTemp=document.getElementById('ace-dry-temp');if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp); + var adDur=document.getElementById('ace-dry-duration');if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration); // conn-btn text (nur wenn nicht im Übergangszustand) updateConnBtn(); // Slot-Edit-Dialog @@ -2885,6 +2978,31 @@ function applyState(){ amsTitle.textContent=modeTxt?(baseTitle+' - '+modeTxt):baseTitle; } + var acePresent=((s.ace_units||[]).length>0) + ||(s.filament_mode&&s.filament_mode!=='toolhead') + ||((s.ams_slots||[]).some(function(sl){return (sl.box_id||-1)>=0;})); + var aceCard=document.getElementById('d-ace-dry-card'); + if(aceCard)aceCard.style.display=acePresent?'':'none'; + var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null}; + var dryStatus=document.getElementById('d-ace-dry-status'); + if(dryStatus){ + if(dry.status){ + var rem=(dry.remain_time||0); + dryStatus.textContent=(T.ace_dry_status_on||'Status: Active')+(rem>0?(' - '+(T.ace_dry_status_remaining||'Remaining')+': '+rem+' min'):''); + }else{ + dryStatus.textContent=T.ace_dry_status_off||'Status: Off'; + } + } + var dryHumidity=document.getElementById('d-ace-dry-humidity'); + if(dryHumidity){ + var hv=(dry.humidity===null||dry.humidity===undefined||dry.humidity==='')?null:Number(dry.humidity); + dryHumidity.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%'); + } + var dryTemp=document.getElementById('ace-dry-temp'); + if(dryTemp&&dry.target_temp>0&&String(dryTemp.value||'')==='55')dryTemp.value=dry.target_temp; + var dryDur=document.getElementById('ace-dry-duration'); + if(dryDur&&dry.duration>0&&String(dryDur.value||'')==='240')dryDur.value=dry.duration; + // AMS if(s.ams_slots&&s.ams_slots.length){ window._amsSlots=s.ams_slots; @@ -3337,6 +3455,32 @@ function amsFeed(type,slotIndex){ .catch(function(e){clog('AMS-Fehler: '+e,'msg-err');throw e;}); } +function aceDryStart(){ + var t=parseInt(document.getElementById('ace-dry-temp').value||55); + var d=parseInt(document.getElementById('ace-dry-duration').value||240); + t=Math.max(30,Math.min(80,t)); + d=Math.max(10,Math.min(1440,d)); + return post('/api/ace/dry',{action:'start',target_temp:t,duration:d}) + .then(function(r){return r.json();}) + .then(function(r){ + if(r.error){throw new Error(r.error);} + clog((T.card_ace_dry||'ACE Drying')+': '+(T.ace_dry_start||'start')+' ('+t+'°C, '+d+' min)','msg-ok'); + poll(); + }) + .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); +} + +function aceDryStop(){ + return post('/api/ace/dry',{action:'stop'}) + .then(function(r){return r.json();}) + .then(function(r){ + if(r.error){throw new Error(r.error);} + clog((T.card_ace_dry||'ACE Drying')+': '+(T.ace_dry_stop||'stop'),'msg-ok'); + poll(); + }) + .catch(function(e){clog('ACE-Fehler: '+e,'msg-err');}); +} + // ── Camera ── function camStart(){ var img=document.getElementById('cam-img'); @@ -4107,26 +4251,103 @@ function loadPrinterTab(){ await loop.run_in_executor(None, _send) return web.json_response({"result": "ok"}) + async def handle_api_ace_dry(self, request): + try: + body = await request.json() + except Exception: + body = {} + + action = str(body.get("action", "start")).lower() + if action not in ("start", "stop"): + return web.json_response({"error": "action must be 'start' or 'stop'"}, status=400) + + ace_ids = [i for i in self._ace_box_ids if 0 <= i <= 3] + if not ace_ids: + ace_ids = sorted({ + int(s.get("box_id", -1)) + for s in self._ams_slots + if 0 <= int(s.get("box_id", -1)) <= 3 + }) + if not ace_ids and self._state.get("filament_mode") != "toolhead": + ace_ids = [0] + if not ace_ids: + return web.json_response({"error": "ACE not detected"}, status=400) + + if action == "start": + target_temp = int(body.get("target_temp", 45)) + duration = int(body.get("duration", 240)) + target_temp = max(30, min(80, target_temp)) + duration = max(10, min(24 * 60, duration)) + humidity = (self._state.get("ace_drying") or {}).get("humidity") + drying_status = { + "status": 1, + "target_temp": target_temp, + "duration": duration, + "remain_time": duration, + } + ui_state = { + "status": 1, + "target_temp": target_temp, + "duration": duration, + "remain_time": duration, + "humidity": humidity, + } + else: + drying_status = {"status": 0} + humidity = (self._state.get("ace_drying") or {}).get("humidity") + ui_state = { + "status": 0, + "target_temp": 0, + "duration": 0, + "remain_time": 0, + "humidity": humidity, + } + + payload = { + "multi_color_box": [ + {"id": bid, "drying_status": dict(drying_status)} + for bid in ace_ids + ] + } + + loop = asyncio.get_event_loop() + + def _send(): + return self.client.publish("multiColorBox", "setDry", payload, timeout=5) + + resp = await loop.run_in_executor(None, _send) + if resp is None: + return web.json_response({"error": "No response from printer"}, status=504) + if int(resp.get("code", 200)) != 200: + return web.json_response({"error": f"Printer rejected command: {resp}"}, status=502) + + self._state["ace_drying"] = ui_state + self._state_dirty = True + return web.json_response({"result": "ok"}) + async def handle_api_axis(self, request): try: body = await request.json() except Exception: body = {} - action = body.get("action", "move") + loop = asyncio.get_event_loop() - if action == "turnOff": + action = str(body.get("action", "")).lower() + + if action == "turnoff": await loop.run_in_executor(None, lambda: self.client.publish( "axis", "turnOff", None, timeout=0 )) else: - axis = int(body.get("axis", 4)) + axis = int(body.get("axis", 4)) move_type = int(body.get("move_type", 2)) - distance = float(body.get("distance", 0)) + distance = float(body.get("distance", 0)) await loop.run_in_executor(None, lambda: self.client.publish( "axis", "move", {"axis": axis, "move_type": move_type, "distance": distance}, timeout=0 )) + return web.json_response({"result": "ok"}) async def handle_api_temperature(self, request): @@ -4337,6 +4558,8 @@ function loadPrinterTab(){ "ams_slots": self._ams_slots, "ams_loaded_slot": self._ams_loaded_slot, "filament_mode": s.get("filament_mode", self._filament_mode), + "ace_drying": s.get("ace_drying", {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None}), + "ace_units": list(self._ace_box_ids), "thumbnail": self._thumbnail_b64, "connection_error": s["connection_error"], "file_ready": s["file_ready"], @@ -4382,6 +4605,7 @@ function loadPrinterTab(){ self._head_tools_model = int(data.get("head_tools_model", self._head_tools_model)) boxes = data.get("multi_color_box") or [] if boxes: + self._update_ace_drying_state(data, boxes) self._filament_mode = self._detect_filament_mode(boxes, self._head_tools_model) self._state["filament_mode"] = self._filament_mode global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) @@ -4971,6 +5195,7 @@ function loadPrinterTab(){ self._head_tools_model = int(data.get("head_tools_model", self._head_tools_model)) boxes = data.get("multi_color_box") or [] if boxes: + self._update_ace_drying_state(data, boxes) self._filament_mode = self._detect_filament_mode(boxes, self._head_tools_model) self._state["filament_mode"] = self._filament_mode global_slots, global_loaded = self._aggregate_slots(boxes, self._filament_mode) @@ -5059,6 +5284,7 @@ def build_app(bridge: KobraXBridge) -> web.Application: 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/set_slot", bridge.handle_api_ams_set_slot) + r.add_post("/api/ace/dry", bridge.handle_api_ace_dry) r.add_post("/api/axis", bridge.handle_api_axis) r.add_post("/api/temperature", bridge.handle_api_temperature) r.add_get("/api/camera", bridge.handle_api_camera) -- 2.49.1 From 24ccf96f5cd671b6cc2ce42ad56ef93fe34ac35d Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 18:56:35 -1000 Subject: [PATCH 04/13] current temp and humidity working --- kobrax_moonraker_bridge.py | 68 +++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 96bbac6..f21813b 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -447,7 +447,7 @@ class KobraXBridge: "connection_error": "", "file_ready": "", "filament_mode": "toolhead", - "ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None}, + "ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None}, } self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id self._ams_loaded_slot: int = -1 # global slot index of currently loaded slot @@ -788,8 +788,8 @@ class KobraXBridge: ace_ids = sorted({int(b.get("id", -1)) for b in boxes if int(b.get("id", -1)) >= 0}) self._ace_box_ids = [i for i in ace_ids if 0 <= i <= 3] - def _humidity_from(src: dict, default=None): - for k in ("humidity", "current_humidity", "cur_humidity", "relative_humidity", "humidity_value"): + def _num_from(src: dict, keys: tuple[str, ...], default=None): + for k in keys: v = src.get(k) if v is not None: try: @@ -798,20 +798,32 @@ class KobraXBridge: return default return default + def _humidity_from(src: dict, default=None): + return _num_from(src, ("humidity", "current_humidity", "cur_humidity", "relative_humidity", "humidity_value"), default) + + def _current_temp_from(src: dict, default=None): + return _num_from(src, ("current_temp", "cur_temp", "temperature", "temp", "drying_temp", "chamber_temp"), default) + per_unit: list[dict] = [] for box in boxes: bid = int(box.get("id", -1)) if bid < 0: continue + bs = box.get("drying_status") or box.get("drying_settings") - if isinstance(bs, dict): + bs = bs if isinstance(bs, dict) else {} + hu = _humidity_from(bs, _humidity_from(box)) + ct = _current_temp_from(bs, _current_temp_from(box)) + + if bs or hu is not None or ct is not None: per_unit.append({ "id": bid, "status": int(bs.get("status", 0)), "target_temp": int(bs.get("target_temp", 0)), "duration": int(bs.get("duration", 0)), "remain_time": int(bs.get("remain_time", 0)), - "humidity": _humidity_from(bs), + "humidity": hu, + "current_temp": ct, }) src = data.get("drying_status") or data.get("drying_settings") @@ -825,15 +837,18 @@ class KobraXBridge: break if isinstance(src, dict): - cur = self._state.get("ace_drying") or {} - self._state["ace_drying"] = { - "status": int(src.get("status", cur.get("status", 0))), - "target_temp": int(src.get("target_temp", cur.get("target_temp", 0))), - "duration": int(src.get("duration", cur.get("duration", 0))), - "remain_time": int(src.get("remain_time", cur.get("remain_time", 0))), - "humidity": _humidity_from(src, cur.get("humidity")), - "units": per_unit, - } + cur = self._state.get("ace_drying") or {} + active = [u for u in per_unit if u.get("status", 0)] + primary = active[0] if active else (per_unit[0] if per_unit else {}) + self._state["ace_drying"] = { + "status": int(src.get("status", cur.get("status", 0))), + "target_temp": int(src.get("target_temp", cur.get("target_temp", 0))), + "duration": int(src.get("duration", cur.get("duration", 0))), + "remain_time": int(src.get("remain_time", cur.get("remain_time", 0))), + "humidity": _humidity_from(src, primary.get("humidity", cur.get("humidity"))), + "current_temp": _current_temp_from(src, primary.get("current_temp", cur.get("current_temp"))), + "units": per_unit, + } elif per_unit: active = [u for u in per_unit if u.get("status", 0)] primary = active[0] if active else per_unit[0] @@ -843,6 +858,7 @@ class KobraXBridge: "duration": int(primary.get("duration", 0)), "remain_time": int(primary.get("remain_time", 0)), "humidity": primary.get("humidity"), + "current_temp": primary.get("current_temp"), "units": per_unit, } @@ -2326,6 +2342,10 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; Humidity: -
+
+ Current Temp: + - +
@@ -2437,7 +2457,7 @@ var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0, curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–', camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80, - ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,units:[]}}; + ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}}; var tempHistory={n:[],b:[]}; var camOn=false; var currentStep=1; @@ -2459,7 +2479,7 @@ var LANG_DE={ 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', lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', - card_ace_dry:'ACE Trocknung',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', + card_ace_dry:'ACE Trocknung',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Aktuelle Temperatur',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', 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', @@ -2522,7 +2542,7 @@ var LANG_EN={ card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer', speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', - card_ace_dry:'ACE Drying',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', + card_ace_dry:'ACE Drying',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Current Temp',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop', 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', @@ -2738,6 +2758,7 @@ function applyLang(){ setText('ace-dry-start',T.ace_dry_start); setText('ace-dry-stop',T.ace_dry_stop); setText('d-ace-dry-humidity-label',(T.ace_dry_humidity||'Humidity')+':'); + setText('d-ace-dry-current-temp-label',(T.ace_dry_current_temp||'Current Temp')+':'); var adTemp=document.getElementById('ace-dry-temp');if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp); var adDur=document.getElementById('ace-dry-duration');if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration); // conn-btn text (nur wenn nicht im Übergangszustand) @@ -2983,7 +3004,7 @@ function applyState(){ ||((s.ams_slots||[]).some(function(sl){return (sl.box_id||-1)>=0;})); var aceCard=document.getElementById('d-ace-dry-card'); if(aceCard)aceCard.style.display=acePresent?'':'none'; - var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null}; + var dry=s.ace_drying||{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null}; var dryStatus=document.getElementById('d-ace-dry-status'); if(dryStatus){ if(dry.status){ @@ -2998,6 +3019,11 @@ function applyState(){ var hv=(dry.humidity===null||dry.humidity===undefined||dry.humidity==='')?null:Number(dry.humidity); dryHumidity.textContent=(hv===null||Number.isNaN(hv))?'-':(Math.round(hv)+'%'); } + var dryCurrentTemp=document.getElementById('d-ace-dry-current-temp'); + if(dryCurrentTemp){ + var ct=(dry.current_temp===null||dry.current_temp===undefined||dry.current_temp==='')?null:Number(dry.current_temp); + dryCurrentTemp.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C'); + } var dryTemp=document.getElementById('ace-dry-temp'); if(dryTemp&&dry.target_temp>0&&String(dryTemp.value||'')==='55')dryTemp.value=dry.target_temp; var dryDur=document.getElementById('ace-dry-duration'); @@ -4279,6 +4305,7 @@ function loadPrinterTab(){ target_temp = max(30, min(80, target_temp)) duration = max(10, min(24 * 60, duration)) humidity = (self._state.get("ace_drying") or {}).get("humidity") + current_temp = (self._state.get("ace_drying") or {}).get("current_temp") drying_status = { "status": 1, "target_temp": target_temp, @@ -4291,16 +4318,19 @@ function loadPrinterTab(){ "duration": duration, "remain_time": duration, "humidity": humidity, + "current_temp": current_temp, } else: drying_status = {"status": 0} humidity = (self._state.get("ace_drying") or {}).get("humidity") + current_temp = (self._state.get("ace_drying") or {}).get("current_temp") ui_state = { "status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": humidity, + "current_temp": current_temp, } payload = { @@ -4558,7 +4588,7 @@ function loadPrinterTab(){ "ams_slots": self._ams_slots, "ams_loaded_slot": self._ams_loaded_slot, "filament_mode": s.get("filament_mode", self._filament_mode), - "ace_drying": s.get("ace_drying", {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None}), + "ace_drying": s.get("ace_drying", {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None}), "ace_units": list(self._ace_box_ids), "thumbnail": self._thumbnail_b64, "connection_error": s["connection_error"], -- 2.49.1 From a3dbe9a0e8af01cfeb909617115492a90aad3248 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 19:15:47 -1000 Subject: [PATCH 05/13] ACE drying card v1 --- kobrax_moonraker_bridge.py | 249 +++++++++++++++++++++++-------------- 1 file changed, 154 insertions(+), 95 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index f21813b..960a9a7 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -2333,27 +2333,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
-