Compare commits

...

2 Commits

2 changed files with 219 additions and 74 deletions

View File

@@ -1077,6 +1077,25 @@ class KobraXBridge:
if (not skipped and self._pending_preprint_skip
and now <= self._pending_preprint_skip_deadline):
return
# Während eines aktiven Drucks sind Skip-Zustände effektiv monoton.
# Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück;
# diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen.
existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n]
existing_set = set(existing_skipped)
incoming_skipped = [str(n) for n in (skipped or []) if n]
incoming_set = set(incoming_skipped)
active_print = self._state.get("print_state") in ("printing", "paused")
if active_print and existing_set:
if not incoming_set:
skipped = list(existing_skipped)
elif not incoming_set.issuperset(existing_set):
merged = list(existing_skipped)
for n in incoming_skipped:
if n not in existing_set:
merged.append(n)
skipped = merged
# Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
self._pending_preprint_skip = []
@@ -1123,7 +1142,10 @@ class KobraXBridge:
return False
for i in range(max(1, int(retries))):
try:
if self._state.get("kobra_state") != "printing":
# kobra_state ist waehrend Start oft nicht exakt "printing"
# (z.B. busy/preheating). Auf print_state pruefen, sonst wird
# skip/start nie abgeschickt.
if self._state.get("print_state") not in ("printing", "paused"):
time.sleep(max(0.1, float(delay_s)))
continue
resp = self.client.skip_objects(wanted)
@@ -2482,26 +2504,29 @@ class KobraXBridge:
async def handle_kx_skip_query(self, request):
"""Druck-Objektliste vom Drucker neu abfragen.
POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten
Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state).
POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den
async skip/report und gibt den zusammengeführten Skip-State zurück.
"""
prev_ts = int(self._skip_state.get("ts", 0) or 0)
try:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
except Exception as e:
return self._json_cors({"error": str(e)}, status=502)
return self._json_cors({"result": self._skip_state})
async def handle_kx_skip_state(self, request):
"""Aktueller Skip-State.
# skip/report kommt asynchron; kurz warten, damit der Rückgabewert
# möglichst den frisch bestätigten Druckerzustand enthält.
deadline = time.time() + 1.5
while time.time() < deadline:
cur_ts = int(self._skip_state.get("ts", 0) or 0)
if cur_ts > prev_ts:
break
await asyncio.sleep(0.1)
Kombiniert:
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
nicht die Gesamtliste.
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
"""
return self._json_cors({"result": self._build_skip_state_result()})
def _build_skip_state_result(self) -> dict:
"""Baut den kombinierten Skip-State für UI-Endpunkte."""
filename = self._state.get("filename", "")
all_objects: list[str] = []
svg = ""
@@ -2513,13 +2538,25 @@ class KobraXBridge:
svg = f.get("svg_image") or ""
except Exception as e:
log.warning(f"skip_state lookup failed: {e}")
result = {
return {
"objects": all_objects,
"skipped": list(self._skip_state.get("skipped", [])),
"svg_b64": svg,
"ts": self._skip_state.get("ts", 0),
"filename": filename,
}
async def handle_kx_skip_state(self, request):
"""Aktueller Skip-State.
Kombiniert:
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
nicht die Gesamtliste.
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
"""
result = self._build_skip_state_result()
return self._json_cors({"result": result})
async def handle_kx_printers(self, request):
@@ -2615,8 +2652,8 @@ class KobraXBridge:
},
}
# Pre-Print-Skip sofort im UI-Status spiegeln
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
# UI erst nach echter Drucker-Bestaetigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
@@ -3139,7 +3176,7 @@ class KobraXBridge:
ams_box_mapping = self._build_auto_ams_box_mapping()
use_ams = len(ams_box_mapping) > 0
auto_leveling = getattr(self._args, "auto_leveling", 1)
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
url = self._state.get("last_upload_url", "")
filesize = self._state.get("last_upload_size", 0)
md5 = self._state.get("last_upload_md5", "")
@@ -3169,6 +3206,15 @@ class KobraXBridge:
},
}
# UI erst nach echter Drucker-Bestaetigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
else:
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
log.info(
f"print/start api=1 mode={self._filament_mode} "
f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}"
@@ -3181,6 +3227,9 @@ class KobraXBridge:
if result is None:
return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504)
if excluded_objects:
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
return web.json_response({"result": "ok"})
async def handle_print_pause(self, request):
@@ -3815,6 +3864,7 @@ class KobraXBridge:
"camera_url": s["camera_url"],
"fan_speed": s["fan_speed"],
"print_speed_mode": s["print_speed_mode"],
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"light_on": s["light_on"],
"light_brightness": s["light_brightness"],

View File

