Slice-Watch mit Ordnern/Filtern, Orca-Nachbearbeitung, Cockpit-Verlauf mit ED, Zeitzone im Browser. Tasten-Grenze und README-API angeglichen, toter Code entfernt, Requirements gepinnt.
573 lines
19 KiB
Python
573 lines
19 KiB
Python
"""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. 3 mm: kalter Kunststoff an der Düse
|
||
# tastet sonst oft 1–2 mm und meldet Restteil.
|
||
"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 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 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 ""
|