112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
"""Anycubic ACE Sidecar (.acm) für Moonraker-Start."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
|
|
# Rinkhals mmu_ace: ohne `; referenced_tools = …` kein Auto-Feed / Nachschub.
|
|
TRAILER_WARN = "ACE-Nachschub braucht Slicer-Metadaten/Trailer"
|
|
|
|
|
|
def color_channels(value):
|
|
if isinstance(value, (list, tuple)) and len(value) >= 3:
|
|
out = []
|
|
for channel in value[:3]:
|
|
if isinstance(channel, bool) or not isinstance(channel, (int, float)):
|
|
return []
|
|
out.append(max(0, min(255, int(round(float(channel))))))
|
|
return out
|
|
if value in (None, ""):
|
|
return []
|
|
text = str(value).strip()
|
|
if text.startswith("#"):
|
|
text = text[1:]
|
|
text = re.sub(r"[^0-9A-Fa-f]", "", text)
|
|
if len(text) == 8:
|
|
# Moonraker mmu.gate_color: RRGGBBAA
|
|
text = text[:6]
|
|
if len(text) != 6:
|
|
return []
|
|
try:
|
|
return [int(text[0:2], 16), int(text[2:4], 16), int(text[4:6], 16)]
|
|
except ValueError:
|
|
return []
|
|
|
|
|
|
def acm_filename(gcode_name):
|
|
"""Sidecar neben dem Start-Dateinamen: `job.gcode` → `job.gcode.acm`."""
|
|
name = (gcode_name or "job.gcode").replace("\\", "/").split("/")[-1]
|
|
if name.lower().endswith(".acm"):
|
|
return name
|
|
return name + ".acm"
|
|
|
|
|
|
def acm_upload_names(gcode_name):
|
|
"""Primärer Basename wie Start-filename, plus Stamm-Variante zum Überschreiben.
|
|
|
|
Anycubic/Rinkhals erwarten typisch `name.gcode.acm`. Parallel kann eine
|
|
ältere Identity-Map als `name.acm` liegen und Grotti-Map verdrängen.
|
|
"""
|
|
primary = acm_filename(gcode_name)
|
|
names = [primary]
|
|
base = (gcode_name or "job.gcode").replace("\\", "/").split("/")[-1]
|
|
if base.lower().endswith(".acm"):
|
|
base = base[: -len(".acm")]
|
|
if base.lower().endswith(".gcode"):
|
|
alt = base[: -len(".gcode")] + ".acm"
|
|
if alt.lower() != primary.lower():
|
|
names.append(alt)
|
|
return names
|
|
|
|
|
|
def normalize_mapping(raw):
|
|
if not isinstance(raw, list):
|
|
return []
|
|
out = []
|
|
seen = set()
|
|
for item in raw:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
try:
|
|
paint = int(item.get("paint_index"))
|
|
ams = int(item.get("ams_index"))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if paint < 0 or ams < 0 or ams > 7 or paint in seen:
|
|
continue
|
|
seen.add(paint)
|
|
out.append({"paint_index": paint, "ams_index": ams})
|
|
return out
|
|
|
|
|
|
def build_acm(filaments, mapping, slots):
|
|
filaments = filaments or []
|
|
slots = slots or []
|
|
slot_by_index = {int(slot.get("index")): slot for slot in slots if isinstance(slot.get("index"), int)}
|
|
entries = []
|
|
for item in normalize_mapping(mapping):
|
|
paint = item["paint_index"]
|
|
ams = item["ams_index"]
|
|
filament = filaments[paint] if 0 <= paint < len(filaments) else {}
|
|
slot = slot_by_index.get(ams) or {}
|
|
paint_color = color_channels(filament.get("color")) or [0, 0, 0]
|
|
ams_color = color_channels(slot.get("color")) or paint_color
|
|
material = (filament.get("type") or slot.get("type") or "PLA").strip() or "PLA"
|
|
entries.append(
|
|
{
|
|
"paint_index": paint,
|
|
"paint_color": paint_color,
|
|
"material_type": material,
|
|
"ams_index": ams,
|
|
"ams_color": ams_color,
|
|
}
|
|
)
|
|
if not entries:
|
|
return None
|
|
return {"use_ams": True, "ams_box_mapping": entries}
|
|
|
|
|
|
def acm_bytes(payload):
|
|
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|