feat: ACE support, filament section visual tweaks
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ dist/
|
||||
releases/*/kx-bridge
|
||||
releases/*/extract_credentials
|
||||
releases/*/extract_credentials.exe
|
||||
config/config.ini
|
||||
data
|
||||
@@ -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 1–3)':'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
|
||||
|
||||
Reference in New Issue
Block a user