Files
grotti/grotti/acm.py
T

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")