lots of my fixes and features

This commit is contained in:
Pavulon87
2026-07-20 16:24:14 +02:00
parent eda14db897
commit 79bf5e77db
11 changed files with 267 additions and 26 deletions

View File

@ -62,6 +62,7 @@ CONFIG_ENV_MAPPING = {
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
"VIBRATION_COMPENSATION": (CONFIG_SECTION_PRINT, "vibration_compensation"),
"FLOW_CALIBRATION": (CONFIG_SECTION_PRINT, "flow_calibration"),
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
@ -126,6 +127,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
"vibration_compensation": env_vals.get("VIBRATION_COMPENSATION", "0"),
"flow_calibration": env_vals.get("FLOW_CALIBRATION", "0"),
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
}
@ -451,6 +453,7 @@ DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
FLOW_CALIBRATION = int(get("FLOW_CALIBRATION", "0"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))

View File

@ -49,6 +49,7 @@ DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
FLOW_CALIBRATION = int(get("FLOW_CALIBRATION", "0"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))

View File

@ -1180,8 +1180,10 @@ class KobraXBridge:
def _on_print(self, payload: dict):
d = payload.get("data") or {}
kobra_state = payload.get("state", "")
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "printing")
if kobra_state:
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "printing")
if kobra_state != "" and kobra_state != "updated":
self._state["kobra_state"] = kobra_state
# Automatically switch on the camera at print start (settings option).
@ -1279,7 +1281,8 @@ class KobraXBridge:
kobra_state = proj_state or d.get("state", "")
if kobra_state:
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "standby")
self._state["kobra_state"] = kobra_state
if kobra_state != "" and kobra_state != "updated":
self._state["kobra_state"] = kobra_state
# Hide the upload banner after the print ends (Issue #29) - the state also
# arrives via info/report (project.state) depending on the printer, not only print/report.
if kobra_state in ("finished", "stoped", "canceled"):
@ -1591,7 +1594,41 @@ class KobraXBridge:
return [int(color[0]), int(color[1]), int(color[2]), 255]
return [255, 255, 255, 255]
def _build_auto_ams_box_mapping(
@staticmethod
def _hex_to_rgba(hex_color: str | None) -> list[int]:
"""Convert a '#RRGGBB' (or 'RRGGBB') string from GCode metadata to [r,g,b,255]."""
if not hex_color:
return [255, 255, 255, 255]
h = str(hex_color).strip().lstrip("#")
if len(h) < 6:
return [255, 255, 255, 255]
try:
return [int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 255]
except ValueError:
return [255, 255, 255, 255]
@staticmethod
def _gcode_filament_by_index(gcode_filaments: list[dict] | None) -> dict[int, dict]:
"""Index parsed GCode filament metadata (_extract_filament_info) by paint/slot index."""
by_index: dict[int, dict] = {}
if not gcode_filaments:
return by_index
for f in gcode_filaments:
try:
by_index[int(f.get("slot_index"))] = f
except (TypeError, ValueError):
continue
return by_index
def _color_distance(self, rgb1: list[int], rgb2: list[int]) -> float:
"""Calculate RGB Euclidean distance."""
return (
(rgb1[0] - rgb2[0]) ** 2 +
(rgb1[1] - rgb2[1]) ** 2 +
(rgb1[2] - rgb2[2]) ** 2
) ** 0.5
def _build_auto_ams_box_mapping_old(
self,
warn_on_empty_default: bool = False,
loaded_slots: list[tuple[int, dict]] | None = None,
@ -1627,6 +1664,128 @@ class KobraXBridge:
})
return result
def _build_auto_ams_box_mapping(
self,
warn_on_empty_default: bool = False,
loaded_slots: list[tuple[int, dict]] | None = None,
gcode_filaments: list[dict] | None = None,
) -> list[dict]:
"""Build print mapping by matching G-code filament colors
with currently loaded AMS slots using RGB distance.
"""
# IMPORTANT: loaded_slots is intentionally ignored whenever gcode_filaments
# is provided. Color matching needs access to ALL currently loaded slots,
# not just the ones whose index happens to coincide with the gcode's
# paint_index - otherwise a black filament physically loaded in slot 3
# could never be matched to gcode paint_index 1 just because slot 1
# holds a different color. loaded_slots is only used as a fallback pool
# when the caller has no gcode filament info to match colors against
# (see _build_auto_ams_box_mapping_old for that plain, index-based case).
loaded = loaded_slots if ( gcode_filaments is None or len(gcode_filaments) == 0 ) else None
if loaded is None:
loaded = self._select_loaded_slots_for_print( warn_on_empty_default=warn_on_empty_default )
if not loaded:
return []
loaded_map = {gidx: s for gidx, s in loaded}
log.debug(f"Loaded map: {loaded_map}")
gcode_by_index = self._gcode_filament_by_index(gcode_filaments)
log.debug(f"GCode filaments by index: {gcode_by_index}")
# Slots available for assignment.
available_slots = list(loaded_map.items())
result = []
max_idx = max(gcode_by_index.keys(), default=-1)
# Maximum RGB distance:
# sqrt(255² + 255² + 255²) = 441
# A threshold of 80100 is reasonable for visually similar colors.
MAX_COLOR_DISTANCE = 100
for paint_index in range(max_idx + 1):
req = gcode_by_index.get(paint_index)
paint_color = (
self._hex_to_rgba(req.get("color_hex"))
if req else
[255, 255, 255, 255]
)
default_material = (
req.get("material", "PLA")
if req else
"PLA"
)
best_match = None
best_distance = float("inf")
if req:
requested_rgb = paint_color[:3]
for slot_idx, slot in available_slots:
slot_color = slot.get("color")
if not slot_color or len(slot_color) < 3:
continue
# Skip slots with a different material type.
if slot.get("type") != default_material:
continue
distance = self._color_distance(
requested_rgb,
slot_color[:3],
)
if distance < best_distance:
best_distance = distance
best_match = (slot_idx, slot)
if best_match and best_distance <= MAX_COLOR_DISTANCE:
slot_idx, slot = best_match
available_slots.remove(best_match)
log.debug(
f"Matched paint index {paint_index}: "
f"GCODE={paint_color[:3]} "
f"AMS={slot.get('color')} "
f"distance={best_distance:.2f} "
f"slot={slot_idx}"
)
result.append({
"paint_index": paint_index,
"ams_index": self._slot_to_print_ams_index(slot_idx),
"paint_color": paint_color,
"ams_color": self._slot_color_rgba(slot),
"material_type": slot.get("type", default_material),
})
else:
log.debug(
f"No AMS match for paint index {paint_index}: "
f"color={paint_color[:3]} "
f"best_distance={best_distance:.2f}"
)
result.append({
"paint_index": paint_index,
"ams_index": self._slot_to_print_ams_index(paint_index),
"paint_color": paint_color,
"ams_color": [255, 255, 255, 255],
"material_type": default_material,
})
return result
def _build_assigned_ams_box_mapping(self, assignments: list) -> tuple[list[dict], int, int]:
"""Build print mapping from UI filament assignments.
@ -2958,7 +3117,7 @@ class KobraXBridge:
return self._json_cors({"error": "no usable filament assignments for current filament mode"}, status=400)
else:
# No dialog -> all occupied slots as with a normal upload print
ams_box_mapping = self._build_auto_ams_box_mapping()
ams_box_mapping = self._build_auto_ams_box_mapping_old()
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
filename = gcode_file["filename"]
@ -3453,7 +3612,7 @@ class KobraXBridge:
"task_settings": {
"auto_leveling": auto_leveling,
"vibration_compensation": getattr(self._args, "vibration_compensation", 0),
"flow_calibration": 0,
"flow_calibration": getattr(self._args, "flow_calibration", 0),
"dry_mode": 0,
"ai_settings": {"status": 0, "count": 0, "type": ai_type},
"timelapse": {"status": 0, "count": 0, "type": timelapse_type},
@ -3496,7 +3655,12 @@ class KobraXBridge:
# used slots; used-but-unloaded -> a warning may follow later.
loaded = [(gidx, s) for (gidx, s) in loaded if gidx in used_paint_indices]
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
used_gcode_filaments = [
f for f in (gcode_filaments or [])
if used_paint_indices is not None and f.get("slot_index") in used_paint_indices
]
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded, gcode_filaments=used_gcode_filaments)
log.debug(f"AMS slots: {len(loaded)} mapped (used paints: {used_paint_indices}) -> {[i for i, _ in loaded]}")
payload = self._build_print_payload(
filename, url, md5, filesize,
@ -4268,6 +4432,7 @@ class KobraXBridge:
"print_speed_mode": s["print_speed_mode"],
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"vibration_compensation": getattr(self._args, "vibration_compensation", 0),
"flow_calibration": getattr(self._args, "flow_calibration", 0),
"camera_on_print": getattr(self._args, "camera_on_print", 0),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"light_on": s["light_on"],
@ -4420,6 +4585,7 @@ class KobraXBridge:
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"vibration_compensation": getattr(self._args, "vibration_compensation", 0),
"flow_calibration": getattr(self._args, "flow_calibration", 0),
"camera_on_print": getattr(self._args, "camera_on_print", 0),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
@ -4458,6 +4624,7 @@ class KobraXBridge:
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
cfg.set("print", "vibration_compensation", str(int(bool(data.get("vibration_compensation", getattr(self._args, "vibration_compensation", 0))))))
cfg.set("print", "flow_calibration", str(int(bool(data.get("flow_calibration", getattr(self._args, "flow_calibration", 0))))))
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
@ -5602,6 +5769,7 @@ def main():
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
parser.add_argument("--vibration-compensation", type=int, default=env_loader.VIBRATION_COMPENSATION)
parser.add_argument("--flow-calibration", type=int, default=env_loader.FLOW_CALIBRATION)
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)

View File

@ -437,6 +437,7 @@ function applyLang(){
setText('opt-slot-auto',T.settings_slot_auto);
setText('lbl-auto-leveling',T.settings_auto_leveling);
setText('lbl-vibration-compensation',T.settings_vibration_compensation);
setText('lbl-flow-calibration',T.settings_flow_calibration);
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
setText('opt-file-ready-banner',T.settings_file_ready_banner);
@ -1091,6 +1092,7 @@ function openSettings(){
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
var vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation;
var vc=document.getElementById('s-flow-calibration');if(vc)vc.checked=!!d.flow_calibration;
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0));
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
@ -1859,6 +1861,7 @@ function saveSettings(){
default_ams_slot: document.getElementById('s-default-slot').value,
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
vibration_compensation: (document.getElementById('s-vibration-compensation')||{}).checked?1:0,
flow_calibration: (document.getElementById('s-flow-calibration')||{}).checked?1:0,
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
web_upload_warning:webUploadWarning,
@ -2915,29 +2918,85 @@ function openFilamentDialog(slots){
var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2];
return (dr*dr + dg*dg + db*db);
}
var defaultSlotByPaint={};
var usedDefaultSlot={};
channels.forEach(function(gc,i){
// Globale, farboptimale Zuordnung Kanal -> Slot.
// Anforderungen: 1) Material muss passen (harter Filter, wie bisher)
// 2) Farbe so nah wie möglich
// 3) Position spielt praktisch keine Rolle mehr
// Ein rein gieriger Ansatz (Kanal für Kanal den "nächsten" Slot wählen)
// kann bei knappen/mehrdeutigen Slots suboptimale Gesamtlösungen liefern
// (z.B. Kanal A "verbraucht" den Slot, der eigentlich viel besser zu
// Kanal B gepasst hätte). Da ein AMS in der Praxis nur wenige Slots hat
// (üblich: bis zu 4), lohnt sich Brute-Force über alle möglichen
// Zuordnungen mit Pruning - liefert garantiert das global beste Ergebnis,
// ganz ohne einen vollen Hungarian-Algorithmus implementieren zu müssen.
function _bestAssignment(slotsPerChannel){
var n=slotsPerChannel.length;
var assign=new Array(n).fill(-1);
// Sicherheitsnetz für unrealistisch viele Kanäle: gieriger Fallback
// (kleinster Farbabstand zuerst bedient), um exponentielle Laufzeit zu
// vermeiden. Greift in der Praxis nie, ein AMS hat max. wenige Slots.
if(n>8){
var used={};
var order=slotsPerChannel.map(function(_,idx){return idx;}).sort(function(a,b){
var da=slotsPerChannel[a].length?slotsPerChannel[a][0].dist:Infinity;
var db=slotsPerChannel[b].length?slotsPerChannel[b][0].dist:Infinity;
return da-db;
});
order.forEach(function(i){
var cands=slotsPerChannel[i];
if(!cands.length)return;
var pick=cands.find(function(c){return !used[c.slot_index];})||cands[0];
assign[i]=pick.slot_index;
used[pick.slot_index]=true;
});
return assign;
}
var bestCost=Infinity, bestAssign=assign.slice(), used={};
(function backtrack(idx,cost){
if(cost>=bestCost)return; // Pruning: kann eh nicht mehr besser werden
if(idx===n){ bestCost=cost; bestAssign=assign.slice(); return; }
var candidates=slotsPerChannel[idx];
if(!candidates.length){
assign[idx]=-1;
backtrack(idx+1,cost);
return;
}
var placedFree=false;
for(var k=0;k<candidates.length;k++){
var c=candidates[k];
if(used[c.slot_index])continue;
placedFree=true;
used[c.slot_index]=true;
assign[idx]=c.slot_index;
backtrack(idx+1,cost+c.dist);
used[c.slot_index]=false;
}
if(!placedFree){
// keine freien kompatiblen Slots mehr -> Mehrfachbelegung des
// farblich besten kompatiblen Slots zulassen (besser als leer)
var best=candidates[0];
assign[idx]=best.slot_index;
backtrack(idx+1,cost+best.dist);
}
assign[idx]=-1;
})(0,0);
return bestAssign;
}
var slotsPerChannel=channels.map(function(gc){
var compatible=_amsSlots.filter(function(s){
return _materialsCompatible(gc.material, s.material);
});
if(!compatible.length){
defaultSlotByPaint[i]=-1;
return;
}
var ranked=compatible.slice().sort(function(a,b){
var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i);
if(da!==db)return da-db;
var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex);
if(ca!==cb)return ca-cb;
return (a.slot_index||0)-(b.slot_index||0);
});
var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0];
defaultSlotByPaint[i]=chosen?chosen.slot_index:-1;
if(chosen) usedDefaultSlot[chosen.slot_index]=1;
return compatible.map(function(s){
return {slot_index:s.slot_index, dist:_colorDist(gc.color_hex, s.color_hex)};
}).sort(function(a,b){ return a.dist-b.dist; });
});
var _assignResult=_bestAssignment(slotsPerChannel);
var defaultSlotByPaint={};
channels.forEach(function(gc,i){ defaultSlotByPaint[i]=_assignResult[i]; });
if(!_amsSlots.length){
body.innerHTML='<p style="color:var(--txt2);font-size:13px;text-align:center;padding:16px 0">'+T.fd_no_slots_msg.replace('{br}','<br>')+'</p>';
@ -3391,4 +3450,4 @@ function loadPrinterTab(){
}).catch(function(e){
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">Fehler: '+e+'</div>';
});
}
}

View File

@ -513,6 +513,10 @@
<input type="checkbox" id="s-vibration-compensation" style="width:auto;margin:0">
<label id="lbl-vibration-compensation" style="margin:0;cursor:pointer" for="s-vibration-compensation">Resonance compensation before print</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-flow-calibration" style="width:auto;margin:0">
<label id="lbl-flow-calibration" style="margin:0;cursor:pointer" for="s-flow-calibration">Flow calibration before print</label>
</div>
<div class="modal-field">
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
<select id="s-file-ready-mode">

View File

@ -235,6 +235,7 @@
"settings_file_ready_banner": "Druckleiste",
"settings_file_ready_dialog": "Druckdialog",
"settings_file_ready_mode": "Nach Upload: Druckstart-Verhalten",
"settings_flow_calibration": "Flow-Kalibrierung vor Druck",
"settings_integrations": "Integrationen",
"settings_language": "Sprache",
"settings_mode_id": "Mode-ID",

View File

@ -235,6 +235,7 @@
"settings_file_ready_banner": "Print bar",
"settings_file_ready_dialog": "Print dialog",
"settings_file_ready_mode": "After upload: Start print behavior",
"settings_flow_calibration": "Flow calibration before print",
"settings_integrations": "Integrations",
"settings_language": "Language",
"settings_mode_id": "Mode ID",

View File

@ -235,6 +235,7 @@
"settings_file_ready_banner": "Barra de impresión",
"settings_file_ready_dialog": "Diálogo de impresión",
"settings_file_ready_mode": "Después de carga: Comportamiento de inicio de impresión",
"settings_flow_calibration": "Calibración de flujo antes de imprimir",
"settings_integrations": "Integraciones",
"settings_language": "Idioma",
"settings_mode_id": "ID de modo",

View File

@ -234,6 +234,7 @@
"settings_file_ready_banner": "Barre d'impression",
"settings_file_ready_dialog": "Dialogue d'impression",
"settings_file_ready_mode": "Après téléchargement : Comportement de démarrage d'impression",
"settings_flow_calibration": "Calibration du flux avant impression",
"settings_integrations": "Intégrations",
"settings_language": "Langue",
"settings_mode_id": "ID du mode",

View File

@ -234,6 +234,7 @@
"settings_file_ready_banner": "Barra di stampa",
"settings_file_ready_dialog": "Finestra di dialogo stampa",
"settings_file_ready_mode": "Dopo il caricamento: Comportamento di avvio stampa",
"settings_flow_calibration": "Calibrazione flusso prima della stampa",
"settings_integrations": "Integrazioni",
"settings_language": "Lingua",
"settings_mode_id": "ID modalità",

View File

@ -235,6 +235,7 @@
"settings_file_ready_banner": "打印栏",
"settings_file_ready_dialog": "打印对话框",
"settings_file_ready_mode": "上传后:开始打印行为",
"settings_flow_calibration": "打印前流量校准",
"settings_integrations": "集成",
"settings_language": "语言",
"settings_mode_id": "模式 ID",