ace dryer, start, stop, temp set/current, time set/remaning, status, humidity monitor. groundwork
This commit is contained in:
@@ -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;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="d-ace-dry-card" style="display:none">
|
||||
<div class="card-title"><span>♨</span> <span id="d-card-ace-dry">ACE Trocknung</span></div>
|
||||
<div style="font-size:12px;color:var(--txt2);margin-bottom:10px">
|
||||
<span id="d-ace-dry-status">Status: Aus</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--txt2);margin:-6px 0 10px 0">
|
||||
<span id="d-ace-dry-humidity-label">Humidity:</span>
|
||||
<span id="d-ace-dry-humidity">-</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:10px">
|
||||
<input type="number" class="temp-input" id="ace-dry-temp" min="30" max="80" step="1" value="55" style="flex:1" placeholder="Temp (°C)">
|
||||
<input type="number" class="temp-input" id="ace-dry-duration" min="10" max="1440" step="10" value="240" style="flex:1" placeholder="Min">
|
||||
</div>
|
||||
<div class="ctrl-btns">
|
||||
<button class="btn btn-sm btn-accent" id="ace-dry-start" onclick="aceDryStart()">▶ Start</button>
|
||||
<button class="btn btn-sm btn-cancel" id="ace-dry-stop" onclick="aceDryStop()">■ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AMS -->
|
||||
<div class="card" style="grid-column:1/-1" id="d-ams-card">
|
||||
<div class="card-title"><span>◫</span> <span id="d-card-ams">Filament</span></div>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user