diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml index 4f8961e..6542cb7 100644 --- a/.gitea/workflows/nightly.yml +++ b/.gitea/workflows/nightly.yml @@ -113,18 +113,23 @@ jobs: | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) [ -z "$PREV_TAG" ] && PREV_TAG=$(git rev-list --max-parents=0 HEAD) - # Changelog generieren — nur feat/fix, keine ci/release/build-Scopes + # Changelog: NIGHTLY_CHANGELOG.md hat Vorrang (manuell gepflegt), + # sonst auto-generiert aus feat/fix-Commits seit letztem Stable-Tag BODY_FILE=$(mktemp) printf '## KX-Bridge %s — Nightly Build\n\n' "$VERSION" > "$BODY_FILE" printf '[experimental] Untested features, for testers only.\n\n' >> "$BODY_FILE" - printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE" - git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \ - | grep -E '^(feat|fix)[:(]' \ - | grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \ - | sed 's/^/- /' \ - >> "$BODY_FILE" || true - if ! grep -q '^- ' "$BODY_FILE"; then - printf '- No user-facing changes in this build\n' >> "$BODY_FILE" + if [ -s NIGHTLY_CHANGELOG.md ]; then + cat NIGHTLY_CHANGELOG.md >> "$BODY_FILE" + else + printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE" + git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \ + | grep -E '^(feat|fix)[:(]' \ + | grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \ + | sed 's/^/- /' \ + >> "$BODY_FILE" || true + if ! grep -q '^- ' "$BODY_FILE"; then + printf '- No user-facing changes in this build\n' >> "$BODY_FILE" + fi fi printf '\n\n---\n\n### Update Docker image\n\n```bash\ndocker compose pull && docker compose up -d\n```\n\n' >> "$BODY_FILE" printf 'Image tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`\n' >> "$BODY_FILE" diff --git a/NIGHTLY_CHANGELOG.md b/NIGHTLY_CHANGELOG.md new file mode 100644 index 0000000..f6a628a --- /dev/null +++ b/NIGHTLY_CHANGELOG.md @@ -0,0 +1,6 @@ +## Changes in this build + +- Unified axes control panel: XY and Z merged into one card, shared step size selector (0.1 / 1 / 5 / 10 mm) plus custom mm input field, Home XY/Z buttons placed directly below their respective pads +- Language selector moved from header bar to Settings → Appearance +- Filament mismatch detection: Upload-and-Print is intercepted when GCode material differs from the loaded AMS slot — slot mapper dialog opens automatically to correct the assignment before printing +- Spoolman: assign a spool per AMS slot directly in the AMS status tab (dropdown per slot kachel) and in the Filaments settings tab (dedicated assignment card) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index dc23f57..afe5c5f 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -870,6 +870,7 @@ class KobraXBridge: "print_speed_mode": 2, "connection_error": "", "file_ready": "", + "filament_mismatch": None, "print_start_dialog": getattr(args, "print_start_dialog", 1), "filament_mode": "toolhead", "supplies_usage": 0, @@ -3273,6 +3274,31 @@ class KobraXBridge: self._state["last_upload_size"] = file_size if auto_print: + mismatch = self._check_filament_mismatch(gcode_filaments) + if mismatch: + log.info(f"Upload+Print blockiert — Filament-Mismatch: {mismatch}") + self._state["file_ready"] = remote_filename + self._state["filament_mismatch"] = mismatch + return web.json_response({ + "done": True, + "filament_mismatch": True, + "mismatch_details": mismatch, + "files": { + "local": { + "name": remote_filename, + "origin": "local", + "path": remote_filename, + "refs": { + "download": f"http://{request.host}/api/files/local/{remote_filename}", + "resource": f"http://{request.host}/api/files/local/{remote_filename}", + } + } + }, + "result": { + "item": {"path": remote_filename, "root": "gcodes"}, + "action": "create_file", + } + }, status=201) log.info(f"Upload+Print (print=true): {remote_filename}") self._state["file_ready"] = "" loop = asyncio.get_event_loop() @@ -3301,6 +3327,45 @@ class KobraXBridge: } }, status=201) + def _check_filament_mismatch(self, gcode_filaments: list | None) -> list[dict] | None: + """Vergleicht GCode-Filamente (is_used=True) mit aktuell belegten AMS-Slots. + + Gibt Liste von Mismatch-Einträgen zurück wenn mindestens ein genutzter + GCode-Slot kein passendes Material im AMS hat — sonst None. + Wird nur ausgelöst wenn AMS-Daten vorhanden sind (mindestens 1 belegter Slot).""" + if not gcode_filaments: + return None + slots = self._ams_slots or [] + occupied = {s["global_index"]: s for s in slots if s.get("type") and s.get("status") == 5} + if not occupied: + return None + mismatches = [] + for f in gcode_filaments: + if not f.get("is_used"): + continue + idx = int(f.get("slot_index", -1)) + gcode_mat = (f.get("material") or "").upper().strip() + if not gcode_mat: + continue + slot = occupied.get(idx) + if slot is None: + mismatches.append({ + "slot_index": idx, + "gcode_material": gcode_mat, + "ams_material": None, + "reason": "empty", + }) + else: + ams_mat = (slot.get("type") or "").upper().strip() + if ams_mat and ams_mat != gcode_mat: + mismatches.append({ + "slot_index": idx, + "gcode_material": gcode_mat, + "ams_material": ams_mat, + "reason": "mismatch", + }) + return mismatches if mismatches else None + def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0, gcode_filaments: list | None = None): self._state["file_ready"] = "" @@ -3492,6 +3557,7 @@ class KobraXBridge: async def handle_api_file_ready_clear(self, request): self._state["file_ready"] = "" + self._state["filament_mismatch"] = None self._thumbnail_b64 = "" self._push_status_update() return web.json_response({"result": "ok"}) diff --git a/web/themes/default/app.js b/web/themes/default/app.js index ad7e02c..e116f34 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -55,6 +55,12 @@ function _loadSpoolmanStatus(){ _slotSpoolMap=d.slot_spools||{}; _updateSpoolmanStatusDot(); _buildSpoolmanSection(); + renderSpoolmanSlotCard(); + if(d.configured){ + fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){ + _spoolmanSpools=sd.spools||[]; + }); + } }).catch(function(){}); } function _updateSpoolmanStatusDot(){ @@ -79,12 +85,8 @@ function _buildSpoolmanSection(){ if(loading)loading.style.display=''; var usedSlots={}; - document.querySelectorAll('#fd-slots select').forEach(function(sel){ - var idx=parseInt(sel.value); - if(idx>=0){ - var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;}); - if(slot&&!usedSlots[idx])usedSlots[idx]=slot; - } + (_amsSlots||[]).forEach(function(slot){ + usedSlots[slot.slot_index]=slot; }); fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){ @@ -377,12 +379,10 @@ function applyLang(){ setText('d-chart-label',T.panel_temps_chart); // Axis labels setText('ptitle-motion-xy',T.panel_motion_xy); - setText('ptitle-motion-z',T.panel_motion_z); document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy); document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors); - document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); // Console setText('ptitle-console',T.panel_console_title); @@ -750,7 +750,7 @@ function applyState(){ frb.style.display='none'; if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ _fdAutoOpenedFile=s.file_ready; - startReadyFileWithSlots(s.file_ready,true); + startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null); } } else { frb.style.display='flex'; @@ -963,6 +963,22 @@ function applyState(){ var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':''); vendorBadge='
'+profile.vendor+'
'; } + var spoolSel=''; + if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){ + var curSpool=_slotSpoolMap[String(globalIdx)]||''; + var spoolOpts=''+_spoolmanSpools.map(function(sp){ + var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':''; + var name=sp.filament?sp.filament.name:'#'+sp.id; + var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':''; + return ''; + }).join(''); + spoolSel='
' + +'
🧵 Spoolman
' + +'' + +'
'; + } html+='
' +'
' @@ -970,6 +986,7 @@ function applyState(){ +vendorBadge +'
'+slotLabel+'
' +'
'+pct+'
' + +spoolSel +'
' +'
'; }); @@ -1076,6 +1093,7 @@ function openSettings(){ pi.value=sec; } renderFilamentMapping(d.filament_profiles||{}); + renderSpoolmanSlotCard(); // Spoolman var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||''; var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30); @@ -1223,6 +1241,64 @@ function _vendorCheck(cb){ var v=cb.getAttribute('data-vendor'); if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v]; } +function renderSpoolmanSlotCard(){ + var card=document.getElementById('spoolman-slot-card'); + var rows=document.getElementById('spoolman-slot-rows'); + if(!card||!rows)return; + if(!_spoolmanStatus.configured){card.style.display='none';return;} + card.style.display=''; + Promise.all([ + fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}), + fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}) + ]).then(function(res){ + var spools=res[0].spools||[]; + var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;}); + if(!slots.length){rows.innerHTML='Keine AMS-Slots bekannt.';return;} + rows.innerHTML=slots.map(function(slot){ + var idx=parseInt(slot.slot_index); + var col=slot.color_hex||'#888'; + var mat=slot.material||''; + var current=_slotSpoolMap[String(idx)]||''; + var opts=''+spools.map(function(sp){ + var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':''; + var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':''; + var name=sp.filament?sp.filament.name:'Spool #'+sp.id; + var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:''; + return ''; + }).join(''); + return '
'+ + ''+ + 'Slot '+(idx+1)+' '+escHtml(mat)+''+ + '
'; + }).join(''); + }).catch(function(){rows.innerHTML='Spoolman nicht erreichbar';}); +} + +function onAmsSpoolChange(sel){ + var idx=sel.getAttribute('data-spool-slot'); + var val=sel.value; + if(val) _slotSpoolMap[String(idx)]=parseInt(val); + else delete _slotSpoolMap[String(idx)]; + var mapping={}; + Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];}); + fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST', + headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})}); +} + +function saveSpoolmanSlots(){ + var mapping={}; + document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){ + var idx=sel.getAttribute('data-spool-slot'); + var val=sel.value; + if(val)mapping[idx]=parseInt(val); + }); + fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){ + _slotSpoolMap=d.slot_spools||{}; + }); +} + function saveVisibleVendors(){ var vendors=Object.keys(_vendorChecklistSel); fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'}, @@ -1792,7 +1868,15 @@ function setStep(btn,v){ currentStep=v; document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); - document.getElementById('step-display').textContent=v; + var ci=document.getElementById('step-custom'); + if(ci)ci.value=''; +} +function setStepCustom(inp){ + var v=parseFloat(inp.value); + if(!isNaN(v)&&v>0){ + currentStep=v; + document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active')); + } } function move(axis,dir,dist){ // axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z @@ -2487,7 +2571,20 @@ function confirmStoreWebVerify(){ }); } -function startReadyFileWithSlots(filename,_autoOpen){ +function _showMismatchWarn(mismatches){ + var el=document.getElementById('fd-mismatch-warn'); + if(!el)return; + if(!mismatches||!mismatches.length){el.style.display='none';el.innerHTML='';return;} + var lines=mismatches.map(function(m){ + var slot='Slot '+(m.slot_index+1); + if(m.reason==='empty') + return '⚠ '+slot+': GCode needs '+m.gcode_material+' — slot is empty'; + return '⚠ '+slot+': GCode needs '+m.gcode_material+', loaded: '+(m.ams_material||'?'); + }); + el.innerHTML='Filament mismatch detected
'+lines.join('
'); + el.style.display=''; +} +function startReadyFileWithSlots(filename,_autoOpen,_mismatch){ if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben var fn=filename||S.file_ready; var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); @@ -2507,9 +2604,11 @@ function startReadyFileWithSlots(filename,_autoOpen){ fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} openFilamentDialog(d.result||[]); + _showMismatchWarn(_mismatch||null); }).catch(function(){ if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} openFilamentDialog([]); + _showMismatchWarn(_mismatch||null); }); } @@ -2745,6 +2844,7 @@ function openFilamentDialog(slots){ function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); + _showMismatchWarn(null); _fdDialogOpen=false; if(_fdAutoOpenedFile){ _fdUserCancelled=true; diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 560a58b..d483d94 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -32,17 +32,6 @@
Standby
-
- - -
@@ -252,39 +241,51 @@
-
XY-Achsen
-
-
- -
- - - -
- -
+
Achsensteuerung
+
+ +
+
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+ +
-
- - - - + +
+
+ + + + +
+
-
- - + +
-
-
Z-Achse
-
- - -
-
Schrittweite: 1 mm
-
@@ -570,6 +571,12 @@
+
@@ -655,6 +662,7 @@

GCode-Kanal → AMS-Slot zuweisen:

+