neustart und alles inw arteschalnge des gleichen typs auf einmal holen
This commit is contained in:
189
agent.py
189
agent.py
@@ -197,8 +197,10 @@ def main():
|
||||
# „Printing" (Job war sehr kurz), gilt er als fertig – verhindert Deadlock.
|
||||
start_grace = max(60, poll * 6)
|
||||
|
||||
# Kontext des aktuell an die Maschine gesendeten Jobs (oder None). Der
|
||||
# nächste Job wird ERST gesendet, wenn die Maschine wieder „Idle" ist –
|
||||
# Kontext des aktuell an die Maschine gesendeten Batches (oder None):
|
||||
# {"contexts": [ctx, …], "gestartet": bool, "ts": monotonic}. Ein Batch fasst
|
||||
# ALLE queued-Jobs gleichen Typs+Templates zu EINEM Durchlauf zusammen. Der
|
||||
# nächste Batch wird ERST gesendet, wenn die Maschine wieder „Idle" ist –
|
||||
# sonst würde ein neuer write_svg (clear=1) den laufenden Druck verwerfen.
|
||||
laufend = None
|
||||
|
||||
@@ -254,20 +256,24 @@ def main():
|
||||
# das „Printing" nie zu Gesicht bekommen.
|
||||
if laufend is not None:
|
||||
if laufend.get("gestartet") or (time.monotonic() - laufend["ts"] > start_grace):
|
||||
_abschluss(dx, laufend)
|
||||
for ctx in laufend["contexts"]:
|
||||
_abschluss(dx, ctx)
|
||||
laufend = None
|
||||
else:
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
# Nächsten Job nur senden, wenn nichts (mehr) läuft.
|
||||
# Nichts läuft → alle queued-Jobs des aktuellen Formats holen und ALLE
|
||||
# vom selben Typ (gleiches Template) als EINEN Durchlauf drucken. Danach
|
||||
# holt die nächste Runde automatisch den Rest desselben Typs (falls noch
|
||||
# queued) bzw. den nächsten Typ.
|
||||
if laufend is None:
|
||||
current_format = (maschine.get("current_format") or "").strip()
|
||||
# Format-Warteschlange: nur passendes Format, kein Auto-Anschluss.
|
||||
if current_format:
|
||||
job = _naechster_job(dx, machine_id, current_format)
|
||||
if job:
|
||||
laufend = _sende(dx, plotter, job, KIND)
|
||||
jobs = _naechste_jobs(dx, machine_id, current_format)
|
||||
if jobs:
|
||||
laufend = _sende_batch(dx, plotter, _batch_gleicher_typ(jobs), KIND)
|
||||
|
||||
time.sleep(poll)
|
||||
|
||||
@@ -286,8 +292,10 @@ def _heartbeat(dx, machine_id, status):
|
||||
pass
|
||||
|
||||
|
||||
def _naechster_job(dx, machine_id, current_format):
|
||||
jobs = dx.get(
|
||||
def _naechste_jobs(dx, machine_id, current_format):
|
||||
"""ALLE queued-Jobs der Maschine im aktuellen Format (sortiert nach Priorität,
|
||||
dann Alter). Die Auswahl fürs gemeinsame Drucken trifft _batch_gleicher_typ."""
|
||||
return dx.get(
|
||||
"/items/jobs",
|
||||
**{
|
||||
"filter[machine][_eq]": machine_id,
|
||||
@@ -295,80 +303,133 @@ def _naechster_job(dx, machine_id, current_format):
|
||||
# Nur Jobs des aktuell eingelegten Formats.
|
||||
"filter[format][_eq]": current_format,
|
||||
"sort": "-priority,date_created",
|
||||
"limit": 1,
|
||||
# Praktisch „alle" – nur eine Obergrenze, damit die Abfrage begrenzt bleibt.
|
||||
"limit": 500,
|
||||
"fields": "id,type,output_ref,params,numbers,"
|
||||
"order.id,order.order_number,order.needs_envelope,"
|
||||
"template.width,template.height,"
|
||||
"template.xpos,template.ypos,template.scale,template.rotation",
|
||||
},
|
||||
)
|
||||
return jobs[0] if jobs else None
|
||||
) or []
|
||||
|
||||
|
||||
def _sende(dx, plotter, job, KIND):
|
||||
"""Sendet einen Job an die Maschine und markiert ihn als „printing".
|
||||
|
||||
Gibt einen Kontext zum späteren Abschluss zurück (oder None bei Fehler).
|
||||
Es wird NICHT auf das Druckende gewartet – das übernimmt die Hauptschleife
|
||||
(erst wenn die Maschine wieder „Idle" ist, wird der nächste Job gesendet).
|
||||
"""
|
||||
job_id = job["id"]
|
||||
order = job.get("order") or {}
|
||||
order_id = order.get("id")
|
||||
# Ordner: entweder Auftragsnummer oder ein output_ref (z. B. Testdruck).
|
||||
order_nr = job.get("output_ref") or order.get("order_number")
|
||||
def _effektives_template(job):
|
||||
"""Template des Jobs inkl. Pro-Druck-Übersteuerung (params) aus dem Popup."""
|
||||
tpl = dict(job.get("template") or {})
|
||||
# Pro-Druck-Übersteuerung der Maße (aus dem Druck-Popup) hat Vorrang.
|
||||
params = job.get("params")
|
||||
if isinstance(params, dict):
|
||||
for k in ("width", "height", "xpos", "ypos", "scale", "rotation"):
|
||||
if params.get(k) is not None:
|
||||
tpl[k] = params[k]
|
||||
nummern = parse_nummern(job.get("numbers"))
|
||||
return tpl
|
||||
|
||||
|
||||
def _template_sig(job):
|
||||
t = _effektives_template(job)
|
||||
return tuple(t.get(k) for k in ("width", "height", "xpos", "ypos", "scale", "rotation"))
|
||||
|
||||
|
||||
def _batch_gleicher_typ(jobs):
|
||||
"""Aus der sortierten queued-Liste den Kopf-Job nehmen und ALLE queued-Jobs
|
||||
gleichen Typs UND gleichen Templates dazu sammeln – ein Format hat i. d. R.
|
||||
genau ein Template, also faktisch „alles von einem Typ auf einmal". Jobs mit
|
||||
abweichendem Template laufen im nächsten Durchgang. Reihenfolge (Priorität/
|
||||
Alter) bleibt erhalten."""
|
||||
kopf = jobs[0]
|
||||
typ = kopf.get("type") or "brief"
|
||||
sig = _template_sig(kopf)
|
||||
return [j for j in jobs
|
||||
if (j.get("type") or "brief") == typ and _template_sig(j) == sig]
|
||||
|
||||
|
||||
def _job_dateien(dx, job, KIND):
|
||||
"""Lädt die zu druckenden SVGs eines Jobs in Stapel-Reihenfolge → [(name, bytes)]."""
|
||||
order = job.get("order") or {}
|
||||
# Ordner: entweder Auftragsnummer oder ein output_ref (z. B. Testdruck).
|
||||
order_nr = job.get("output_ref") or order.get("order_number")
|
||||
if not order_nr:
|
||||
raise RuntimeError("Auftragsnummer fehlt am Job.")
|
||||
typ = job.get("type") or "brief"
|
||||
print(f"[Agent] Job {job_id}: {typ} für {order_nr} → an Maschine senden")
|
||||
liste = dx.get_raw(f"/skrift-orders/files/{order_nr}") or {}
|
||||
gewuenscht = KIND.get(typ, "schriftstueck")
|
||||
passende = sorted(
|
||||
[f for f in (liste.get("files") or []) if f.get("kind") == gewuenscht],
|
||||
key=lambda f: f.get("name", ""),
|
||||
)
|
||||
# Nur bestimmte Briefnummern drucken, falls im Job angegeben.
|
||||
# Positionsbasiert (1 = erstes Dokument), NICHT über die Zahl im
|
||||
# Dateinamen: Alt-Aufträge sind 0-basiert (letter_000.svg), neue
|
||||
# 1-basiert (letter_001.svg). Die Position stimmt in beiden Fällen.
|
||||
nummern = parse_nummern(job.get("numbers"))
|
||||
if nummern is not None:
|
||||
passende = [f for i, f in enumerate(passende, start=1) if i in nummern]
|
||||
if not passende:
|
||||
raise RuntimeError(f"Keine Dateien vom Typ '{gewuenscht}' für {order_nr} gefunden.")
|
||||
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "printing", "error": None})
|
||||
# Produktionsstatus fürs Kanban: sobald gedruckt wird → „im_druck".
|
||||
if order_id:
|
||||
try: dx.patch(f"/items/orders/{order_id}", {"production_status": "im_druck"})
|
||||
except Exception: pass
|
||||
try:
|
||||
if not order_nr:
|
||||
raise RuntimeError("Auftragsnummer fehlt am Job.")
|
||||
liste = dx.get_raw(f"/skrift-orders/files/{order_nr}") or {}
|
||||
gewuenscht = KIND.get(typ, "schriftstueck")
|
||||
passende = sorted(
|
||||
[f for f in (liste.get("files") or []) if f.get("kind") == gewuenscht],
|
||||
key=lambda f: f.get("name", ""),
|
||||
)
|
||||
# Nur bestimmte Briefnummern drucken, falls im Job angegeben.
|
||||
# Positionsbasiert (1 = erstes Dokument), NICHT über die Zahl im
|
||||
# Dateinamen: Alt-Aufträge sind 0-basiert (letter_000.svg), neue
|
||||
# 1-basiert (letter_001.svg). Die Position stimmt in beiden Fällen.
|
||||
if nummern is not None:
|
||||
passende = [f for i, f in enumerate(passende, start=1) if i in nummern]
|
||||
if not passende:
|
||||
raise RuntimeError(f"Keine Dateien vom Typ '{gewuenscht}' für {order_nr} gefunden.")
|
||||
# Mehrseitige Briefe: Seiten je Brief absteigend an den Plotter (Stapel-
|
||||
# Reihenfolge, damit Seite 1 am Ende oben liegt). Einseitige unverändert.
|
||||
passende = seiten_reihenfolge(passende)
|
||||
dateien = []
|
||||
for f in passende:
|
||||
inhalt = dx.download(f"/skrift-orders/files/{order_nr}/download", {"rel": f["rel"]})
|
||||
dateien.append((f["name"], inhalt))
|
||||
return dateien
|
||||
|
||||
# Mehrseitige Briefe: Seiten je Brief absteigend an den Plotter (Stapel-
|
||||
# Reihenfolge, damit Seite 1 am Ende oben liegt). Einseitige unverändert.
|
||||
passende = seiten_reihenfolge(passende)
|
||||
|
||||
dateien = []
|
||||
for f in passende:
|
||||
inhalt = dx.download(f"/skrift-orders/files/{order_nr}/download", {"rel": f["rel"]})
|
||||
dateien.append((f["name"], inhalt))
|
||||
def _sende_batch(dx, plotter, jobs, KIND):
|
||||
"""Sendet mehrere gleichartige Jobs als EINEN Druckdurchlauf an die Maschine.
|
||||
|
||||
plotter.login()
|
||||
plotter.write(dateien, tpl)
|
||||
print(f"[Agent] Job {job_id} an Maschine übergeben ({len(dateien)} Dateien) – warte auf Fertigstellung.")
|
||||
return {"job_id": job_id, "order_id": order_id, "order_nr": order_nr,
|
||||
"typ": typ, "gestartet": False, "ts": time.monotonic()}
|
||||
except Exception as e:
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "failed", "error": str(e)[:1000]})
|
||||
print(f"[Agent] Job {job_id} fehlgeschlagen: {e}")
|
||||
Alle Jobs teilen Typ und Template (siehe _batch_gleicher_typ). Ihre Dokumente
|
||||
werden nacheinander in EINEM Lauf gedruckt (clear nur am Anfang, start am Ende) –
|
||||
so hält die Maschine zwischen den Aufträgen nicht an. Ein einzelner Job, dessen
|
||||
Dateien fehlen/fehlerhaft sind, wird als „failed" markiert und übersprungen; der
|
||||
Rest wird trotzdem gedruckt. Gibt den Batch-Kontext zurück (oder None, wenn nichts
|
||||
Druckbares übrig bleibt). Es wird NICHT auf das Druckende gewartet – das übernimmt
|
||||
die Hauptschleife (nächster Batch erst bei „Idle").
|
||||
"""
|
||||
contexts = []
|
||||
dateien = []
|
||||
tpl_final = None
|
||||
for job in jobs:
|
||||
job_id = job["id"]
|
||||
order = job.get("order") or {}
|
||||
order_id = order.get("id")
|
||||
order_nr = job.get("output_ref") or order.get("order_number")
|
||||
typ = job.get("type") or "brief"
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "printing", "error": None})
|
||||
# Produktionsstatus fürs Kanban: sobald gedruckt wird → „im_druck".
|
||||
if order_id:
|
||||
try: dx.patch(f"/items/orders/{order_id}", {"production_status": "im_druck"})
|
||||
except Exception: pass
|
||||
try:
|
||||
job_dateien = _job_dateien(dx, job, KIND)
|
||||
except Exception as e:
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "failed", "error": str(e)[:1000]})
|
||||
print(f"[Agent] Job {job_id} fehlgeschlagen: {e}")
|
||||
continue
|
||||
if tpl_final is None:
|
||||
tpl_final = _effektives_template(job)
|
||||
dateien.extend(job_dateien)
|
||||
contexts.append({"job_id": job_id, "order_id": order_id,
|
||||
"order_nr": order_nr, "typ": typ})
|
||||
print(f"[Agent] Job {job_id}: {typ} für {order_nr} ({len(job_dateien)} Dateien) → Batch")
|
||||
|
||||
if not dateien:
|
||||
return None
|
||||
try:
|
||||
plotter.login()
|
||||
plotter.write(dateien, tpl_final)
|
||||
except Exception as e:
|
||||
# Ganzer Durchlauf fehlgeschlagen → alle beteiligten Jobs auf „failed".
|
||||
for ctx in contexts:
|
||||
try: dx.patch(f"/items/jobs/{ctx['job_id']}", {"status": "failed", "error": str(e)[:1000]})
|
||||
except Exception: pass
|
||||
print(f"[Agent] Batch-Druck fehlgeschlagen: {e}")
|
||||
return None
|
||||
|
||||
print(f"[Agent] Batch an Maschine übergeben: {len(contexts)} Job(s), "
|
||||
f"{len(dateien)} Dateien – warte auf Fertigstellung.")
|
||||
return {"contexts": contexts, "gestartet": False, "ts": time.monotonic()}
|
||||
|
||||
|
||||
def _abschluss(dx, ctx):
|
||||
|
||||
26
neustart_agent.bat
Normal file
26
neustart_agent.bat
Normal file
@@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
rem ===================================================================
|
||||
rem Skrift-Agent neu starten (zum Doppelklicken).
|
||||
rem 1) laufende agent.py-Instanz(en) beenden
|
||||
rem 2) fensterlos frisch starten (ueber run_agent.vbs)
|
||||
rem ===================================================================
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo Beende laufende Agent-Instanz(en)...
|
||||
powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -like '*agent.py*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
|
||||
|
||||
rem kurz warten, damit der Prozess wirklich weg ist (ping statt timeout: braucht kein stdin)
|
||||
"%SystemRoot%\System32\ping.exe" -n 2 127.0.0.1 >nul
|
||||
|
||||
echo ##### NEUSTART #####>> "logs\agent.log"
|
||||
|
||||
echo Starte Agent neu...
|
||||
wscript.exe "%~dp0run_agent.vbs"
|
||||
|
||||
rem kurz warten und pruefen, ob wieder eine Instanz laeuft
|
||||
"%SystemRoot%\System32\ping.exe" -n 5 127.0.0.1 >nul
|
||||
powershell -NoProfile -Command "$n = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -like '*agent.py*' }).Count; if ($n -gt 0) { Write-Host 'OK: Agent laeuft wieder.' -ForegroundColor Green } else { Write-Host 'WARNUNG: Keine Instanz gefunden - Log pruefen.' -ForegroundColor Red }"
|
||||
|
||||
echo.
|
||||
echo Fenster schliesst sich in 3 Sekunden...
|
||||
"%SystemRoot%\System32\ping.exe" -n 4 127.0.0.1 >nul
|
||||
Reference in New Issue
Block a user