ACE2 Drying Card v2, Dryer Presets,

This commit is contained in:
Gangoke
2026-05-17 22:14:51 -10:00
parent a3dbe9a0e8
commit 8c4bff6b86
3 changed files with 585 additions and 84 deletions

View File

@@ -32,3 +32,29 @@ auto_leveling = 1
[bridge]
# Poll-Intervall in Sekunden
poll_interval = 3
[ace_dry_presets]
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
pla_temp = 45
pla_duration_sec = 14400
pla_plus_temp = 45
pla_plus_duration_sec = 14400
petg_temp = 50
petg_duration_sec = 14400
tpu_temp = 55
tpu_duration_sec = 14400
abs_asa_temp = 45
abs_asa_duration_sec = 28800
pa_pc_temp = 55
pa_pc_duration_sec = 43200
# Custom Presets (Name + Temp + Dauer)
custom_1_name = Custom 1
custom_1_temp = 45
custom_1_duration_sec = 14400
custom_2_name = Custom 2
custom_2_temp = 45
custom_2_duration_sec = 14400
custom_3_name = Custom 3
custom_3_temp = 45
custom_3_duration_sec = 14400

View File

@@ -34,3 +34,29 @@ auto_leveling = 1
[bridge]
# Poll-Intervall in Sekunden
poll_interval = 3
[ace_dry_presets]
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
pla_temp = 45
pla_duration_sec = 14400
pla_plus_temp = 45
pla_plus_duration_sec = 14400
petg_temp = 50
petg_duration_sec = 14400
tpu_temp = 55
tpu_duration_sec = 14400
abs_asa_temp = 45
abs_asa_duration_sec = 28800
pa_pc_temp = 55
pa_pc_duration_sec = 43200
# Custom Presets (Name + Temp + Dauer)
custom_1_name = Custom 1
custom_1_temp = 45
custom_1_duration_sec = 14400
custom_2_name = Custom 2
custom_2_temp = 45
custom_2_duration_sec = 14400
custom_3_name = Custom 3
custom_3_temp = 45
custom_3_duration_sec = 14400

View File

