From d1eb87ba25cebb2c53cdb33b2027655115380d33 Mon Sep 17 00:00:00 2001 From: Dominic Gundel Date: Wed, 21 Oct 2026 21:04:28 +0200 Subject: [PATCH] Grotti 3.5.2: Zu wenig Rest blockiert den Start nicht mehr, und ein anlaufender Druck wird nicht als Neustart neu eingereiht. --- CHANGELOG.md | 10 +++++++ README.md | 2 +- docs/Anleitung-ACE.md | 4 +-- grotti/jetzt.py | 6 ++++- grotti/jobs.py | 40 ++-------------------------- grotti/print_queue.py | 30 ++++++++++++++++----- grotti/store.py | 15 +++-------- grotti/version.py | 2 +- web/static/api.js | 2 +- web/templates/doku.html | 2 +- web/templates/layout.html | 10 ++++--- web/templates/wand.html | 45 +++++++++++++++++++++++++------- web/templates/warteschlange.html | 7 +++-- 13 files changed, 98 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fba58c1..c23159f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ Alle Änderungen an Grotti. Format nach [Keep a Changelog](https://keepachangelo ## [Unreleased] +## [3.5.2] – 2026-10-21 + +### Geändert + +- **Wenig Rest:** ACE oder Spule unter dem Dateigewicht warnt unter Dateien, bricht Start und Warteschlange nicht mehr ab. Falsche Sorte bleibt ein Wartegrund. + +### Behoben + +- **Queue „Nochmal — Start abgebrochen (Neustart)“ während der Drucker anläuft:** Nach dem Upload bleibt der Auftrag in `await_print`, bis Moonraker druckt. Die 45-Sekunden-Regel gilt nur noch, wenn der Upload-Thread weg ist. Wand und Seitenleiste zeigen „startet“ und dass es etwa eine Minute dauern kann. + ## [3.5.1] – 2026-10-20 ### Geändert diff --git a/README.md b/README.md index f40f184..5fce73e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- Version 3.5.1 + Version 3.5.2 Python 3.11+ PostgreSQL Moonraker diff --git a/docs/Anleitung-ACE.md b/docs/Anleitung-ACE.md index 5ea6563..bde6863 100644 --- a/docs/Anleitung-ACE.md +++ b/docs/Anleitung-ACE.md @@ -1,6 +1,6 @@ # ACE und Spulen -Stand **18.10.2026**. Mehrfarb-Drucke brauchen vor dem Start eine Zuordnung Datei-Farbe → ACE-Platz und eine passende Spule am Drucker. +Stand **21.10.2026**. Mehrfarb-Drucke brauchen vor dem Start eine Zuordnung Datei-Farbe → ACE-Platz und eine passende Spule am Drucker. ![ACE-Zuordnung beim Senden](../web/static/doku/ace-zuordnung.png) @@ -44,7 +44,7 @@ Nach einem Grotti-Update mit ACE-Farben: unter Dateien **neu lesen**, damit alte | „T… fehlt in der Map“ | Datei neu lesen, Zuordnung erneut setzen (Slicer-T ≠ ACE-Platz) | | **invalid filament id** (10011700) | Alte Grotti-Version ohne T-Verdichtung, oder Map nicht neu gesetzt. Grotti neu starten, Datei neu lesen, nochmal senden. | | Material-Warnung | Eingelegte Sorte ≠ Datei. Der Start geht trotzdem. PA12-CF steht in Grotti so, an den ACE geht PA6-CF. | -| Rest zu gering | Andere Spule oder leichteres Teil | +| Rest zu gering | Warnung unter Dateien („nur … g, Datei braucht … g“). Der Start geht trotzdem; leer pausiert der Drucker. | | ACE blinkt nicht / kein Wechsel | Spule und Platz prüfen, ggf. Druck neu starten | | Drucker hängt kurz nach Multi-Color-Start | Oft falsche Map (T10 gemappt als Platz 0). Neu lesen + Zuordnung. | diff --git a/grotti/jetzt.py b/grotti/jetzt.py index 4d22fdb..de15911 100644 --- a/grotti/jetzt.py +++ b/grotti/jetzt.py @@ -255,13 +255,17 @@ def build(conn, watcher_latest, printers=None, send_jobs=None): live_by_host[host]["display_step_index"] = job.get("display_step_index") live_by_host[host]["step_index"] = job.get("step_index") else: + phase = (job.get("phase") or "").lower() + starting = phase in ("starting", "await_print") running.append({ "host": host, "name": job.get("printer_name") or host, - "state": "running", + "state": "starting" if starting else "running", + "starting": starting, "progress": job.get("progress") or 0, "remain": job.get("remain") or 0, "filename": job.get("filename") or "", + "message": job.get("message") or "", "queue_id": job.get("id"), "is_ablauf": bool(job.get("is_ablauf")), }) diff --git a/grotti/jobs.py b/grotti/jobs.py index a0e071f..c248755 100644 --- a/grotti/jobs.py +++ b/grotti/jobs.py @@ -415,44 +415,8 @@ def sync_history(conn, cfg): def assert_filament_enough(conn, file_row, spool_id=None, ace_map=None, host=""): - """Dateien-Gewicht gegen Spule oder ACE-Plätze prüfen.""" - need = float((file_row or {}).get("weight_g") or 0) - if need <= 0: - return - mapping = acm.normalize_mapping(ace_map) - if mapping and host: - leftovers = [] - for item in mapping: - slot = int(item["ams_index"]) + 1 - row = conn.execute( - """ - SELECT remaining_g, name FROM spools - WHERE printer_host = %s AND ace_slot = %s - ORDER BY id LIMIT 1 - """, - (host, slot), - ).fetchone() - if row: - leftovers.append(float(row["remaining_g"] or 0)) - if leftovers and sum(leftovers) + 0.5 < need: - raise GrottiError( - "ACE-Plätze haben nur %.0f g, Datei braucht %.0f g." - % (sum(leftovers), need), - "spule", - ) - return - if not spool_id: - return - spool = store.get_spool_row(conn, spool_id) - if not spool: - return - left = float(spool["remaining_g"] or 0) - if left + 0.5 < need: - raise GrottiError( - "Spule „%s“ hat nur %.0f g, Datei braucht %.0f g." - % (spool["name"], left, need), - "spule", - ) + """Zu wenig Rest bricht den Start nicht ab. Die Oberfläche warnt vorher.""" + return def prepare_send(conn, file_id, host, confirm, override, spool_id, ace_map=None): diff --git a/grotti/print_queue.py b/grotti/print_queue.py index 0f44ebe..0c7d33b 100644 --- a/grotti/print_queue.py +++ b/grotti/print_queue.py @@ -30,7 +30,9 @@ BUSY = {"printing", "paused", "heating", "error"} KEEPALIVE_S = 120 # Nach Start ohne print_active: Upload/MQTT/LeviQ brauchen Zeit, aber nicht ewig PRINT_START_TIMEOUT_S = 600 -# Einfacher Queue-Start ohne Worker (typisch Grotti-Neustart) +# Upload-Worker weg (typisch Grotti-Neustart mitten im Hochladen). +# Nach erfolgreichem Senden ist die Phase await_print — dort gilt das nicht, +# sonst wird ein noch anlaufender Druck als „Neustart“ neu eingereiht. SIMPLE_START_STUCK_S = 45 @@ -421,16 +423,18 @@ class PrintQueueDispatcher: from grotti import orders orders.attach_order_item_to_live_run(wconn, host, name, oid) - # Phase „starting“ lassen, bis Moonraker printing meldet. - # Sofort print_active + alter complete/standby → fälschlich „Fertig.“ + # await_print: Datei ist raus, Drucker meldet printing oft erst + # nach einer Minute. Nicht bei starting lassen — sonst gilt nach + # 45 s der Neustart-Retry und der zweite Start trifft „druckt schon“. store.update_print_queue( wconn, item_id, { "status": "running", - "message": "Startet auf %s: %s" % (printer_copy["name"], name), + "message": "Datei ist auf %s. Druckstart kann etwa eine Minute dauern." + % printer_copy["name"], "host": host, - "phase": "starting", + "phase": "await_print", "phase_started_at": "now", }, ) @@ -487,7 +491,7 @@ class PrintQueueDispatcher: """Einfach-Druck: running bleiben bis Moonraker fertig/abgebrochen.""" phase = (item.get("phase") or "").lower() state = (live.get("state") or "").lower() - if phase == "starting": + if phase in ("starting", "await_print"): if state in ("printing", "paused", "heating") or live.get("print_active"): store.update_print_queue( conn, @@ -498,6 +502,20 @@ class PrintQueueDispatcher: "message": "Druckt…", }, ) + return + if phase == "await_print" and self._print_start_timed_out(item): + store.update_print_queue( + conn, + item["id"], + { + "status": "error", + "message": "Druckstart-Timeout: Drucker hat nach %d min nicht angefangen." + % (PRINT_START_TIMEOUT_S // 60), + "phase": "", + "phase_started_at": None, + }, + ) + return # complete/standby vom vorherigen Auftrag: noch warten, nicht „Fertig.“ return if phase != "print_active": diff --git a/grotti/store.py b/grotti/store.py index 41c3152..2637795 100644 --- a/grotti/store.py +++ b/grotti/store.py @@ -2858,7 +2858,10 @@ def _match_spools_for_need(need, have, used_ids=None): def material_fit_score(conn, host, file_id): - """Höher = besser. -1 = warten (falsche Sorte mit Profil, oder zu wenig Rest).""" + """Höher = besser. -1 = warten (falsche Sorte mit Profil). + + Zu wenig Rest blockiert nicht; genug Rest gibt Zusatzpunkte. + """ need, weight = _file_needs(conn, file_id) have = _spool_material_set(conn, host) if not need: @@ -2883,11 +2886,6 @@ def material_fit_score(conn, host, file_id): score += 5 if weight and best["remaining_g"] + 1e-6 >= weight: score += 5 - elif weight and all(h["remaining_g"] + 1e-6 < weight for h in matched): - return -1, "braucht %s (%s g fehlen)" % ( - _need_label(n), - int(max(0, weight - max(h["remaining_g"] for h in matched))), - ) return score, "" have_types = {h["material_type"] for h in have if h["material_type"]} @@ -2900,11 +2898,6 @@ def material_fit_score(conn, host, file_id): score += 5 if weight and any(h["remaining_g"] + 1e-6 >= weight for h in matched): score += 5 - elif weight and all(h["remaining_g"] + 1e-6 < weight for h in matched): - return -1, "braucht %s (%s g fehlen)" % ( - _need_label(n), - int(max(0, weight - max(h["remaining_g"] for h in matched))), - ) return score, "" diff --git a/grotti/version.py b/grotti/version.py index d002ae5..e76cd41 100644 --- a/grotti/version.py +++ b/grotti/version.py @@ -1,3 +1,3 @@ """Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen.""" -__version__ = "3.5.1" +__version__ = "3.5.2" diff --git a/web/static/api.js b/web/static/api.js index 01411bb..7d1a9e8 100644 --- a/web/static/api.js +++ b/web/static/api.js @@ -4,7 +4,7 @@ const CSRF = document.querySelector('meta[name="csrf"]')?.content || ""; const STATE_LABELS = { printing: "druckt", paused: "pausiert", complete: "fertig", standby: "bereit", heating: "heizt", error: "Fehler", cancelled: "abgebrochen", offline: "offline", - waiting: "wartet", running: "läuft", + waiting: "wartet", running: "läuft", starting: "startet", }; function escapeHtml(value) { diff --git a/web/templates/doku.html b/web/templates/doku.html index fe2e384..ea5b863 100644 --- a/web/templates/doku.html +++ b/web/templates/doku.html @@ -49,7 +49,7 @@

Auftrag
Ein Eintrag in der Warteschlange: eine Datei für einen Drucker oder für den nächsten freien.
Ablauf
Auftrag mit mehreren Schritten und Wiederholungen. Nur Kobra 3.
Dieser Drucker
Frei und Platte klar → sofort; sonst Warteschlange für genau diesen Drucker.
-
Nächster passender
Grotti claimt den nächsten freien Drucker mit passender Katalog-Sorte und Restfilament.
+
Nächster passender
Grotti claimt den nächsten freien Drucker mit passender Katalog-Sorte. Zu wenig Rest warnt nur und hält den Start nicht auf.
Platte belegt
Nach jedem Druck gesetzt. Blockiert den nächsten Auftrag auf diesem Drucker.
Alert
Wichtiges Ereignis (Fertig, Fehler, Abbruch, offline). Steht in der Seitenleiste und geht auf Wunsch an Telegram.
Ereignis
Jeder Eintrag unter Aktivität. Ablauf-Schritte und Starts sind Ereignisse, aber keine Alerts.
diff --git a/web/templates/layout.html b/web/templates/layout.html index 22350db..957dab3 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -160,7 +160,7 @@ {% endif %}
- +