Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.
This commit is contained in:
commit
b2e6443c68
69 files changed
+18719
No files matched your search
+671
@@ -0,0 +1,671 @@
|
||||
"""Abräumen auf der Kobra 3: Abkühlen und mit dem Extruder nach vorn schieben."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from grotti.errors import GrottiError
|
||||
from grotti.printers import family_from_name
|
||||
|
||||
# Grenzen gegen Tippfehler (Wischpad ~Y 260, Achse max 260 — Ansetzen knapper davor)
|
||||
LIMITS = {
|
||||
"x_min": 0.0,
|
||||
"x_max": 250.0,
|
||||
"y_min": 0.0,
|
||||
"y_max": 250.0,
|
||||
"z_push_min": 0.6,
|
||||
"z_push_max": 30.0,
|
||||
"z_travel_min": 1.0,
|
||||
"z_travel_max": 80.0,
|
||||
"speed_min": 5.0,
|
||||
"speed_max": 80.0,
|
||||
"bed_max_lo": 15.0,
|
||||
"bed_max_hi": 80.0,
|
||||
"nozzle_max_lo": 20.0,
|
||||
"nozzle_max_hi": 120.0,
|
||||
"fan_lo": 0,
|
||||
"fan_hi": 100,
|
||||
"timeout_lo": 1,
|
||||
"timeout_hi": 120,
|
||||
"max_delta_lo": 0.05,
|
||||
"max_delta_hi": 5.0,
|
||||
"check_timeout_lo": 1,
|
||||
"check_timeout_hi": 15,
|
||||
}
|
||||
|
||||
DEFAULT_COOL = {
|
||||
"type": "cool",
|
||||
"bed_max": 30.0,
|
||||
"nozzle_max": 45.0,
|
||||
"fan_percent": 100,
|
||||
"timeout_min": 25,
|
||||
}
|
||||
|
||||
DEFAULT_SWEEP = {
|
||||
"type": "sweep",
|
||||
"z_push": 2.0,
|
||||
"z_travel": 20.0,
|
||||
"x_from": 25.0,
|
||||
"x_to": 225.0,
|
||||
"x_step": 20.0,
|
||||
# möglichst weit hinten, bevor Z sinkt — Present war 220, Teile oft davor
|
||||
"y_from": 250.0,
|
||||
"y_to": 5.0,
|
||||
"speed": 25.0,
|
||||
}
|
||||
|
||||
# Ein Punkt in der Bettmitte — kein LeviQ, kein 3×3, kein Heizen.
|
||||
DEFAULT_CHECK = {
|
||||
"type": "check",
|
||||
# Result is z= gegen Grenze. Temporär 3 mm — kalter Kunststoff an der Düse
|
||||
# tastet sonst oft 1–2 mm und meldet Restteil. Später wieder straffen.
|
||||
"max_delta_mm": 3.0,
|
||||
"timeout_min": 2,
|
||||
"x": 125.0,
|
||||
"y": 125.0,
|
||||
"ref_x": 25.0,
|
||||
"ref_y": 125.0,
|
||||
"z_clear": 20.0,
|
||||
}
|
||||
|
||||
|
||||
def _num(value, default, lo, hi):
|
||||
try:
|
||||
n = float(value if value is not None else default)
|
||||
except (TypeError, ValueError):
|
||||
n = float(default)
|
||||
return max(lo, min(hi, n))
|
||||
|
||||
|
||||
def _int(value, default, lo, hi):
|
||||
return int(round(_num(value, default, lo, hi)))
|
||||
|
||||
|
||||
def require_kobra_3(printer):
|
||||
family = (printer or {}).get("family") or family_from_name((printer or {}).get("name") or "")
|
||||
if family != "kobra_3":
|
||||
raise GrottiError("Abräumen gibt es nur auf der Kobra 3.", "familie")
|
||||
return family
|
||||
|
||||
|
||||
def normalize_cool(step=None):
|
||||
raw = step if isinstance(step, dict) else {}
|
||||
return {
|
||||
"type": "cool",
|
||||
"bed_max": _num(raw.get("bed_max"), DEFAULT_COOL["bed_max"], LIMITS["bed_max_lo"], LIMITS["bed_max_hi"]),
|
||||
"nozzle_max": _num(
|
||||
raw.get("nozzle_max"), DEFAULT_COOL["nozzle_max"], LIMITS["nozzle_max_lo"], LIMITS["nozzle_max_hi"]
|
||||
),
|
||||
"fan_percent": _int(raw.get("fan_percent"), DEFAULT_COOL["fan_percent"], LIMITS["fan_lo"], LIMITS["fan_hi"]),
|
||||
"timeout_min": _int(
|
||||
raw.get("timeout_min"), DEFAULT_COOL["timeout_min"], LIMITS["timeout_lo"], LIMITS["timeout_hi"]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def normalize_sweep(step=None):
|
||||
raw = step if isinstance(step, dict) else {}
|
||||
z_push = _num(raw.get("z_push"), DEFAULT_SWEEP["z_push"], LIMITS["z_push_min"], LIMITS["z_push_max"])
|
||||
z_travel = _num(raw.get("z_travel"), DEFAULT_SWEEP["z_travel"], LIMITS["z_travel_min"], LIMITS["z_travel_max"])
|
||||
if z_travel < z_push + 1.0:
|
||||
z_travel = min(LIMITS["z_travel_max"], z_push + 1.0)
|
||||
x_from = _num(raw.get("x_from"), DEFAULT_SWEEP["x_from"], LIMITS["x_min"], LIMITS["x_max"])
|
||||
x_to = _num(raw.get("x_to"), DEFAULT_SWEEP["x_to"], LIMITS["x_min"], LIMITS["x_max"])
|
||||
x_step = abs(_num(raw.get("x_step"), DEFAULT_SWEEP["x_step"], 1.0, 100.0))
|
||||
y_from = _num(raw.get("y_from"), DEFAULT_SWEEP["y_from"], LIMITS["y_min"], LIMITS["y_max"])
|
||||
y_to = _num(raw.get("y_to"), DEFAULT_SWEEP["y_to"], LIMITS["y_min"], LIMITS["y_max"])
|
||||
if y_from <= y_to:
|
||||
raise GrottiError("Schieben: Y hinten (y_from) muss größer sein als Y vorn (y_to).", "bahn")
|
||||
return {
|
||||
"type": "sweep",
|
||||
"z_push": z_push,
|
||||
"z_travel": z_travel,
|
||||
"x_from": x_from,
|
||||
"x_to": x_to,
|
||||
"x_step": x_step,
|
||||
"y_from": y_from,
|
||||
"y_to": y_to,
|
||||
"speed": _num(raw.get("speed"), DEFAULT_SWEEP["speed"], LIMITS["speed_min"], LIMITS["speed_max"]),
|
||||
}
|
||||
|
||||
|
||||
def normalize_print(step=None, fallback=None):
|
||||
raw = step if isinstance(step, dict) else {}
|
||||
fb = fallback if isinstance(fallback, dict) else {}
|
||||
try:
|
||||
file_id = int(raw.get("file_id") if raw.get("file_id") is not None else fb.get("file_id"))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise GrottiError("Druck-Schritt braucht eine Datei.", "datei") from exc
|
||||
spool_raw = raw.get("spool_id") if "spool_id" in raw else fb.get("spool_id")
|
||||
try:
|
||||
spool_id = int(spool_raw) if spool_raw not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
spool_id = None
|
||||
ace = raw.get("ace_map") if isinstance(raw.get("ace_map"), list) else fb.get("ace_map")
|
||||
if not isinstance(ace, list):
|
||||
ace = []
|
||||
return {
|
||||
"type": "print",
|
||||
"file_id": file_id,
|
||||
"spool_id": spool_id,
|
||||
"ace_map": ace,
|
||||
"bed_leveling": bool(raw.get("bed_leveling", fb.get("bed_leveling", False))),
|
||||
"override_machine": bool(raw.get("override_machine", fb.get("override_machine", False))),
|
||||
}
|
||||
|
||||
|
||||
def normalize_check(step=None):
|
||||
raw = step if isinstance(step, dict) else {}
|
||||
# Alte enge Grenzen (1.0 / 2.5) → aktueller Default (Düsen-Klumpen).
|
||||
raw_delta = raw.get("max_delta_mm")
|
||||
try:
|
||||
if raw_delta is not None and raw_delta != "":
|
||||
d = float(raw_delta)
|
||||
if abs(d - 2.5) < 1e-9 or abs(d - 1.0) < 1e-9:
|
||||
raw_delta = DEFAULT_CHECK["max_delta_mm"]
|
||||
except (TypeError, ValueError):
|
||||
raw_delta = raw.get("max_delta_mm")
|
||||
return {
|
||||
"type": "check",
|
||||
"max_delta_mm": _num(
|
||||
raw_delta, DEFAULT_CHECK["max_delta_mm"], LIMITS["max_delta_lo"], LIMITS["max_delta_hi"]
|
||||
),
|
||||
"timeout_min": _int(
|
||||
raw.get("timeout_min"),
|
||||
DEFAULT_CHECK["timeout_min"],
|
||||
LIMITS["check_timeout_lo"],
|
||||
LIMITS["check_timeout_hi"],
|
||||
),
|
||||
"x": _num(raw.get("x"), DEFAULT_CHECK["x"], LIMITS["x_min"], LIMITS["x_max"]),
|
||||
"y": _num(raw.get("y"), DEFAULT_CHECK["y"], LIMITS["y_min"], LIMITS["y_max"]),
|
||||
"ref_x": _num(raw.get("ref_x"), DEFAULT_CHECK["ref_x"], LIMITS["x_min"], LIMITS["x_max"]),
|
||||
"ref_y": _num(raw.get("ref_y"), DEFAULT_CHECK["ref_y"], LIMITS["y_min"], LIMITS["y_max"]),
|
||||
"z_clear": _num(
|
||||
raw.get("z_clear"), DEFAULT_CHECK["z_clear"], LIMITS["z_travel_min"], LIMITS["z_travel_max"]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def normalize_steps(steps, fallback=None):
|
||||
if not isinstance(steps, list) or not steps:
|
||||
raise GrottiError("Ablauf braucht mindestens einen Schritt.", "ablauf")
|
||||
out = []
|
||||
for raw in steps:
|
||||
if not isinstance(raw, dict):
|
||||
raise GrottiError("Ungültiger Ablauf-Schritt.", "ablauf")
|
||||
kind = (raw.get("type") or "").strip().lower()
|
||||
if kind == "print":
|
||||
out.append(normalize_print(raw, fallback))
|
||||
elif kind == "cool":
|
||||
out.append(normalize_cool(raw))
|
||||
elif kind == "sweep":
|
||||
out.append(normalize_sweep(raw))
|
||||
elif kind == "check":
|
||||
out.append(normalize_check(raw))
|
||||
else:
|
||||
raise GrottiError("Unbekannter Schritt: %s" % kind, "ablauf")
|
||||
if not any(step["type"] == "print" for step in out):
|
||||
raise GrottiError("Ablauf braucht mindestens einen Druck-Schritt.", "ablauf")
|
||||
return out
|
||||
|
||||
|
||||
def first_print_file_id(steps):
|
||||
for step in steps:
|
||||
if step.get("type") == "print":
|
||||
return step["file_id"]
|
||||
return None
|
||||
|
||||
|
||||
def x_positions(sweep):
|
||||
start = sweep["x_from"]
|
||||
end = sweep["x_to"]
|
||||
step = sweep["x_step"]
|
||||
if abs(end - start) < 0.01:
|
||||
return [round(start, 2)]
|
||||
direction = 1.0 if end >= start else -1.0
|
||||
positions = []
|
||||
x = start
|
||||
# inklusive Endpunkt, ohne Endlosschleife
|
||||
for _ in range(200):
|
||||
positions.append(round(x, 2))
|
||||
if direction > 0 and x >= end - 0.01:
|
||||
break
|
||||
if direction < 0 and x <= end + 0.01:
|
||||
break
|
||||
x += direction * step
|
||||
if direction > 0 and x > end:
|
||||
positions.append(round(end, 2))
|
||||
break
|
||||
if direction < 0 and x < end:
|
||||
positions.append(round(end, 2))
|
||||
break
|
||||
# Duplikate am Ende entfernen
|
||||
cleaned = []
|
||||
for pos in positions:
|
||||
if not cleaned or abs(cleaned[-1] - pos) > 0.01:
|
||||
cleaned.append(pos)
|
||||
return cleaned
|
||||
|
||||
|
||||
# Abkühlen: Düse zur Bettmitte (Park während Abkühlung).
|
||||
COOL_PARK_X = 125.0
|
||||
COOL_PARK_Y = 125.0
|
||||
# Slice-Ende hebt Z nicht — relativ weit hoch, gegen axis_maximum Z gekappt.
|
||||
COOL_PARK_LIFT = 40.0
|
||||
COOL_PARK_Z_MAX_DEFAULT = 250.0
|
||||
COOL_PARK_TOLERANCE_MM = 20.0
|
||||
COOL_PARK_Z_TOLERANCE_MM = 2.0
|
||||
|
||||
|
||||
def cool_script(cool):
|
||||
fan = max(0, min(100, int(cool["fan_percent"])))
|
||||
pwm = int(round(fan * 255 / 100)) if fan > 0 else 0
|
||||
lines = [
|
||||
"SET_HEATER_TEMPERATURE HEATER=extruder TARGET=0",
|
||||
"SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=0",
|
||||
]
|
||||
if pwm > 0:
|
||||
lines.append("M106 S%d" % pwm)
|
||||
else:
|
||||
lines.append("M107")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def cool_park_z_now(live):
|
||||
pos = (live or {}).get("position") or []
|
||||
if len(pos) >= 3 and isinstance(pos[2], (int, float)):
|
||||
return float(pos[2])
|
||||
return None
|
||||
|
||||
|
||||
def cool_park_z_max(live):
|
||||
z_max = (live or {}).get("z_max")
|
||||
if isinstance(z_max, (int, float)) and float(z_max) > 10:
|
||||
return float(z_max)
|
||||
return float(COOL_PARK_Z_MAX_DEFAULT)
|
||||
|
||||
|
||||
def cool_park_lift_mm(live):
|
||||
"""Relativer Z-Hub, begrenzt auf verbleibenden Weg bis Z-max."""
|
||||
z_now = cool_park_z_now(live)
|
||||
z_max = cool_park_z_max(live)
|
||||
want = float(COOL_PARK_LIFT)
|
||||
if z_now is None:
|
||||
return want
|
||||
headroom = z_max - z_now
|
||||
if headroom < 0.5:
|
||||
return 0.0
|
||||
return min(want, headroom)
|
||||
|
||||
|
||||
def cool_park_script(live=None):
|
||||
"""Relativ Z hoch (gegen Z-max gekappt), dann Bettmitte."""
|
||||
lift = cool_park_lift_mm(live)
|
||||
lines = ["M83", "G90"]
|
||||
if lift >= 0.5:
|
||||
lines.extend([
|
||||
"G91",
|
||||
"G0 Z%.2f F1800" % lift,
|
||||
"G90",
|
||||
])
|
||||
lines.extend([
|
||||
"G0 X%.2f Y%.2f F6000" % (COOL_PARK_X, COOL_PARK_Y),
|
||||
"M400",
|
||||
"G90",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def cool_parked(live, before_z=None, lift_mm=None, tolerance_mm=COOL_PARK_TOLERANCE_MM):
|
||||
"""True wenn Mitte und geplanter Z-Hub angekommen ist (before_z/lift_mm nötig)."""
|
||||
pos = (live or {}).get("position") or []
|
||||
if len(pos) < 3 or not all(isinstance(v, (int, float)) for v in pos[:3]):
|
||||
return False
|
||||
dx = float(pos[0]) - COOL_PARK_X
|
||||
dy = float(pos[1]) - COOL_PARK_Y
|
||||
if (dx * dx + dy * dy) ** 0.5 > float(tolerance_mm):
|
||||
return False
|
||||
if before_z is None or lift_mm is None:
|
||||
return False
|
||||
if float(lift_mm) < 0.5:
|
||||
return True
|
||||
need = float(before_z) + float(lift_mm) - float(COOL_PARK_Z_TOLERANCE_MM)
|
||||
return float(pos[2]) >= need
|
||||
|
||||
|
||||
def idle_timeout_script(cool=None):
|
||||
"""Leerlauf-Timeout verlängern — Stepper bleiben gehomed.
|
||||
|
||||
Ohne cool-Schritt: 1 h (Ablauf-Druck). Mit cool: Cool-Zeit + 10 min.
|
||||
"""
|
||||
if cool and isinstance(cool, dict) and cool.get("timeout_min") is not None:
|
||||
seconds = max(900, int(cool["timeout_min"]) * 60 + 600)
|
||||
else:
|
||||
seconds = 3600
|
||||
return "SET_IDLE_TIMEOUT TIMEOUT=%d" % seconds
|
||||
|
||||
|
||||
def strip_end_motor_disable(content):
|
||||
"""M84/M18 aus G-code entfernen — Slice-Ende schaltet sonst die Stepper aus.
|
||||
|
||||
Anycubic Present-Ende: `M84; disable motors` nach X250 Y220. Danach unhomed,
|
||||
Abkühlen/Schieben scheitern. Kommentarzeilen bleiben.
|
||||
"""
|
||||
if content is None:
|
||||
return content
|
||||
if isinstance(content, bytes):
|
||||
text = content.decode("utf-8", errors="replace")
|
||||
as_bytes = True
|
||||
else:
|
||||
text = str(content)
|
||||
as_bytes = False
|
||||
out = []
|
||||
for line in text.splitlines(keepends=True):
|
||||
body = line.split(";", 1)[0].strip().upper()
|
||||
if body == "M84" or body.startswith("M84 ") or body == "M18" or body.startswith("M18 "):
|
||||
continue
|
||||
out.append(line)
|
||||
text = "".join(out)
|
||||
return text.encode("utf-8") if as_bytes else text
|
||||
|
||||
|
||||
def restore_position_script(live, sweep, allow_low_z=False):
|
||||
"""Nach M84/Idle: letzte Toolhead-XYZ übernehmen (keine Bewegung).
|
||||
|
||||
Nur echte Moonraker-Position — keine erfundene Z. Nach Slice-M84 steht der Kopf
|
||||
noch am Present; SET_KINEMATIC mit genau dieser XYZ ist korrekt. Zu niedrige Z → Abbruch.
|
||||
"""
|
||||
_ = allow_low_z
|
||||
pos = (live or {}).get("position") or []
|
||||
if len(pos) < 3 or not all(isinstance(v, (int, float)) for v in pos[:3]):
|
||||
raise GrottiError(
|
||||
"Achsen nicht gehomed und letzte Position unbekannt. "
|
||||
"Bei leerem Bett in Mainsail G28, dann Ablauf neu.",
|
||||
"homed",
|
||||
)
|
||||
x, y, z = (float(v) for v in pos[:3])
|
||||
min_z = float((sweep or {}).get("z_push") or 2) + 1
|
||||
if z < min_z:
|
||||
raise GrottiError(
|
||||
"Achsen nicht gehomed und Düse zu tief gemeldet (Z %.1f mm) — unsicher. "
|
||||
"Bei leerem Bett in Mainsail G28, dann Ablauf neu." % z,
|
||||
"homed",
|
||||
)
|
||||
return "SET_KINEMATIC_POSITION X=%.3f Y=%.3f Z=%.3f" % (x, y, z), z
|
||||
|
||||
|
||||
def cool_ready(live, cool):
|
||||
bed = live.get("bed")
|
||||
nozzle = live.get("nozzle")
|
||||
if not isinstance(bed, (int, float)) or not isinstance(nozzle, (int, float)):
|
||||
return False
|
||||
return float(bed) <= float(cool["bed_max"]) and float(nozzle) <= float(cool["nozzle_max"])
|
||||
|
||||
|
||||
def explain_printer_error(message):
|
||||
"""Moonraker/Klipper-Text → kurze Grotti-Meldung."""
|
||||
text = (message or "").strip()
|
||||
low = text.lower()
|
||||
if "must home axis first" in low or "must home" in low:
|
||||
return (
|
||||
"Achsen sind nicht gehomed. Gleich nach dem Druck abräumen "
|
||||
"(bevor die Stepper ausgehen), oder in Mainsail homen — "
|
||||
"Z nur bei leerem Bett."
|
||||
)
|
||||
if "is not homed" in low:
|
||||
return (
|
||||
"Achsen sind nicht gehomed. In Mainsail homen "
|
||||
"(Z nur bei leerem Bett), dann nochmal abräumen."
|
||||
)
|
||||
if "no trigger on probe" in low:
|
||||
return (
|
||||
"Taste hat nicht ausgelöst (Wägezelle, voller Hub ohne Kontakt). "
|
||||
"Bei leerem Bett meist falsche Z nach Leerlauf (zu wenig Hub) — "
|
||||
"nicht Restteil. Düse auf Klumpen prüfen, dann Tasten erneut; "
|
||||
"Grotti setzt vor dem Tasten ein Z-Budget."
|
||||
)
|
||||
return text or "Der Drucker hat den Befehl abgelehnt."
|
||||
|
||||
|
||||
# Senken auf z_push: nie F3000 — bei falscher Z-Null war das Vollgas ins Bett.
|
||||
SWEEP_FEED_DOWN = 400 # mm/min ≈ 6,7 mm/s
|
||||
|
||||
|
||||
def sweep_go_back_high_script(sweep):
|
||||
"""Nur hoch + nach hinten (y_from) — noch nicht senken."""
|
||||
travel = sweep["z_travel"]
|
||||
mid_x = (sweep["x_from"] + sweep["x_to"]) / 2.0
|
||||
feed_travel = max(3000, int(round(float(sweep["speed"]) * 60)))
|
||||
return "\n".join([
|
||||
"G90",
|
||||
"G91",
|
||||
"G0 Z%.2f F1800" % max(20.0, float(travel)),
|
||||
"G90",
|
||||
"G0 X%.2f Y%.2f F%d" % (mid_x, sweep["y_from"], feed_travel),
|
||||
"M400",
|
||||
])
|
||||
|
||||
|
||||
def sweep_lower_to_push_script(sweep):
|
||||
"""Langsam auf z_push — kein F3000-Sturz."""
|
||||
push = sweep["z_push"]
|
||||
return "\n".join([
|
||||
"G90",
|
||||
"G0 Z%.2f F%d" % (push, SWEEP_FEED_DOWN),
|
||||
"M400",
|
||||
])
|
||||
|
||||
|
||||
def sweep_probe_height_script(sweep):
|
||||
"""Kompatibel: hoch, hinten, langsam auf z_push."""
|
||||
return sweep_go_back_high_script(sweep) + "\n" + sweep_lower_to_push_script(sweep)
|
||||
|
||||
|
||||
def assert_clear_before_lower(live, sweep, min_z_mm=15.0):
|
||||
"""Vor dem Senken: Toolhead muss noch klar über dem Bett stehen."""
|
||||
pos = (live or {}).get("position") or []
|
||||
if len(pos) < 3 or not isinstance(pos[2], (int, float)):
|
||||
raise GrottiError("Schieben: Z-Position unbekannt — kein Senken.", "bahn")
|
||||
z = float(pos[2])
|
||||
need = max(float(min_z_mm), float(sweep["z_push"]) + 8.0)
|
||||
if z < need:
|
||||
raise GrottiError(
|
||||
"Schieben gestoppt: nach hinten nur Z %.1f mm (braucht ≥ %.0f mm). "
|
||||
"Senken würde ins Bett/Teil rauschen — Z-Null prüfen, leeres Bett: G28."
|
||||
% (z, need),
|
||||
"bahn",
|
||||
)
|
||||
|
||||
|
||||
def assert_at_push_height(live, sweep, tol_mm=1.5):
|
||||
"""Wenn Z nicht bei z_push liegt, würde die Bahn das Teil nur überfliegen."""
|
||||
pos = (live or {}).get("position") or []
|
||||
if len(pos) < 3 or not isinstance(pos[2], (int, float)):
|
||||
raise GrottiError("Schieben: Z-Position unbekannt — Abbruch.", "bahn")
|
||||
z = float(pos[2])
|
||||
want = float(sweep["z_push"])
|
||||
if abs(z - want) > float(tol_mm):
|
||||
raise GrottiError(
|
||||
"Schieben gestoppt: Düse bei Z %.1f mm, erwartet %.1f mm. "
|
||||
"Mit falscher Z-Höhe fährt die Bahn über das Teil (ohne Schrittverlust). "
|
||||
"Z-Koordinate und reale Höhe passen oft nicht — "
|
||||
"bei leerem Bett in Mainsail G28, dann neu."
|
||||
% (z, want),
|
||||
"bahn",
|
||||
)
|
||||
|
||||
|
||||
def sweep_script(sweep):
|
||||
"""Absolute Bahn: anheben, nach hinten, langsam senken, nach vorn — nur Config-Spuren."""
|
||||
travel = sweep["z_travel"]
|
||||
push = sweep["z_push"]
|
||||
y_from = sweep["y_from"]
|
||||
y_to = sweep["y_to"]
|
||||
# mm/s → mm/min für G0 F
|
||||
feed = int(round(float(sweep["speed"]) * 60))
|
||||
feed_travel = max(feed, 3000)
|
||||
mid_x = (sweep["x_from"] + sweep["x_to"]) / 2.0
|
||||
lines = [
|
||||
"G90",
|
||||
"G91",
|
||||
"G0 Z%.2f F1800" % max(20.0, float(travel)),
|
||||
"G90",
|
||||
"G0 X%.2f Y%.2f F%d" % (mid_x, y_from, feed_travel),
|
||||
]
|
||||
for x in x_positions(sweep):
|
||||
lines.append("G0 X%.2f Y%.2f F%d" % (x, y_from, feed_travel))
|
||||
lines.append("G0 Z%.2f F%d" % (push, SWEEP_FEED_DOWN))
|
||||
lines.append("G0 Y%.2f F%d" % (y_to, feed))
|
||||
lines.append("G0 Z%.2f F%d" % (travel, feed_travel))
|
||||
# hinten oben parken, weg von der Kiste
|
||||
lines.append("G0 X%.2f Y%.2f Z%.2f F%d" % (mid_x, y_from, travel, feed_travel))
|
||||
lines.append("M107")
|
||||
lines.append("M400")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def check_script(check, current_z=None):
|
||||
"""Restteil-Tasten ohne G28 Z — mit aktivem Bettnetz (LeviQ).
|
||||
|
||||
Absolut auf Freiraum (z_clear bzw. aktuelle Z), Tastpunkt, ein PROBE.
|
||||
Result is z= = Höhe in der echten Koordinate. Kein SET Z=100 / Bezug-Null —
|
||||
das hat die Koordinate nur kaputt gemacht.
|
||||
"""
|
||||
check = normalize_check(check)
|
||||
clear = max(25.0, float(check["z_clear"]))
|
||||
z_travel = clear
|
||||
if isinstance(current_z, (int, float)) and float(current_z) > clear:
|
||||
z_travel = float(current_z)
|
||||
return "\n".join([
|
||||
"G90",
|
||||
"G0 Z%.2f F1800" % z_travel,
|
||||
"G0 X%.2f Y%.2f F6000" % (check["x"], check["y"]),
|
||||
"PROBE",
|
||||
"M400",
|
||||
"G90",
|
||||
])
|
||||
|
||||
|
||||
def check_z_from_live(live):
|
||||
"""Nur Notfall: probe.last_z_result. Toolhead-Z nicht nutzen (Retract +2 mm)."""
|
||||
probe_z = (live or {}).get("probe_z")
|
||||
if isinstance(probe_z, (int, float)):
|
||||
return float(probe_z), "probe"
|
||||
return None, ""
|
||||
|
||||
|
||||
def check_lift_script(check):
|
||||
"""Nach dem Tasten absolut hoch — Düse nicht auf dem Bett lassen."""
|
||||
check = normalize_check(check)
|
||||
return "\n".join([
|
||||
"G90",
|
||||
"G0 Z%.2f F1800" % max(25.0, float(check["z_clear"])),
|
||||
"M400",
|
||||
])
|
||||
|
||||
|
||||
def _axis_coords(lo, hi, count):
|
||||
if count <= 1:
|
||||
return [(lo + hi) / 2.0]
|
||||
return [lo + (hi - lo) * i / (count - 1) for i in range(count)]
|
||||
|
||||
|
||||
def _matrix_ok(matrix):
|
||||
if not isinstance(matrix, list) or not matrix:
|
||||
return False
|
||||
width = None
|
||||
for row in matrix:
|
||||
if not isinstance(row, list) or not row:
|
||||
return False
|
||||
if width is None:
|
||||
width = len(row)
|
||||
if len(row) != width:
|
||||
return False
|
||||
for value in row:
|
||||
if not isinstance(value, (int, float)):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def interpolate_mesh_z(mesh, x, y):
|
||||
"""Bilinear aus Roh-probed_matrix. Klipper: Zeile 0 = mesh_min Y."""
|
||||
matrix = mesh.get("matrix") if isinstance(mesh, dict) else None
|
||||
if not _matrix_ok(matrix):
|
||||
return None
|
||||
mesh_min = mesh.get("mesh_min") or [0, 0]
|
||||
mesh_max = mesh.get("mesh_max") or [0, 0]
|
||||
try:
|
||||
x0, y0 = float(mesh_min[0]), float(mesh_min[1])
|
||||
x1, y1 = float(mesh_max[0]), float(mesh_max[1])
|
||||
except (TypeError, ValueError, IndexError):
|
||||
return None
|
||||
rows = len(matrix)
|
||||
cols = len(matrix[0])
|
||||
if cols < 1 or rows < 1:
|
||||
return None
|
||||
if abs(x1 - x0) < 1e-9 or abs(y1 - y0) < 1e-9:
|
||||
return float(matrix[0][0])
|
||||
tx = 0.0 if cols == 1 else (float(x) - x0) / (x1 - x0) * (cols - 1)
|
||||
ty = 0.0 if rows == 1 else (float(y) - y0) / (y1 - y0) * (rows - 1)
|
||||
tx = max(0.0, min(float(cols - 1), tx))
|
||||
ty = max(0.0, min(float(rows - 1), ty))
|
||||
c0 = int(tx)
|
||||
r0 = int(ty)
|
||||
c1 = min(cols - 1, c0 + 1)
|
||||
r1 = min(rows - 1, r0 + 1)
|
||||
fx = tx - c0
|
||||
fy = ty - r0
|
||||
z00 = float(matrix[r0][c0])
|
||||
z10 = float(matrix[r0][c1])
|
||||
z01 = float(matrix[r1][c0])
|
||||
z11 = float(matrix[r1][c1])
|
||||
z0 = z00 * (1 - fx) + z10 * fx
|
||||
z1 = z01 * (1 - fx) + z11 * fx
|
||||
return z0 * (1 - fy) + z1 * fy
|
||||
|
||||
|
||||
def mesh_delta_mm(baseline, measured):
|
||||
"""Max |Δ| und Mittel |Δ|: Messpunkte gegen interpoliertes Baseline-Netz."""
|
||||
if not isinstance(baseline, dict) or not _matrix_ok(baseline.get("matrix")):
|
||||
raise GrottiError("Kein Bettnetz zum Vergleich — vorher nivellieren.", "mesh")
|
||||
if not isinstance(measured, dict) or not _matrix_ok(measured.get("matrix")):
|
||||
raise GrottiError("Neues Bettnetz fehlt nach dem Antasten.", "mesh")
|
||||
matrix = measured["matrix"]
|
||||
mesh_min = measured.get("mesh_min") or [0, 0]
|
||||
mesh_max = measured.get("mesh_max") or [0, 0]
|
||||
try:
|
||||
x0, y0 = float(mesh_min[0]), float(mesh_min[1])
|
||||
x1, y1 = float(mesh_max[0]), float(mesh_max[1])
|
||||
except (TypeError, ValueError, IndexError) as exc:
|
||||
raise GrottiError("Messnetz ohne gültigen Bereich.", "mesh") from exc
|
||||
rows = len(matrix)
|
||||
cols = len(matrix[0])
|
||||
xs = _axis_coords(x0, x1, cols)
|
||||
ys = _axis_coords(y0, y1, rows)
|
||||
deltas = []
|
||||
for ri, y in enumerate(ys):
|
||||
for ci, x in enumerate(xs):
|
||||
expected = interpolate_mesh_z(baseline, x, y)
|
||||
if expected is None:
|
||||
raise GrottiError("Baseline-Netz lässt sich nicht interpolieren.", "mesh")
|
||||
deltas.append(abs(float(matrix[ri][ci]) - expected))
|
||||
if not deltas:
|
||||
raise GrottiError("Keine Messpunkte zum Vergleich.", "mesh")
|
||||
return {
|
||||
"max_delta_mm": max(deltas),
|
||||
"mean_delta_mm": sum(deltas) / len(deltas),
|
||||
"points": len(deltas),
|
||||
}
|
||||
|
||||
|
||||
def step_label(step):
|
||||
if not isinstance(step, dict):
|
||||
return ""
|
||||
kind = step.get("type")
|
||||
if kind == "print":
|
||||
return "Drucken"
|
||||
if kind == "cool":
|
||||
return "Abkühlen"
|
||||
if kind == "sweep":
|
||||
return "Schieben"
|
||||
if kind == "check":
|
||||
return "Tasten"
|
||||
return kind or ""
|
||||
Reference in new issue
Block a user