@@ -422,32 +422,32 @@ class KobraXBridge:
self.ws_clients: set[web.WebSocketResponse] = set()
self._last_state: dict = {}
self._state = {
"nozzle_temp": 0.0,
"nozzle_target": 0.0,
"bed_temp": 0.0,
"bed_target": 0.0,
"print_state": "standby",
"kobra_state": "free",
"filename": "",
"slicer_time": 0,
"progress": 0.0,
"print_duration": 0,
"remain_time": 0,
"curr_layer": 0,
"total_layers": 0,
"printer_name": env_loader.get("BRIDGE_PRINTER_NAME", "Anycubic Kobra X"),
"firmware_version": "unknown",
"upload_url": "",
"camera_url": "",
"fan_speed": 0,
"light_on": False,
"light_brightness": 80,
"taskid": "-1",
"print_speed_mode": 2,
"connection_error": "",
"file_ready": "",
"filament_mode": "toolhead",
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
"nozzle_temp": 0.0,
"nozzle_target": 0.0,
"bed_temp": 0.0,
"bed_target": 0.0,
"print_state": "standby",
"kobra_state": "free",
"filename": "",
"slicer_time": 0,
"progress": 0.0,
"print_duration": 0,
"remain_time": 0,
"curr_layer": 0,
"total_layers": 0,
"printer_name": env_loader.get("BRIDGE_PRINTER_NAME", "Anycubic Kobra X"),
"firmware_version": "unknown",
"upload_url": "",
"camera_url": "",
"fan_speed": 0,
"light_on": False,
"light_brightness": 80,
"taskid": "-1",
"print_speed_mode": 2,
"connection_error": "",
"file_ready": "",
"filament_mode": "toolhead",
"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
@@ -461,6 +461,7 @@ class KobraXBridge:
self._current_job_id: str = ""
self._thumbnail_b64: str = ""
self._ace_dry_presets: dict[str, dict] = self._load_ace_dry_presets_config()
# Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10)
self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0}
@@ -474,6 +475,73 @@ class KobraXBridge:
client.callbacks["light/report"] = self._on_light
client.callbacks["skip/report"] = self._on_skip
def _default_ace_dry_presets(self) -> dict[str, dict]:
return {
"pla": {"temp": 45, "duration_sec": 4 * 3600},
"pla_plus": {"temp": 45, "duration_sec": 4 * 3600},
"petg": {"temp": 50, "duration_sec": 4 * 3600},
"tpu": {"temp": 55, "duration_sec": 4 * 3600},
"abs_asa": {"temp": 45, "duration_sec": 8 * 3600},
"pa_pc": {"temp": 55, "duration_sec": 12 * 3600},
"custom_1": {"name": "Custom 1", "temp": 45, "duration_sec": 4 * 3600},
"custom_2": {"name": "Custom 2", "temp": 45, "duration_sec": 4 * 3600},
"custom_3": {"name": "Custom 3", "temp": 45, "duration_sec": 4 * 3600},
}
def _sanitize_ace_dry_presets(self, presets: dict) -> dict[str, dict]:
out = self._default_ace_dry_presets()
for key in list(out.keys()):
src = presets.get(key) if isinstance(presets, dict) else None
if not isinstance(src, dict):
continue
try:
t = int(src.get("temp", out[key]["temp"]))
except Exception:
t = out[key]["temp"]
try:
d = int(src.get("duration_sec", out[key]["duration_sec"]))
except Exception:
d = out[key]["duration_sec"]
out[key]["temp"] = max(30, min(80, t))
out[key]["duration_sec"] = max(10 * 60, min(24 * 3600, d))
if key.startswith("custom_"):
name = str(src.get("name", out[key].get("name", key.replace("_", " ").title()))).strip()
out[key]["name"] = name or out[key].get("name", "Custom")
return out
def _load_ace_dry_presets_config(self) -> dict[str, dict]:
import configparser
defaults = self._default_ace_dry_presets()
cfg_path = self._find_config_path()
if not cfg_path.is_file():
return defaults
cfg = configparser.ConfigParser()
cfg.read(cfg_path, encoding="utf-8")
sec = "ace_dry_presets"
if not cfg.has_section(sec):
return defaults
out = {}
for key, d in defaults.items():
temp_k = f"{key}_temp"
dur_k = f"{key}_duration_sec"
try:
temp = int(cfg.get(sec, temp_k, fallback=str(d["temp"])))
except Exception:
temp = d["temp"]
try:
dur = int(cfg.get(sec, dur_k, fallback=str(d["duration_sec"])))
except Exception:
dur = d["duration_sec"]
out[key] = {
"temp": max(30, min(80, temp)),
"duration_sec": max(10 * 60, min(24 * 3600, dur)),
}
if key.startswith("custom_"):
name_k = f"{key}_name"
name = cfg.get(sec, name_k, fallback=str(d.get("name", key.replace("_", " ").title()))).strip()
out[key]["name"] = name or str(d.get("name", "Custom"))
return out
# -------------------------------------------------------------------------
# MQTT callbacks (called from reader thread)
# -------------------------------------------------------------------------
@@ -804,6 +872,17 @@ class KobraXBridge:
def _current_temp_from(src: dict, default=None):
return _num_from(src, ("current_temp", "cur_temp", "temperature", "temp", "drying_temp", "chamber_temp"), default)
def _minutes_from(src: dict, key: str, default=0):
raw = src.get(key, default)
try:
value = int(float(raw))
except Exception:
return int(default)
# Some firmware payloads report dryer times in seconds while the UI uses minutes.
if value > (24 * 60):
return max(0, int(round(value / 60.0)))
return max(0, value)
per_unit: list[dict] = []
for box in boxes:
bid = int(box.get("id", -1))
@@ -820,8 +899,8 @@ class KobraXBridge:
"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)),
"duration": _minutes_from(bs, "duration", 0),
"remain_time": _minutes_from(bs, "remain_time", 0),
"humidity": hu,
"current_temp": ct,
})
@@ -843,8 +922,8 @@ class KobraXBridge:
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))),
"duration": _minutes_from(src, "duration", cur.get("duration", 0)),
"remain_time": _minutes_from(src, "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,
@@ -2438,12 +2517,82 @@ 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,current_temp:null,units:[]}};
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]}};
var tempHistory={n:[],b:[]};
var aceDryHistory={0:{t:[],h:[]},1:{t:[],h:[]},2:{t:[],h:[]},3:{t:[],h:[]}};
var camOn=false;
var currentStep=1;
var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){
try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};}
})();
var aceDryProfiles=(function(){
try{return JSON.parse(localStorage.getItem('aceDryProfiles')||'{}')||{};}catch(_){return {};}
})();
var _aceDryDialogAceId=-1;
var _aceDryDialogPresetKey='';
var _aceDryDialogPresetOriginals={};
var ACE_DRY_PRESET_DEFAULTS={
pla:{temp:45,duration_sec:4*3600},
pla_plus:{temp:45,duration_sec:4*3600},
petg:{temp:50,duration_sec:4*3600},
tpu:{temp:55,duration_sec:4*3600},
abs_asa:{temp:45,duration_sec:8*3600},
pa_pc:{temp:55,duration_sec:12*3600}
};
var ACE_DRY_PRESETS={
pla:{temp:45,duration_sec:4*3600},
pla_plus:{temp:45,duration_sec:4*3600},
petg:{temp:50,duration_sec:4*3600},
tpu:{temp:55,duration_sec:4*3600},
abs_asa:{temp:45,duration_sec:8*3600},
pa_pc:{temp:55,duration_sec:12*3600},
custom_1:{name:'Custom 1',temp:45,duration_sec:4*3600},
custom_2:{name:'Custom 2',temp:45,duration_sec:4*3600},
custom_3:{name:'Custom 3',temp:45,duration_sec:4*3600}
};
function _aceAutoRefillGet(aceId){return !!aceAutoRefillPrefs[String(aceId)];}
function _aceAutoRefillSet(aceId,on){
aceAutoRefillPrefs[String(aceId)]=!!on;
localStorage.setItem('aceAutoRefillPrefs',JSON.stringify(aceAutoRefillPrefs));
}
function _aceDryProfileGet(aceId){
var p=aceDryProfiles[String(aceId)]||{};
var temp=parseInt(p.temp,10);
var dur=parseInt(p.duration_sec,10);
if(!Number.isFinite(temp))temp=45;
if(!Number.isFinite(dur))dur=4*3600;
temp=Math.max(30,Math.min(80,temp));
dur=Math.max(10*60,Math.min(24*3600,dur));
return {temp:temp,duration_sec:dur,preset:p.preset||''};
}
function _aceDryProfileSet(aceId,temp,durationSec,preset){
aceDryProfiles[String(aceId)]={
temp:Math.max(30,Math.min(80,parseInt(temp,10)||45)),
duration_sec:Math.max(10*60,Math.min(24*3600,parseInt(durationSec,10)||4*3600)),
preset:preset||''
};
localStorage.setItem('aceDryProfiles',JSON.stringify(aceDryProfiles));
}
function _aceDryDurationMinFromSec(sec){
var minutes=Math.round((parseInt(sec,10)||0)/60);
return Math.max(10,Math.min(1440,minutes));
}
function _syncAceDryPresetsFromServer(raw){
if(!raw||typeof raw!=='object')return;
Object.keys(ACE_DRY_PRESETS).forEach(function(k){
var p=raw[k];
if(!p||typeof p!=='object')return;
var t=parseInt(p.temp,10);
var d=parseInt(p.duration_sec,10);
if(Number.isFinite(t))ACE_DRY_PRESETS[k].temp=Math.max(30,Math.min(80,t));
if(Number.isFinite(d))ACE_DRY_PRESETS[k].duration_sec=Math.max(10*60,Math.min(24*3600,d));
if(/^custom_[123]$/.test(k)&&typeof p.name==='string'){
var n=p.name.trim();
ACE_DRY_PRESETS[k].name=n||('Custom '+k.slice(-1));
}
});
}
// ── Theme ──
function toggleTheme(){
@@ -2461,7 +2610,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_dryer:'Trockner',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_chart:'Verlauf (Temp/Feuchte)',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_dryer:'Trockner',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:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Custom Name',
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',
@@ -2524,7 +2673,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_dryer:'Dryer',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_chart:'History (Temp/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_dryer:'Dryer',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:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name',
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',
@@ -2739,14 +2888,25 @@ function applyLang(){
document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload);
for(var i=0;i<4;i++){
setText('d-card-ace-dry-'+i,'ACE '+(i+1)+' - '+(T.ace_dry_dryer||'Dryer'));
setText('ace-dry-start-'+i,T.ace_dry_start);
setText('ace-dry-stop-'+i,T.ace_dry_stop);
setText('d-ace-auto-refill-label-'+i,T.ace_dry_auto_refill||'Auto Refill');
setText('d-ace-drying-enable-label-'+i,T.ace_dry_enable||'Enable Drying');
setText('d-ace-dry-humidity-label-'+i,(T.ace_dry_humidity||'Humidity')+':');
setText('d-ace-dry-current-temp-label-'+i,(T.ace_dry_current_temp||'Current Temp')+':');
setText('d-ace-dry-target-label-'+i,(T.ace_dry_temp_line||'Drying Temperature')+':');
setText('d-ace-dry-time-label-'+i,(T.ace_dry_time_line||'Drying Time')+':');
setText('d-ace-dry-chart-label-'+i,T.ace_dry_chart||'History (Temp/Humidity)');
var adTemp=document.getElementById('ace-dry-temp-'+i);if(adTemp)adTemp.setAttribute('placeholder',T.ace_dry_temp);
var adDur=document.getElementById('ace-dry-duration-'+i);if(adDur)adDur.setAttribute('placeholder',T.ace_dry_duration);
}
setText('ace-dry-dialog-title',T.ace_dry_dialog_title||'Dryer Temp/Time Settings');
setText('ace-dry-dialog-temp-label',T.ace_dry_dialog_temp||'Temperature (30-80°C)');
setText('ace-dry-dialog-time-label',T.ace_dry_dialog_time||'Rem. Time (h:m:s)');
setText('ace-dry-dialog-custom-name-label',T.ace_dry_dialog_custom_name||'Custom Name');
setText('ace-dry-dialog-cancel',T.ace_dry_dialog_cancel||'Cancel');
setText('ace-dry-dialog-confirm',T.ace_dry_dialog_confirm||'Confirm');
setText('ace-dry-dialog-reset-default',T.ace_dry_dialog_reset_default||'Reset to Default');
setText('ace-dry-dialog-save-preset',T.ace_dry_dialog_save_restart||'Save & Restart');
aceDryDialogSyncCustomButtonNames();
// conn-btn text (nur wenn nicht im Übergangszustand)
updateConnBtn();
// Slot-Edit-Dialog
@@ -2770,21 +2930,35 @@ function ensureAceDryCards(){
for(var i=0;i<4;i++){
html+='<div class="card" id="d-ace-dry-card-'+i+'" style="display:none">'
+'<div class="card-title"><span>♨</span> <span id="d-card-ace-dry-'+i+'">ACE '+(i+1)+' - Dryer</span></div>'
+'<div style="font-size:12px;color:var(--txt2);margin-bottom:10px"><span id="d-ace-dry-status-'+i+'">Status: Off</span></div>'
+'<div style="font-size:12px;color:var(--txt2);margin:-6px 0 10px 0"><span id="d-ace-dry-humidity-label-'+i+'">Humidity:</span> <span id="d-ace-dry-humidity-'+i+'">-</span></div>'
+'<div style="font-size:12px;color:var(--txt2);margin:-6px 0 10px 0"><span id="d-ace-dry-current-temp-label-'+i+'">Current Temp:</span> <span id="d-ace-dry-current-temp-'+i+'">-</span></div>'
+'<div style="display:flex;gap:8px;margin-bottom:10px">'
+'<input type="number" class="temp-input" id="ace-dry-temp-'+i+'" min="30" max="80" step="1" value="55" style="flex:1" placeholder="Temp (°C)">'
+'<input type="number" class="temp-input" id="ace-dry-duration-'+i+'" min="10" max="1440" step="10" value="240" style="flex:1" placeholder="Min">'
+'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">'
+'<span><span id="d-ace-dry-current-temp-label-'+i+'">Temperature:</span> <span id="d-ace-dry-current-temp-'+i+'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>'
+'<span><span id="d-ace-dry-humidity-label-'+i+'">Humidity:</span> <span id="d-ace-dry-humidity-'+i+'" style="font-size:30px;font-weight:700;color:#3aa8ff;line-height:1">-</span></span>'
+'</div>'
+'<div class="ctrl-btns">'
+'<button class="btn btn-sm btn-accent" id="ace-dry-start-'+i+'" onclick="aceDryStart('+i+')">▶ Start</button>'
+'<button class="btn btn-sm btn-cancel" id="ace-dry-stop-'+i+'" onclick="aceDryStop('+i+')">■ Stop</button>'
+'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>'
+'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">'
+'<span><span id="d-ace-dry-target-label-'+i+'">Drying Temperature:</span> <span id="d-ace-dry-target-'+i+'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>'
+'<span><span id="d-ace-dry-time-label-'+i+'">Drying Time:</span> <span id="d-ace-dry-time-'+i+'" style="font-size:30px;font-weight:700;color:#fff;line-height:1">-</span></span>'
+'</div>'
+'<div style="margin-bottom:10px">'
+'<button onclick="openAceDryDialog('+i+')" title="Edit Dryer Temp/Time Settings" style="width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#000;cursor:pointer;font-size:13px;font-weight:600;line-height:1.2">Set Temp/Time</button>'
+'</div>'
+'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>'
+'<div class="toggle-row" style="margin-bottom:10px">'
+'<span class="toggle-label" id="d-ace-auto-refill-label-'+i+'">Auto Refill</span>'
+'<label class="toggle">'
+'<input type="checkbox" id="ace-auto-refill-toggle-'+i+'" onchange="aceAutoRefillToggle('+i+')">'
+'<span class="toggle-track"></span>'
+'<span class="toggle-thumb"></span>'
+'</label>'
+'</div>'
+'<div class="toggle-row" style="margin-bottom:8px">'
+'<span class="toggle-label" id="d-ace-drying-enable-label-'+i+'">Enable Drying</span>'
+'<label class="toggle">'
+'<input type="checkbox" id="ace-dry-enable-toggle-'+i+'" onchange="aceDryToggle('+i+',this.checked)">'
+'<span class="toggle-track"></span>'
+'<span class="toggle-thumb"></span>'
+'</label>'
+'</div>'
+'<div style="margin-top:10px">'
+'<div style="font-size:10px;color:var(--txt2);margin-bottom:4px" id="d-ace-dry-chart-label-'+i+'">History (Temp/Humidity)</div>'
+'<canvas id="d-ace-dry-chart-'+i+'" width="600" height="90" style="width:100%;height:90px;background:var(--raised);border-radius:8px"></canvas>'
+'</div>'
+'</div>';
}
grid.innerHTML=html;
@@ -2914,12 +3088,20 @@ function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(
// ── Helpers ──
function fmtTime(s){if(!s||s<0)return'';var m=Math.floor(s/60),h=Math.floor(m/60);m%=60;return h>0?h+'h '+m+'m':m+'m'}
function fmtHmsFromSec(total){
total=Math.max(0,parseInt(total||0,10));
var h=Math.floor(total/3600);
var mm=Math.floor((total%3600)/60);
var ss=total%60;
return h+':'+String(mm).padStart(2,'0')+':'+String(ss).padStart(2,'0');
}
function post(url,body){return fetch(_apiUrl(url),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})}
function clamp(v,lo,hi){return Math.min(hi,Math.max(lo,v))}
// ── Apply state to DOM ──
function applyState(){
var s=S;
_syncAceDryPresetsFromServer(s.ace_dry_presets);
// connection error banner nur wenn überhaupt ein Drucker konfiguriert ist
var banner=document.getElementById('conn-error-banner');
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent=''+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
@@ -3035,15 +3217,10 @@ function applyState(){
card.style.display=show?'':'none';
if(!show)continue;
var ud=unitMap[i]||dry;
var st=document.getElementById('d-ace-dry-status-'+i);
if(st){
if(Number(ud.status||0)){
var rem=(Number(ud.remain_time)||0);
st.textContent=(T.ace_dry_status_on||'Status: Active')+(rem>0?(' - '+(T.ace_dry_status_remaining||'Remaining')+': '+rem+' min'):'' );
}else{
st.textContent=T.ace_dry_status_off||'Status: Off';
}
}
var refillToggle=document.getElementById('ace-auto-refill-toggle-'+i);
if(refillToggle)refillToggle.checked=_aceAutoRefillGet(i);
var dryToggle=document.getElementById('ace-dry-enable-toggle-'+i);
if(dryToggle)dryToggle.checked=Number(ud.status||0)>0;
var hh=document.getElementById('d-ace-dry-humidity-'+i);
if(hh){
var hv=(ud.humidity===null||ud.humidity===undefined||ud.humidity==='')?null:Number(ud.humidity);
@@ -3054,10 +3231,15 @@ function applyState(){
var ct=(ud.current_temp===null||ud.current_temp===undefined||ud.current_temp==='')?null:Number(ud.current_temp);
ht.textContent=(ct===null||Number.isNaN(ct))?'-':(ct.toFixed(1)+'°C');
}
var dryTemp=document.getElementById('ace-dry-temp-'+i);
if(dryTemp&&Number(ud.target_temp)>0&&String(dryTemp.value||'')==='55')dryTemp.value=Number(ud.target_temp);
var dryDur=document.getElementById('ace-dry-duration-'+i);
if(dryDur&&Number(ud.duration)>0&&String(dryDur.value||'')==='240')dryDur.value=Number(ud.duration);
var prof=_aceDryProfileGet(i);
var useSec=(Number(ud.status||0)>0&&Number(ud.remain_time)>0)
?Number(ud.remain_time||0)*60
:prof.duration_sec;
var showTemp=(Number(ud.status||0)>0&&Number(ud.target_temp)>0)?Number(ud.target_temp):prof.temp;
var dryTempEl=document.getElementById('d-ace-dry-target-'+i);
if(dryTempEl)dryTempEl.textContent=showTemp+'°C';
var dryTimeEl=document.getElementById('d-ace-dry-time-'+i);
if(dryTimeEl)dryTimeEl.textContent=fmtHmsFromSec(useSec);
}
// AMS
@@ -3158,25 +3340,6 @@ function updateHistory(){
if(tempHistory.n.length>60)tempHistory.n.shift();
if(tempHistory.b.length>60)tempHistory.b.shift();
drawChart('d-chart',tempHistory,[{data:tempHistory.n,color:'#00c8ff',max:300},{data:tempHistory.b,color:'#ff6b35',max:120}]);
var dry=S.ace_drying||{};
var units=(dry.units||[]);
var unitMap={};
units.forEach(function(u){var id=Number(u.id);if(id>=0&&id<=3)unitMap[id]=u;});
for(var i=0;i<4;i++){
var u=unitMap[i];
var t=u&&u.current_temp!=null?Number(u.current_temp):null;
var h=u&&u.humidity!=null?Number(u.humidity):null;
if((t===null||Number.isNaN(t))&&(h===null||Number.isNaN(h)))continue;
if(t!==null&&!Number.isNaN(t))aceDryHistory[i].t.push(t);
if(h!==null&&!Number.isNaN(h))aceDryHistory[i].h.push(h);
if(aceDryHistory[i].t.length>60)aceDryHistory[i].t.shift();
if(aceDryHistory[i].h.length>60)aceDryHistory[i].h.shift();
drawChart('d-ace-dry-chart-'+i,aceDryHistory[i],[
{data:aceDryHistory[i].t,color:'#ff8c2f',max:100},
{data:aceDryHistory[i].h,color:'#3aa8ff',max:100}
]);
}
}
function drawChart(id,_,series){
var canvas=document.getElementById(id);if(!canvas)return;
@@ -3577,8 +3740,9 @@ function camStop(){
function aceDryStart(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
var t=parseInt((document.getElementById('ace-dry-temp-'+aceId)||{}).value||55);
var d=parseInt((document.getElementById('ace-dry-duration-'+aceId)||{}).value||240);
var prof=_aceDryProfileGet(aceId);
var t=parseInt(prof.temp,10);
var d=_aceDryDurationMinFromSec(prof.duration_sec);
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,ace_id:aceId})
@@ -3591,6 +3755,226 @@ function aceDryStart(aceId){
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
}
function aceAutoRefillToggle(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
var on=!!((document.getElementById('ace-auto-refill-toggle-'+aceId)||{}).checked);
_aceAutoRefillSet(aceId,on);
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_auto_refill||'Auto Refill')+': '+(on?'ON':'OFF')+' '+(T.ace_dry_ui_pending||'(UI only, backend next)'),'msg-info');
}
function openAceDryDialog(aceId){
aceId=(typeof aceId==='number'&&aceId>=0)?aceId:0;
_aceDryDialogAceId=aceId;
_syncAceDryPresetsFromServer(S.ace_dry_presets);
_aceDryDialogPresetOriginals=JSON.parse(JSON.stringify(ACE_DRY_PRESETS));
aceDryDialogSyncCustomButtonNames();
var hasStored=Object.prototype.hasOwnProperty.call(aceDryProfiles,String(aceId));
var prof=_aceDryProfileGet(aceId);
if(hasStored&&prof.preset&&ACE_DRY_PRESETS[prof.preset]){
aceDryDialogPreset(prof.preset);
}else if(hasStored){
var sec=prof.duration_sec;
document.getElementById('ace-dry-dialog-temp').value=prof.temp;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogHighlightPreset('');
}else{
aceDryDialogPreset('pla');
}
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
var sb=document.getElementById('ace-dry-dialog-save-preset');
if(sb){sb.disabled=false;sb.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';}
document.getElementById('ace-dry-dialog').classList.add('open');
}
function closeAceDryDialog(){
_aceDryDialogAceId=-1;
_aceDryDialogPresetOriginals={};
var sb=document.getElementById('ace-dry-dialog-save-preset');
if(sb)sb.style.display='none';
var rb=document.getElementById('ace-dry-dialog-reset-default');
if(rb)rb.style.display='none';
document.getElementById('ace-dry-dialog').classList.remove('open');
}
function aceDryDialogIsCustomPreset(key){
return /^custom_[123]$/.test(String(key||''));
}
function aceDryDialogSyncCustomButtonNames(){
['custom_1','custom_2','custom_3'].forEach(function(k){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1));
});
}
function aceDryDialogUpdateCustomNameUi(){
var row=document.getElementById('ace-dry-dialog-custom-name-row');
var input=document.getElementById('ace-dry-dialog-custom-name');
if(!row||!input)return;
if(!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){
row.style.display='none';
return;
}
row.style.display='flex';
input.value=(ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||'';
}
function aceDryDialogCurrentValues(){
var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10);
var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10);
var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10);
var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10);
t=Math.max(30,Math.min(80,t));
h=Math.max(0,Math.min(24,h));
m=Math.max(0,Math.min(59,m));
s=Math.max(0,Math.min(59,s));
var totalSec=(h*3600)+(m*60)+s;
totalSec=Math.max(10*60,Math.min(24*3600,totalSec));
return {temp:t,duration_sec:totalSec};
}
function aceDryDialogUpdateSaveButton(){
var btn=document.getElementById('ace-dry-dialog-save-preset');
if(!btn)return;
var key=_aceDryDialogPresetKey||'';
if(!key||!ACE_DRY_PRESETS[key]){btn.style.display='none';return;}
var p=_aceDryDialogPresetOriginals[key]||ACE_DRY_PRESETS[key];
var cur=aceDryDialogCurrentValues();
var changed=(cur.temp!==Number(p.temp)||cur.duration_sec!==Number(p.duration_sec));
if(aceDryDialogIsCustomPreset(key)){
var nameInp=document.getElementById('ace-dry-dialog-custom-name');
var n=((nameInp&&nameInp.value)||'').trim();
var old=(p&&p.name?String(p.name):('Custom '+key.slice(-1))).trim();
if((n||old)!==old)changed=true;
}
btn.style.display=changed?'':'none';
}
function aceDryDialogUpdateResetButton(){
var btn=document.getElementById('ace-dry-dialog-reset-default');
if(!btn)return;
var key=_aceDryDialogPresetKey||'';
var d=ACE_DRY_PRESET_DEFAULTS[key];
if(!key||!d){btn.style.display='none';return;}
var cur=aceDryDialogCurrentValues();
var changed=(cur.temp!==Number(d.temp)||cur.duration_sec!==Number(d.duration_sec));
btn.style.display=changed?'':'none';
}
function aceDryDialogInputsChanged(){
if(aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)){
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+_aceDryDialogPresetKey+'"]');
var i=document.getElementById('ace-dry-dialog-custom-name');
if(b&&i){
var t=(i.value||'').trim();
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1)));
}
}
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
}
function aceDryDialogHighlightPreset(presetKey){
_aceDryDialogPresetKey=presetKey||'';
document.querySelectorAll('.ace-dry-preset-btn').forEach(function(btn){
var on=(btn.getAttribute('data-preset')===presetKey);
btn.style.background=on?'var(--accent)':'var(--raised)';
btn.style.color=on?'#fff':'var(--txt2)';
btn.style.borderColor=on?'var(--accent)':'var(--border)';
});
aceDryDialogUpdateCustomNameUi();
}
function aceDryDialogPreset(presetKey){
var p=ACE_DRY_PRESETS[presetKey];
if(!p)return;
var sec=p.duration_sec;
document.getElementById('ace-dry-dialog-temp').value=p.temp;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogHighlightPreset(presetKey);
aceDryDialogSyncCustomButtonNames();
aceDryDialogUpdateSaveButton();
aceDryDialogUpdateResetButton();
}
function resetAceDryPresetToDefault(){
var key=_aceDryDialogPresetKey||'';
var d=ACE_DRY_PRESET_DEFAULTS[key];
if(!key||!d)return;
var sec=Number(d.duration_sec)||0;
document.getElementById('ace-dry-dialog-temp').value=Number(d.temp)||45;
document.getElementById('ace-dry-dialog-h').value=Math.floor(sec/3600);
document.getElementById('ace-dry-dialog-m').value=Math.floor((sec%3600)/60);
document.getElementById('ace-dry-dialog-s').value=sec%60;
aceDryDialogInputsChanged();
}
function saveAceDryPresetAndRestart(){
var key=_aceDryDialogPresetKey||'';
var btn=document.getElementById('ace-dry-dialog-save-preset');
if(!key||!ACE_DRY_PRESETS[key]||!btn)return;
var cur=aceDryDialogCurrentValues();
if(!ACE_DRY_PRESETS[key])ACE_DRY_PRESETS[key]={};
ACE_DRY_PRESETS[key].temp=cur.temp;
ACE_DRY_PRESETS[key].duration_sec=cur.duration_sec;
if(aceDryDialogIsCustomPreset(key)){
var nameInp=document.getElementById('ace-dry-dialog-custom-name');
var nm=((nameInp&&nameInp.value)||'').trim();
ACE_DRY_PRESETS[key].name=nm||('Custom '+key.slice(-1));
}
btn.disabled=true;
btn.textContent='';
fetch(_apiUrl('/api/settings')).then(function(r){return r.json();}).then(function(d){
d.ace_dry_presets={
pla:{temp:ACE_DRY_PRESETS.pla.temp,duration_sec:ACE_DRY_PRESETS.pla.duration_sec},
pla_plus:{temp:ACE_DRY_PRESETS.pla_plus.temp,duration_sec:ACE_DRY_PRESETS.pla_plus.duration_sec},
petg:{temp:ACE_DRY_PRESETS.petg.temp,duration_sec:ACE_DRY_PRESETS.petg.duration_sec},
tpu:{temp:ACE_DRY_PRESETS.tpu.temp,duration_sec:ACE_DRY_PRESETS.tpu.duration_sec},
abs_asa:{temp:ACE_DRY_PRESETS.abs_asa.temp,duration_sec:ACE_DRY_PRESETS.abs_asa.duration_sec},
pa_pc:{temp:ACE_DRY_PRESETS.pa_pc.temp,duration_sec:ACE_DRY_PRESETS.pa_pc.duration_sec},
custom_1:{name:ACE_DRY_PRESETS.custom_1.name,temp:ACE_DRY_PRESETS.custom_1.temp,duration_sec:ACE_DRY_PRESETS.custom_1.duration_sec},
custom_2:{name:ACE_DRY_PRESETS.custom_2.name,temp:ACE_DRY_PRESETS.custom_2.temp,duration_sec:ACE_DRY_PRESETS.custom_2.duration_sec},
custom_3:{name:ACE_DRY_PRESETS.custom_3.name,temp:ACE_DRY_PRESETS.custom_3.temp,duration_sec:ACE_DRY_PRESETS.custom_3.duration_sec}
};
return post('/api/settings',d);
}).then(function(){
clog('ACE preset '+key+' '+(T.settings_save||'Save & Restart'),'msg-ok');
closeAceDryDialog();
}).catch(function(e){
btn.disabled=false;
btn.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';
clog('ACE-Preset Fehler: '+e,'msg-err');
});
}
function confirmAceDryDialog(){
if(_aceDryDialogAceId<0)return;
var t=parseInt(document.getElementById('ace-dry-dialog-temp').value||45,10);
var h=parseInt(document.getElementById('ace-dry-dialog-h').value||0,10);
var m=parseInt(document.getElementById('ace-dry-dialog-m').value||0,10);
var s=parseInt(document.getElementById('ace-dry-dialog-s').value||0,10);
t=Math.max(30,Math.min(80,t));
h=Math.max(0,Math.min(24,h));
m=Math.max(0,Math.min(59,m));
s=Math.max(0,Math.min(59,s));
var totalSec=(h*3600)+(m*60)+s;
totalSec=Math.max(10*60,Math.min(24*3600,totalSec));
var preset=_aceDryDialogPresetKey||'';
_aceDryProfileSet(_aceDryDialogAceId,t,totalSec,preset);
closeAceDryDialog();
applyState();
}
function aceDryToggle(aceId,on){
if(on)return aceDryStart(aceId);
return aceDryStop(aceId);
}
function toggleCam(){if(camOn)camStop();else camStart()}
function aceDryStop(aceId){
@@ -4195,6 +4579,60 @@ function loadPrinterTab(){
</div>
</div>
<!-- ACE Dryer Temp/Time Settings Dialog -->
<div class="modal-overlay" id="ace-dry-dialog" onclick="if(event.target===this)closeAceDryDialog()">
<div class="modal-box" style="max-width:560px;width:100%">
<div class="modal-header" style="margin-bottom:10px">
<span class="modal-title" id="ace-dry-dialog-title">Dryer Temp/Time Settings</span>
<button onclick="closeAceDryDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label id="ace-dry-dialog-temp-label" style="min-width:190px;font-size:12px;color:var(--txt)">Temperature (30-80°C)</label>
<input id="ace-dry-dialog-temp" type="number" min="30" max="80" step="1"
oninput="aceDryDialogInputsChanged()"
style="width:130px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center" value="45">
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<label id="ace-dry-dialog-time-label" style="min-width:190px;font-size:12px;color:var(--txt)">Rem. Time (h:m:s)</label>
<div style="display:flex;align-items:center;gap:8px">
<input id="ace-dry-dialog-h" type="number" min="0" max="24" step="1" value="4"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
<span style="color:var(--txt2)">:</span>
<input id="ace-dry-dialog-m" type="number" min="0" max="59" step="1" value="0"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
<span style="color:var(--txt2)">:</span>
<input id="ace-dry-dialog-s" type="number" min="0" max="59" step="1" value="0"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
</div>
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
<input id="ace-dry-dialog-custom-name" type="text" maxlength="32" oninput="aceDryDialogInputsChanged()"
style="width:220px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt)">
</div>
<div style="display:flex;justify-content:flex-end;gap:8px">
<button id="ace-dry-dialog-reset-default" onclick="resetAceDryPresetToDefault()" style="display:none;padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Reset to Default</button>
<button id="ace-dry-dialog-save-preset" onclick="saveAceDryPresetAndRestart()" style="display:none;padding:8px 14px;background:var(--warn);border:1px solid transparent;border-radius:8px;color:#fff;cursor:pointer">Save & Restart</button>
<button id="ace-dry-dialog-cancel" onclick="closeAceDryDialog()" style="padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Cancel</button>
<button id="ace-dry-dialog-confirm" onclick="confirmAceDryDialog()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Confirm</button>
</div>
</div>
</div>
<footer style="text-align:center;padding:12px;font-size:11px;color:var(--txt2);border-top:1px solid var(--border);margin-top:auto">
&copy; ViewIT 2026
</footer>
@@ -4649,6 +5087,7 @@ function loadPrinterTab(){
"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, "current_temp": None}),
"ace_units": list(self._ace_box_ids),
"ace_dry_presets": self._ace_dry_presets,
"thumbnail": self._thumbnail_b64,
"connection_error": s["connection_error"],
"file_ready": s["file_ready"],
@@ -4731,6 +5170,7 @@ function loadPrinterTab(){
"device_id": self._args.device_id,
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"ace_dry_presets": self._ace_dry_presets,
})
async def handle_api_settings_post(self, request):
@@ -4745,7 +5185,7 @@ function loadPrinterTab(){
cfg.read(config_path, encoding="utf-8")
# Sections sicherstellen
for section in ("connection", "print", "bridge"):
for section in ("connection", "print", "bridge", "ace_dry_presets"):
if not cfg.has_section(section):
cfg.add_section(section)
@@ -4766,6 +5206,15 @@ function loadPrinterTab(){
elif cfg.has_option("bridge", "printer_name"):
cfg.remove_option("bridge", "printer_name")
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
for key, val in presets.items():
cfg.set("ace_dry_presets", f"{key}_temp", str(val["temp"]))
cfg.set("ace_dry_presets", f"{key}_duration_sec", str(val["duration_sec"]))
if key.startswith("custom_"):
cfg.set("ace_dry_presets", f"{key}_name", str(val.get("name", key.replace("_", " ").title())))
self._ace_dry_presets = presets
with open(config_path, "w", encoding="utf-8") as f:
f.write("# KX-Bridge Konfigurationsdatei\n\n")
cfg.write(f)