@@ -10,8 +10,8 @@ var camUserStopped=false; // user stopped camera manually — suppress auto-rest
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
var _fdDialogOpen=false; // Dialog ist gerade offen
var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde
var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen
var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null;
var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1';
var currentStep=1;
var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){
@@ -664,10 +664,18 @@ function applyState(){
if(s.file_ready&&s.print_state==='standby'){
document.getElementById('file-ready-name').textContent=s.file_ready;
// Neue Datei → Abbruch-Sperre aufheben
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false;
if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true);
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){
_fdUserCancelled=false;
sessionStorage.removeItem('fdUserCancelled');
sessionStorage.removeItem('fdAutoOpenedFile');
}
if(shouldAutoOpen){
// Dialog-Modus: nur Dialog verwenden, den Banner niemals anzeigen.
frb.style.display='none';
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true);
}
} else {
frb.style.display='flex';
bannerVisible=true;
@@ -1329,23 +1337,40 @@ function slotEditFeed(){
}
function startReadyFile(filename){
var fn=filename||S.file_ready;
function _doStartReadyFile(){
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
.then(function(r){return r.json();})
.then(function(r){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
}
function _gateAndStart(fileObj){
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); });
return;
}
_doStartReadyFile();
}
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); });
if(currentFile){
_gateAndStart(currentFile);
return;
}
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:fn})
.then(function(r){return r.json();})
.then(function(r){
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
.catch(function(e){
clog(tr('log_error')+' '+e,'msg-err');
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
});
// Fresh page loads may not have storeFiles populated yet.
fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){
storeFiles=d.result||[];
var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null;
_gateAndStart(refreshed);
}).catch(function(){
_doStartReadyFile();
});
}
function cancelReadyFile(){
post('/api/file_ready/clear',{})
@@ -2132,6 +2157,7 @@ var _filamentDialogMode='store'; // 'store' oder 'banner'
var _pendingWebVerifyFileId=null;
var _pendingWebVerifyFilename='';
var _pendingWebVerifyAction=null;
var _pendingWebVerifyAutoOpen=false;
// GCode-Store-Dateiliste. MUSS deklariert sein sonst ReferenceError, wenn
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
// wurde (Issue #29 / Theme-Auslagerung PR #27).
@@ -2199,7 +2225,8 @@ function clearWebUploadWarningFlag(fileId, onDone){
});
}
function maybeGateWebUpload(fileObj, onContinue){
function maybeGateWebUpload(fileObj, onContinue, opts){
opts=opts||{};
if(!fileObj || !fileObj.web_unverified){
if(onContinue) onContinue();
return;
@@ -2208,15 +2235,20 @@ function maybeGateWebUpload(fileObj, onContinue){
if(onContinue) onContinue();
return;
}
var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||'';
if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){
return;
}
openWebVerifyDialog(fileObj.id, fileObj.filename, function(){
clearWebUploadWarningFlag(fileObj.id, onContinue);
});
}, !!opts.autoOpen);
}
function openWebVerifyDialog(fileId, filename, onConfirm){
function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){
_pendingWebVerifyFileId=fileId;
_pendingWebVerifyFilename=filename;
_pendingWebVerifyAction=onConfirm||null;
_pendingWebVerifyAutoOpen=!!autoOpen;
var status=document.getElementById('store-web-verify-status');
if(status){status.textContent='';}
openStoreWebVerifyDialog();
@@ -2230,9 +2262,13 @@ function openStoreWebVerifyDialog(){
function closeStoreWebVerifyDialog(){
var modal=document.getElementById('store-web-verify-dialog');
if(modal){modal.classList.remove('open');}
if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){
sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId));
}
_pendingWebVerifyFileId=null;
_pendingWebVerifyFilename='';
_pendingWebVerifyAction=null;
_pendingWebVerifyAutoOpen=false;
}
function confirmStoreWebVerify(){
@@ -2252,9 +2288,11 @@ function confirmStoreWebVerify(){
.then(function(){
var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;});
if(fileObj){fileObj.web_unverified=false;}
sessionStorage.removeItem('webVerifyCancelledFileId');
_pendingWebVerifyFileId=null;
_pendingWebVerifyFilename='';
_pendingWebVerifyAction=null;
_pendingWebVerifyAutoOpen=false;
closeStoreWebVerifyDialog();
loadStore();
if(typeof action==='function') action();
@@ -2270,7 +2308,7 @@ function startReadyFileWithSlots(filename,_autoOpen){
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); });
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
return;
}
_filamentDialogMode='banner';
@@ -2291,23 +2329,31 @@ function startReadyFileWithSlots(filename,_autoOpen){
});
}
function _proceedWithFileObj(fileObj){
if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){
// Verify gate was bypassed (storeFiles was empty on page load) — re-gate now.
if(_autoOpen){_fdDialogOpen=false;}
maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen});
return;
}
if(fileObj){
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
}
openWithSlots();
}
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(fileObj){
_storeFileId=fileObj.id;
_setGcodeFilamentsFromFileObj(fileObj);
openWithSlots();
_proceedWithFileObj(fileObj);
return;
}
// Fallback: refresh file list, then resolve current file by filename.
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
storeFiles=d.result||[];
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
if(refreshed){
_storeFileId=refreshed.id;
_setGcodeFilamentsFromFileObj(refreshed);
}
openWithSlots();
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null;
_proceedWithFileObj(refreshed);
}).catch(function(){
openWithSlots();
});
@@ -2491,7 +2537,11 @@ function closeFilamentDialog(){
var dlg=document.getElementById('filament-dialog');
if(dlg)dlg.classList.remove('open');
_fdDialogOpen=false;
if(_fdAutoOpenedFile) _fdUserCancelled=true;
if(_fdAutoOpenedFile){
_fdUserCancelled=true;
sessionStorage.setItem('fdUserCancelled','1');
sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile);
}
}
function confirmFilamentPrint(){
@@ -2535,12 +2585,38 @@ function confirmFilamentPrint(){
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
closeFilamentDialog();
if(_filamentDialogMode==='banner'){
// Banner-Modus: normaler print/start mit Slot-Override
// Banner-Modus: identisches Start-Verhalten wie im File-Browser bevorzugen.
var btn=document.getElementById('file-ready-btn');
if(btn){btn.disabled=true;btn.textContent='…';}
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
.then(function(r){return r.json();})
.then(function(){
var startPromise;
if(_storeFileId){
startPromise=fetch(_apiUrl('/kx/print'),{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
file_id:_storeFileId,
filament_assignments:assignments,
excluded_objects:excludedObjects,
auto_leveling:fdAutoLeveling
})
});
}else{
startPromise=post('/printer/print/start',{
filename:S.file_ready||_storeFilename,
filament_assignments:assignments,
excluded_objects:excludedObjects,
auto_leveling:fdAutoLeveling
});
}
startPromise.then(function(r){
if(!r.ok){
return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});
}
return r.json();
})
.then(function(d){
if(d&&d.error) throw new Error(d.error);
if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result));
document.getElementById('file-ready-banner').style.display='none';
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
})
@@ -2618,35 +2694,54 @@ function renderObjectSvg(){
// ── Mid-Print Skip ──
var _skipObjects=[]; // [{name, skipped, willSkip}]
var _skipSvg='';
var _skipPollTimer=null;
function _applySkipDialogState(s){
s=s||{};
_skipSvg=s.svg_b64||'';
var skipped=s.skipped||[];
// Keep local pending selections while live state refreshes in background.
var prevWillSkip={};
(_skipObjects||[]).forEach(function(o){
if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;
});
_skipObjects=(s.objects||[]).map(function(n){
var isSkipped=(skipped.indexOf(n)>=0);
return {
name:n,
skipped:isSkipped,
willSkip:isSkipped?false:!!prevWillSkip[n]
};
});
renderSkipList();
renderSkipSvg();
}
function openSkipDialog(){
document.getElementById('skip-status').textContent='';
document.getElementById('skip-confirm').disabled=false;
_refreshSkipDialog();
if(_skipPollTimer)clearInterval(_skipPollTimer);
_skipPollTimer=setInterval(function(){
var dlg=document.getElementById('skip-dialog');
if(!(dlg&&dlg.classList.contains('open')))return;
_refreshSkipDialog();
},2000);
document.getElementById('skip-dialog').classList.add('open');
}
function _refreshSkipDialog(){
// Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
_skipSvg=s.svg_b64||'';
_skipObjects=(s.objects||[]).map(function(n){
return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false};
});
renderSkipList(); renderSkipSvg();
});
// Frisch nachfragen (skipped-Liste aktualisieren)
fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){
setTimeout(function(){
// Query-Endpoint liefert bereits den frischen, zusammengeführten Skip-State.
fetch(_apiUrl('/kx/skip/query'),{method:'POST'})
.then(function(r){return r.json()})
.then(function(d){
_applySkipDialogState(d.result||{});
})
.catch(function(){
fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){
var s=d.result||{};
var skipped=s.skipped||[];
_skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; });
renderSkipList(); renderSkipSvg();
});
}, 500);
}).catch(function(){});
_applySkipDialogState(d.result||{});
}).catch(function(){});
});
}
function closeSkipDialog(){
if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;}
document.getElementById('skip-dialog').classList.remove('open');
}
function _shortLabel(name){