feat: ACE support, filament section visual tweaks

This commit is contained in:
Gangoke
2026-05-17 17:39:46 -10:00
parent d040475a62
commit e28a91b64d
2 changed files with 309 additions and 67 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ dist/
releases/*/kx-bridge
releases/*/extract_credentials
releases/*/extract_credentials.exe
config/config.ini
data

View File

@@ -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;
<div style="display:flex;align-items:center;gap:10px">
<input type="range" id="ams-slot-sel" min="0" max="3" step="1" value="0"
class="slider" style="flex:1"
oninput="document.getElementById('ams-slot-label').textContent='Slot '+(parseInt(this.value)+1)">
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)">
<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">
@@ -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+='<div class="ams-slot'+(active?' active':'')+(empty?' empty':'')+ '" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+'<div class="slot-label">Slot '+(idx+1)+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
+'</div>';
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 13)':'Toolhead')
:('ACE '+(bid+1));
html+='<div class="ams-box-group">'
+'<div class="ams-box-label">'+label+'</div>'
+'<div class="ams-box-slots">';
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+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
+'</div>';
});
if(bid===-1&&acePresent){
html+='<div class="ams-slot ams-slot-bridge" aria-label="Slot 4 connected to ACE">'
+'<div class="bridge-chip">ACE</div>'
+'</div>';
}
html+='</div></div>';
});
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