Compare commits
3 Commits
nightly-0.
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b05362c2b | |||
| cfe70430d3 | |||
| 2c8a62f130 |
@@ -108,19 +108,29 @@ jobs:
|
||||
. /tmp/nightly_version.env
|
||||
TAG="nightly-${VERSION}"
|
||||
|
||||
# Letzten nightly-Tag als Changelog-Basis ermitteln
|
||||
PREV_TAG=$(git tag --list 'nightly-*' --sort=-version:refname | head -1)
|
||||
[ -z "$PREV_TAG" ] && PREV_TAG="$LAST_STABLE"
|
||||
# Letzten Stable-Tag als Changelog-Basis (nur echte vX.Y.Z-Tags)
|
||||
PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \
|
||||
| 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
|
||||
# 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 \
|
||||
--invert-grep --grep='^chore: nightly' \
|
||||
>> "$BODY_FILE" || true
|
||||
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"
|
||||
|
||||
@@ -142,13 +152,21 @@ jobs:
|
||||
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases/tags/${TAG}" \
|
||||
2>/dev/null || true
|
||||
|
||||
# Release erstellen
|
||||
BODY_JSON=$(awk '{
|
||||
gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t");
|
||||
printf "%s\\n", $0
|
||||
}' "$BODY_FILE" | awk 'BEGIN{printf "\""} {printf "%s", $0} END{printf "\""}')
|
||||
JSON_PAYLOAD="{\"tag_name\":\"${TAG}\",\"name\":\"KX-Bridge ${VERSION} Nightly\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":true}"
|
||||
printf '%s' "$JSON_PAYLOAD" > /tmp/release_body.json
|
||||
# Release erstellen — JSON via python3 serialisieren (kein SIGPIPE-Risiko)
|
||||
python3 - "$TAG" "$VERSION" "$BODY_FILE" <<'PYEOF'
|
||||
import sys, json
|
||||
tag, version, body_file = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
body = open(body_file).read()
|
||||
payload = json.dumps({
|
||||
"tag_name": tag,
|
||||
"name": f"KX-Bridge {version} Nightly",
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": True
|
||||
})
|
||||
with open("/tmp/release_body.json", "w") as f:
|
||||
f.write(payload)
|
||||
PYEOF
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
6
NIGHTLY_CHANGELOG.md
Normal file
6
NIGHTLY_CHANGELOG.md
Normal file
@@ -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)
|
||||
@@ -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"})
|
||||
|
||||
@@ -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='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
|
||||
}
|
||||
var spoolSel='';
|
||||
if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){
|
||||
var curSpool=_slotSpoolMap[String(globalIdx)]||'';
|
||||
var spoolOpts='<option value="">–</option>'+_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 '<option value="'+sp.id+'"'+(String(sp.id)==String(curSpool)?' selected':'')+'>'+
|
||||
escHtml(vendor+name+rem)+'</option>';
|
||||
}).join('');
|
||||
spoolSel='<div onclick="event.stopPropagation()" style="margin-top:6px;border-top:1px solid rgba(255,255,255,.07);padding-top:5px">'
|
||||
+'<div style="font-size:8px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">🧵 Spoolman</div>'
|
||||
+'<select data-spool-slot="'+globalIdx+'" onchange="onAmsSpoolChange(this)" '
|
||||
+'style="width:100%;padding:3px 5px;font-size:10px;border-radius:5px;border:1px solid var(--border);background:var(--card);color:var(--txt);cursor:pointer">'+spoolOpts+'</select>'
|
||||
+'</div>';
|
||||
}
|
||||
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
|
||||
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
|
||||
+'<div class="slot-circle" style="background:'+col+'"></div>'
|
||||
@@ -970,6 +986,7 @@ function applyState(){
|
||||
+vendorBadge
|
||||
+'<div class="slot-label">'+slotLabel+'</div>'
|
||||
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
|
||||
+spoolSel
|
||||
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
|
||||
+'</div>';
|
||||
});
|
||||
@@ -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='<span style="font-size:11px;color:var(--txt2)">Keine AMS-Slots bekannt.</span>';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='<option value="">–</option>'+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 '<option value="'+sp.id+'"'+(String(sp.id)==String(current)?' selected':'')+'>'+
|
||||
escHtml('#'+sp.id+' '+vendor+name+mat2+rem)+'</option>';
|
||||
}).join('');
|
||||
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
|
||||
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
|
||||
'<span style="color:var(--txt2);min-width:60px">Slot '+(idx+1)+' <span style="color:var(--txt2);font-size:10px">'+escHtml(mat)+'</span></span>'+
|
||||
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+opts+'</select></div>';
|
||||
}).join('');
|
||||
}).catch(function(){rows.innerHTML='<span style="font-size:11px;color:var(--err)">Spoolman nicht erreichbar</span>';});
|
||||
}
|
||||
|
||||
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='<strong>Filament mismatch detected</strong><br>'+lines.join('<br>');
|
||||
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;
|
||||
|
||||
@@ -32,17 +32,6 @@
|
||||
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
||||
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
||||
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
|
||||
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
@@ -252,39 +241,51 @@
|
||||
|
||||
<!-- Achsensteuerung -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
|
||||
<div class="joypad">
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||
<button class="joy home" onclick="homeAll()" title="Home All">⌂</button>
|
||||
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||
<div></div>
|
||||
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">Achsensteuerung</span></div>
|
||||
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
|
||||
<!-- XY -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div class="joypad">
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||
<div></div>
|
||||
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||
<div></div>
|
||||
</div>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||
</div>
|
||||
<!-- Z -->
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
|
||||
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||
</div>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-btns">
|
||||
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
|
||||
<!-- Einheitliche Step-Size -->
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:10px;flex-wrap:wrap">
|
||||
<div class="step-btns" style="margin-top:0">
|
||||
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||
<button class="step-btn" onclick="setStep(this,10)">10</button>
|
||||
</div>
|
||||
<input id="step-custom" type="number" min="0.1" max="260" step="0.1"
|
||||
placeholder="mm"
|
||||
style="width:60px;padding:4px 6px;font-size:12px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);text-align:center"
|
||||
oninput="setStepCustom(this)">
|
||||
</div>
|
||||
<div class="home-btns">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||
<!-- Globale Befehle zentriert -->
|
||||
<div style="display:flex;justify-content:center;gap:8px;margin-top:10px">
|
||||
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>↕</span> <span id="ptitle-motion-z">Z-Achse</span></div>
|
||||
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Speed -->
|
||||
<div class="card">
|
||||
@@ -570,6 +571,12 @@
|
||||
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
|
||||
</div>
|
||||
<div class="card" id="spoolman-slot-card" style="display:none">
|
||||
<div class="card-title"><span>🧵</span> <span id="lbl-spoolman-slot-assign">Spoolman — Slot-Zuordnung</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:10px" id="lbl-spoolman-slot-hint">Spoolman-Spool pro AMS-Slot zuweisen. Der Verbrauch wird beim Druck automatisch gemeldet.</div>
|
||||
<div id="spoolman-slot-rows" style="display:flex;flex-direction:column;gap:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:10px" onclick="saveSpoolmanSlots()"><span id="lbl-spoolman-slot-save">Zuordnung speichern</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integrationen -->
|
||||
@@ -655,6 +662,7 @@
|
||||
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-mismatch-warn" style="display:none;margin-bottom:10px;padding:8px 10px;background:rgba(255,160,0,.12);border:1px solid rgba(255,160,0,.4);border-radius:8px;font-size:11px;color:#ffa000"></div>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
|
||||
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Kamera",
|
||||
"panel_extras_fan": "Lüfter",
|
||||
"panel_extras_light": "Licht",
|
||||
"panel_motion_xy": "XY-Achsen",
|
||||
"panel_motion_z": "Z-Achse",
|
||||
"panel_motion_xy": "Achsensteuerung",
|
||||
"panel_print_btn_cancel": "✕ Abbrechen",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Fortsetzen",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "Fehler",
|
||||
"update_none": "Bereits aktuell",
|
||||
"update_restarting": "Starte neu..."
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Camera",
|
||||
"panel_extras_fan": "Fan",
|
||||
"panel_extras_light": "Light",
|
||||
"panel_motion_xy": "XY Axes",
|
||||
"panel_motion_z": "Z Axis",
|
||||
"panel_motion_xy": "Axes Control",
|
||||
"panel_print_btn_cancel": "✕ Cancel",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Resume",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "Error",
|
||||
"update_none": "Already up to date",
|
||||
"update_restarting": "Restarting..."
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Cámara",
|
||||
"panel_extras_fan": "Ventilador",
|
||||
"panel_extras_light": "Luz",
|
||||
"panel_motion_xy": "Ejes XY",
|
||||
"panel_motion_z": "Eje Z",
|
||||
"panel_motion_xy": "Control de Ejes",
|
||||
"panel_print_btn_cancel": "✕ Cancelar",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Reanudar",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "Error",
|
||||
"update_none": "Ya actualizado",
|
||||
"update_restarting": "Reiniciando..."
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Caméra",
|
||||
"panel_extras_fan": "Ventilateur",
|
||||
"panel_extras_light": "Lumière",
|
||||
"panel_motion_xy": "Axes XY",
|
||||
"panel_motion_z": "Axe Z",
|
||||
"panel_motion_xy": "Contrôle des Axes",
|
||||
"panel_print_btn_cancel": "✕ Annuler",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Reprendre",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "Erreur",
|
||||
"update_none": "Déjà à jour",
|
||||
"update_restarting": "Redémarrage…"
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "Camera",
|
||||
"panel_extras_fan": "Ventola",
|
||||
"panel_extras_light": "Luce",
|
||||
"panel_motion_xy": "Assi XY",
|
||||
"panel_motion_z": "Asse Z",
|
||||
"panel_motion_xy": "Controllo Assi",
|
||||
"panel_print_btn_cancel": "✕ Annulla",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Riprendi",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "Errore",
|
||||
"update_none": "Già aggiornato",
|
||||
"update_restarting": "Riavvio in corso..."
|
||||
}
|
||||
}
|
||||
@@ -189,8 +189,7 @@
|
||||
"panel_extras_camera": "相机",
|
||||
"panel_extras_fan": "风扇",
|
||||
"panel_extras_light": "灯光",
|
||||
"panel_motion_xy": "XY 轴",
|
||||
"panel_motion_z": "Z 轴",
|
||||
"panel_motion_xy": "轴控制",
|
||||
"panel_print_btn_cancel": "✕ 取消",
|
||||
"panel_print_btn_pause": "⏸ 暂停",
|
||||
"panel_print_btn_resume": "▶ 继续",
|
||||
@@ -326,4 +325,4 @@
|
||||
"update_error": "错误",
|
||||
"update_none": "已是最新版本",
|
||||
"update_restarting": "重启中..."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user