From e28a91b64d2382eb788a4064844623e44b16f930 Mon Sep 17 00:00:00 2001 From: Gangoke Date: Sun, 17 May 2026 17:39:46 -1000 Subject: [PATCH] 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