SCL aus parametrisierten Templates: Golden-Files, Render-Kontext, Pilot 1Sep1Swi
Schritte 1-3 aus doc/SCL_Templates.md (neu: Konzept, Wertklassen je Zeile, Namensregeln, Tests, Migrationsplan, Vertrag fuer den CSV->SCL-Workflow). - Golden-Files (tests/test_scl_skeleton_golden.py, tests/fixtures/scl/): komplette FB_Main-Ausgabe von mubea und HundM05 byte-genau festgeschrieben, Zeitstempel maskiert; gewollte Aenderung per SKEL_UPDATE_GOLDEN=1 - lib/scl_context.py (neu): alles, was scl_skeleton aus der TRO-JSON ableitet (Reihenfolge, Separator-/Weichen-/Scanner-Bloecke, Staubereiche, Ziellisten, Namen) als pydantic-Kontext SclFile/SclTro; Namen als SclName (Wert + todo-Merker) als Andockstelle fuer die Namensregeln (Schritt 4). scl_skeleton schreibt nur noch Text; Ausgabe byte-identisch (Golden-Files, zusaetzlich dematic/showroom mit und ohne --start alt/neu verglichen) - lib/scl_templates/ (neu, Jinja2): _macros.scl.j2 + Pilot FB_ILS_MTRO_1Sep1Swi.scl.j2 nach der realen Aufrufstelle TRO107 (PriorityManagerDir2, stInSenJam<n>, xInCarouselRun1, xInRelease TRUE, Separatorzeiten 100/50 ms). Ortsgebundene Zeilen als Vorschlag umgesetzt, jede offene Stelle mit TODO(E-Planung) - Bestaetigung durch SPS steht aus - scl_skeleton: --templates [DIR] (opt-in, ohne Schalter Ausgabe unveraendert), --strict (Exit 1 ohne SCL, wenn ein FB-Typ kein Template hat); Kopf und Zusammenfassung nennen Template-/generische Typen; Templatefehler (StrictUndefined) -> Exit 1 ohne SCL - tests/test_scl_templates.py: Rundlauf Template mit Referenzwerten == Referenz-REGION (normalisiert); jede Platzhalterzeile traegt den TODO-Marker - requirements.txt: jinja2 >= 3.1 (bestehende .venv: pip install -r requirements.txt) - Doku: SCL_Templates.md, Python_Scripts.md §5b, Commandlines.md §2.5, CLAUDE.md Befunde: xInCarouselRun1 laesst sich nicht aus der TRO-JSON ableiten (Kanten- art "umlauf" kommt praktisch nicht vor) -> immer als TODO; nTo1Destinations ist in der Referenz 3 bei 2 Abgaengen (zaehlt evtl. Ziele) -> "pruefen". Mit --templates nur die 1Sep1Swi-REGIONs (+ Kopf) anders als generisch; 32 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
97a0db6707
commit
4895ebf5ed
+242
-211
@@ -40,17 +40,60 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import (
|
||||
Environment,
|
||||
FileSystemLoader,
|
||||
StrictUndefined,
|
||||
TemplateError,
|
||||
TemplateNotFound,
|
||||
)
|
||||
from pydantic import ValidationError
|
||||
|
||||
from material_flow import env_dir
|
||||
from model import SCHEMA, TroBase, TroDocument
|
||||
from tro_catalog import TroItem
|
||||
from model import SCHEMA, TroDocument
|
||||
from scl_context import TODO, SclFile, SclName, SclTro, build_context
|
||||
|
||||
TODO = "TODO(E-Planung)"
|
||||
# Parametrisierte Aufrufstellen je FB (doc/SCL_Templates.md); Dateiname = fbType.
|
||||
TEMPLATE_DIR = Path(__file__).resolve().parent / "scl_templates"
|
||||
TEMPLATE_SUFFIX = ".scl.j2"
|
||||
|
||||
|
||||
class SclTemplates:
|
||||
"""Die FB-Templates eines Ordners; ohne Template -> generischer Aufbau."""
|
||||
|
||||
def __init__(self, folder: Path) -> None:
|
||||
self.folder = folder
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(str(folder)),
|
||||
# Tippfehler im Template -> Fehler statt leerer SCL-Stelle
|
||||
undefined=StrictUndefined,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=True,
|
||||
autoescape=False,
|
||||
)
|
||||
self.env.globals["TODO"] = TODO
|
||||
|
||||
def has(self, fb: str) -> bool:
|
||||
return (self.folder / f"{fb}{TEMPLATE_SUFFIX}").is_file()
|
||||
|
||||
def render(self, tro: SclTro) -> str:
|
||||
"""REGION eines TRO aus seinem Template (ohne abschliessenden Umbruch)."""
|
||||
try:
|
||||
template = self.env.get_template(f"{tro.fb}{TEMPLATE_SUFFIX}")
|
||||
except TemplateNotFound as exc:
|
||||
raise TemplateError(f"Template fuer {tro.fb} fehlt: {exc}") from exc
|
||||
# Leerzeilen hier zusammenfassen statt im Template mit Whitespace-
|
||||
# Steuerung ({%- ... -%}) - die macht Templates schwer lesbar und
|
||||
# bricht leicht die Einrueckung.
|
||||
zeilen: list[str] = []
|
||||
for zeile in template.render(t=tro).split("\n"):
|
||||
zeile = zeile.rstrip()
|
||||
if zeile or (zeilen and zeilen[-1]):
|
||||
zeilen.append(zeile)
|
||||
return "\n".join(zeilen).strip("\n")
|
||||
|
||||
# Zeitwerte aus den Referenz-Aufrufstellen. Sie sind plausible Startwerte, KEINE
|
||||
# fuer diese Anlage ermittelten - deshalb ueberall mit Pruefhinweis.
|
||||
@@ -67,140 +110,58 @@ def load_document(path: Path) -> TroDocument:
|
||||
return TroDocument.from_json(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def flow_order(tros: list[TroBase], start_id: str = "") -> tuple[list[TroBase], str]:
|
||||
"""TROs in Fliessrichtung sortieren.
|
||||
|
||||
Startpunkt ist ein TRO ohne Vorgaenger - also die Stelle, an der Material
|
||||
in die Anlage kommt. Solange Be-/Entladestation fehlen, ist der Fluss ein
|
||||
geschlossener Kreis und es gibt keinen solchen TRO; dann muss der Kreis
|
||||
irgendwo aufgetrennt werden. Das ist eine willkuerliche Wahl, deshalb wird
|
||||
sie zurueckgegeben und im Kopf der SCL vermerkt (und laesst sich mit
|
||||
--start ueberschreiben). Nicht erreichte TROs haengt die Funktion hinten
|
||||
an, damit nie einer verlorengeht.
|
||||
"""
|
||||
nach_id = {t.tro_id: t for t in tros}
|
||||
ohne_vorgaenger = sorted(t.tro_id for t in tros if not t.predecessors)
|
||||
# Materialeingang ueber eine Station (SYSn von tro_flow.py): der TRO, den
|
||||
# sie speist, ist der natuerliche Anfang - auch wenn er zusaetzlich einen
|
||||
# TRO-Vorgaenger hat (Kreisel-Umlauf).
|
||||
eingaenge = sorted({
|
||||
t.tro_id for t in tros
|
||||
for vor in t.predecessors if vor not in nach_id
|
||||
})
|
||||
|
||||
if start_id and start_id in nach_id:
|
||||
start, grund = [start_id], "per --start gesetzt"
|
||||
elif ohne_vorgaenger:
|
||||
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
|
||||
elif eingaenge:
|
||||
stationen = sorted({
|
||||
vor for i in eingaenge for vor in nach_id[i].predecessors
|
||||
if vor not in nach_id
|
||||
})
|
||||
start = eingaenge
|
||||
grund = f"Materialeingang ueber {', '.join(stationen)}"
|
||||
else:
|
||||
start = [min(nach_id)]
|
||||
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
|
||||
"Be-/Entladestation im Export stehen, ergibt sich der Anfang")
|
||||
|
||||
gesehen: list[str] = []
|
||||
queue = deque(start)
|
||||
while queue:
|
||||
tro_id = queue.popleft()
|
||||
if tro_id in gesehen:
|
||||
continue
|
||||
gesehen.append(tro_id)
|
||||
for nxt in nach_id[tro_id].successors if tro_id in nach_id else []:
|
||||
# SYSn (Materialausgang) ist kein TRO - keine REGION, keine Instanz.
|
||||
if nxt in nach_id and nxt not in gesehen:
|
||||
queue.append(nxt)
|
||||
for tro_id in sorted(nach_id):
|
||||
if tro_id not in gesehen:
|
||||
gesehen.append(tro_id)
|
||||
return [nach_id[i] for i in gesehen], grund
|
||||
def _todo(name: SclName, abstand: str) -> str:
|
||||
"""TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist."""
|
||||
return f"{abstand}// {TODO}" if name.todo else ""
|
||||
|
||||
|
||||
def reachable_via(start: str, first: str, tros: dict[str, TroBase]) -> list[str]:
|
||||
"""Welche TROs sind ueber diesen Abgang erreichbar?
|
||||
|
||||
Das ist die Zielliste eines Weichenabgangs: von `first` aus weiterlaufen,
|
||||
ohne ueber die Weiche selbst zurueckzugehen. Genau die Information, die
|
||||
FC_Direction braucht, um zu entscheiden - und sie steht im Graphen.
|
||||
"""
|
||||
gesehen: set[str] = set()
|
||||
queue = deque([first])
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node in gesehen or node == start:
|
||||
continue
|
||||
gesehen.add(node)
|
||||
if node in tros:
|
||||
queue.extend(tros[node].successors)
|
||||
return sorted(gesehen)
|
||||
|
||||
|
||||
def nachbar(node: str, alle: dict[str, TroBase]) -> str:
|
||||
"""Nachbar fuer Kommentare; SYSn als Station kenntlich machen."""
|
||||
return node if node in alle else f"{node} (Station, kein TRO)"
|
||||
|
||||
|
||||
def region_title(tro: TroBase) -> str:
|
||||
seps = ", ".join(tro.separators)
|
||||
teile = f" Sep {seps}" if seps else ""
|
||||
return f"{tro.tro_id} ({tro.type_name}){teile}"
|
||||
|
||||
|
||||
def emit_separators(add, tro: TroBase, anzahl: int, hat_scanner: bool) -> None:
|
||||
seps = tro.separators
|
||||
for i in range(1, anzahl + 1):
|
||||
pfad = f"#{tro.tro_id}.stInSeparator{i}"
|
||||
layout = seps[i - 1] if i <= len(seps) else "?"
|
||||
add(f" // Separator {i} = Bauteil {layout} aus dem Layout")
|
||||
add(f' {pfad}.Settings.nSeparatorNo := "cSep_____"; // {TODO}')
|
||||
def emit_separators(add, tro: SclTro) -> None:
|
||||
for s in tro.separators:
|
||||
pfad = f"#{tro.id}.stInSeparator{s.n}"
|
||||
add(f" // Separator {s.n} = Bauteil {s.layout} aus dem Layout")
|
||||
add(f' {pfad}.Settings.nSeparatorNo := "{s.no.value}";{_todo(s.no, " ")}')
|
||||
for name, wert, hinweis in DEFAULT_SEP_TIMES:
|
||||
kommentar = f" // {TODO}: {hinweis}" if hinweis else ""
|
||||
add(f" {pfad}.Settings.{name} := {wert};{kommentar}")
|
||||
add(f" {pfad}.Settings.xPluggedJam := TRUE;")
|
||||
add(f" {pfad}.Settings.xPluggedPart := FALSE;")
|
||||
add(f" {pfad}.Settings.xPluggedScanner := "
|
||||
f"{'TRUE' if hat_scanner else 'FALSE'};")
|
||||
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
f"{'TRUE' if tro.has_scanner else 'FALSE'};")
|
||||
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["{s.sensor.value}"];'
|
||||
f'{_todo(s.sensor, " ")}')
|
||||
add(f" {pfad}.Settings.tSenFree := T#1500ms; // {TODO}: pruefen")
|
||||
add(f" {pfad}.Settings.tSenWait := T#4000ms; // {TODO}: pruefen")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_switches(add, tro: TroBase, anzahl: int, alle: dict[str, TroBase]) -> None:
|
||||
abgaenge = tro.successors
|
||||
for j in range(1, anzahl + 1):
|
||||
pfad = f"#{tro.tro_id}.stInSwitch{j}"
|
||||
add(f' {pfad}.Settings.nSwitchNo := "cSwi_____"; // {TODO}')
|
||||
def emit_switches(add, tro: SclTro) -> None:
|
||||
for w in tro.switches:
|
||||
pfad = f"#{tro.id}.stInSwitch{w.n}"
|
||||
add(f' {pfad}.Settings.nSwitchNo := "{w.no.value}";{_todo(w.no, " ")}')
|
||||
add(f" {pfad}.Settings.xPluggedExSen3 := FALSE;")
|
||||
add(f" {pfad}.Settings.xPluggedExSen4 := FALSE;")
|
||||
if anzahl == 1:
|
||||
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
|
||||
add(f" {pfad}.Settings.nTo1Destinations := {len(abgaenge)};"
|
||||
if w.to1_destinations is not None:
|
||||
add(f" {pfad}.Settings.nTo1Destinations := {w.to1_destinations};"
|
||||
f" // Abgaenge laut Topologie")
|
||||
else:
|
||||
# Bei mehreren Weichen ist es eine Planungsentscheidung, welche
|
||||
# Weiche welche Abgaenge bedient - die Summe ist bekannt, die
|
||||
# Aufteilung nicht. Deshalb hier kein geratener Wert.
|
||||
add(f" {pfad}.Settings.nTo1Destinations := _;"
|
||||
f" // {TODO}: dieser TRO hat {len(abgaenge)} "
|
||||
f"Abgaenge auf {anzahl} Weichen - Aufteilung festlegen")
|
||||
f" // {TODO}: dieser TRO hat {len(tro.successors)} "
|
||||
f"Abgaenge auf {len(tro.switches)} Weichen - Aufteilung festlegen")
|
||||
add("")
|
||||
if abgaenge:
|
||||
if tro.switch_exits:
|
||||
add(" // Zielliste je Abgang (aus dem TRO-Graphen berechnet) -")
|
||||
add(" // Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:")
|
||||
for abgang in abgaenge:
|
||||
ziele = reachable_via(tro.tro_id, abgang, alle)
|
||||
add(f" // ueber {abgang}: {', '.join(ziele) if ziele else '-'}")
|
||||
for abgang in tro.switch_exits:
|
||||
ziele = ", ".join(abgang.targets) if abgang.targets else "-"
|
||||
add(f" // ueber {abgang.via}: {ziele}")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_vario(add, tro: TroBase) -> None:
|
||||
pfad = f"#{tro.tro_id}.stInVario"
|
||||
def emit_vario(add, tro: SclTro) -> None:
|
||||
pfad = f"#{tro.id}.stInVario"
|
||||
add(f" {pfad}.Settings.xContinousEmptying := FALSE;")
|
||||
add(f" {pfad}.Settings.tWaitForJog := T#300ms; // {TODO}: pruefen")
|
||||
add(f" {pfad}.Settings.tConvEmpty := T#45000ms; // {TODO}: pruefen")
|
||||
@@ -210,17 +171,18 @@ def emit_vario(add, tro: TroBase) -> None:
|
||||
add(f" {pfad}.Settings.tCarrierInPosEdgeDelay := t#100ms;")
|
||||
add(f" {pfad}.Settings.xJamSenDelayActive := TRUE;")
|
||||
add(f" {pfad}.Settings.tJamSenDelay := T#2s;")
|
||||
add(f' {pfad}.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
# Der Kommentar steht bei allen Sensoren in derselben Spalte
|
||||
# (Parametername + Abstand = 16 Zeichen).
|
||||
for name, sensor in tro.vario_sensors.items():
|
||||
abstand = " " * (16 - len(name))
|
||||
add(f' {pfad}.{name} := "DB_Inputs".Sensors["{sensor.value}"];'
|
||||
f"{_todo(sensor, abstand)}")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_barcode(add, tro: TroBase, anzahl: int) -> None:
|
||||
for i in range(1, anzahl + 1):
|
||||
suffix = "" if anzahl == 1 else str(i)
|
||||
pfad = f"#{tro.tro_id}.stInBarcodeReader{suffix}"
|
||||
def emit_barcode(add, tro: SclTro) -> None:
|
||||
for scanner in tro.scanners:
|
||||
pfad = f"#{tro.id}.stInBarcodeReader{scanner.suffix}"
|
||||
add(f" {pfad}.Settings.xActivateCheckASCI := TRUE;")
|
||||
add(f" {pfad}.Settings.tTimeout := T#3000ms;")
|
||||
add(f' // {TODO}: Cognex-Interface verdrahten '
|
||||
@@ -228,49 +190,55 @@ def emit_barcode(add, tro: TroBase, anzahl: int) -> None:
|
||||
add("")
|
||||
|
||||
|
||||
def emit_call(add, tro: TroBase, items: dict[str, int], alle: dict[str, TroBase]) -> None:
|
||||
def emit_call(add, tro: SclTro) -> None:
|
||||
"""Aufrufstelle schreiben.
|
||||
|
||||
Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim
|
||||
Zusammensetzen gesetzt - sonst landet es hinter dem "//" und ist Teil des
|
||||
Kommentars, womit die Parameterliste nicht mehr uebersetzt.
|
||||
"""
|
||||
tro_id = tro.tro_id
|
||||
def offen(name: SclName, text: str = "") -> str:
|
||||
if not name.todo:
|
||||
return text
|
||||
return f"{TODO} - {text}" if text else TODO
|
||||
|
||||
zeilen: list[tuple[str, str]] = [
|
||||
('nInMainTroNo := "cMainTro____"', TODO),
|
||||
('xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q', TODO),
|
||||
(f'nInMainTroNo := "{tro.main_tro.value}"', offen(tro.main_tro)),
|
||||
(f"xInSftyOk := {tro.safety.value}", offen(tro.safety)),
|
||||
("xInAllRdyToStart := TRUE", ""),
|
||||
(f'xInRelease := "DB_TRO_TEST".{tro_id}', ""),
|
||||
(f"xInRelease := {tro.release}", ""),
|
||||
]
|
||||
if items.get(TroItem.VARIO_DRIVE):
|
||||
zeilen.append(('xInMotorProtection := "FC____"', TODO))
|
||||
if tro.has_vario:
|
||||
zeilen.append((f"xInMotorProtection := {tro.motor_protection.value}",
|
||||
offen(tro.motor_protection)))
|
||||
zeilen.append(("xInAutomatic := #t_automatik", ""))
|
||||
zeilen.append(("stInOutMachineState := #stInOutControlUnitCabinet", ""))
|
||||
# Der generische Aufbau hat den Schaltschrank nie als offen markiert;
|
||||
# das bleibt so, damit seine Ausgabe stabil ist (Golden-Files). Die
|
||||
# Templates markieren ihn.
|
||||
zeilen.append((f"stInOutMachineState := {tro.machine_state.value}", ""))
|
||||
zeilen.append(("stInOutCognexInterface := #stCognexInterfaceDummy", ""))
|
||||
|
||||
# Staubereiche: je ankommender/abgehender Verbindung einer. Welche
|
||||
# DB-Struktur dahintersteht, ist eine Planungsentscheidung - hier steht
|
||||
# nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren.
|
||||
for n, vor in enumerate(tro.predecessors, start=1):
|
||||
zeilen.append((f'arInOutJamEntr{n} := "DB_JamArea____".stJam____.arCarrier',
|
||||
f"{TODO} - Staubereich von {nachbar(vor, alle)}"))
|
||||
zeilen.append((f'stInOutJamEntr{n} := "DB_JamArea____".stJam____.stData', ""))
|
||||
for n, nach in enumerate(tro.successors, start=2):
|
||||
zeilen.append((f'arInOutJamExit{n} := "DB_JamArea____".stJam____.arCarrier',
|
||||
f"{TODO} - Staubereich nach {nachbar(nach, alle)}"))
|
||||
zeilen.append((f'stInOutJamExit{n} := "DB_JamArea____".stJam____.stData', ""))
|
||||
for jam in tro.jam_entries:
|
||||
zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"Staubereich von {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamEntr{jam.n} := {jam.db.value}.stData", ""))
|
||||
for jam in tro.jam_exits:
|
||||
zeilen.append((f"arInOutJamExit{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"Staubereich nach {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", ""))
|
||||
|
||||
for i in range(1, items.get(TroItem.SEPARATOR, 1) + 1):
|
||||
zeilen.append((f'xOutStopper{i} => "MB____"', TODO))
|
||||
if items.get(TroItem.VARIO_DRIVE):
|
||||
zeilen.append(('xOutVarioMotor => "MA____"', TODO))
|
||||
for j in range(1, items.get(TroItem.SWITCH, 0) + 1):
|
||||
zeilen.append((f'xOutSw{j}ExTo3 => "MB____"', TODO))
|
||||
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro_id}', ""))
|
||||
zeilen.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]',
|
||||
TODO))
|
||||
for s in tro.separators:
|
||||
zeilen.append((f'xOutStopper{s.n} => "{s.stopper.value}"', offen(s.stopper)))
|
||||
if tro.has_vario:
|
||||
zeilen.append((f'xOutVarioMotor => "{tro.vario_motor.value}"',
|
||||
offen(tro.vario_motor)))
|
||||
for w in tro.switches:
|
||||
zeilen.append((f'xOutSw{w.n}ExTo3 => "{w.valve.value}"', offen(w.valve)))
|
||||
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro.id}', ""))
|
||||
zeilen.append((f'nOutStateLast => "DB_GLOB_TroState".arTroState["{tro.main_tro.value}"]',
|
||||
offen(tro.main_tro)))
|
||||
|
||||
kopf = f" #{tro_id}("
|
||||
kopf = f" #{tro.id}("
|
||||
einzug = " " * len(kopf)
|
||||
for index, (code, kommentar) in enumerate(zeilen):
|
||||
trenner = ");" if index == len(zeilen) - 1 else ","
|
||||
@@ -279,34 +247,75 @@ def emit_call(add, tro: TroBase, items: dict[str, int], alle: dict[str, TroBase]
|
||||
add(f"{text} // {kommentar}" if kommentar else text)
|
||||
|
||||
|
||||
def emit_direction(add, tro: TroBase, anzahl: int) -> None:
|
||||
def emit_direction(add, tro: SclTro) -> None:
|
||||
"""Weichenlogik - je Weiche ein FC_Direction-Aufruf."""
|
||||
add("")
|
||||
add(" REGION Direction")
|
||||
for j in range(1, anzahl + 1):
|
||||
ziel = (f"#{tro.tro_id}.nInDirection" if anzahl == 1
|
||||
else f"#{tro.tro_id}.nInDirection{j}")
|
||||
add(f' "FC_Direction"(nInSwitchNo := "cSwi_____", // {TODO}')
|
||||
add(f" nOutDirection => {ziel},")
|
||||
add(f' InOutCarrier := "DB_JamArea____".stJam____'
|
||||
f".arCarrier[1]);")
|
||||
if j < anzahl:
|
||||
for w in tro.switches:
|
||||
add(f' "FC_Direction"(nInSwitchNo := "{w.no.value}",{_todo(w.no, " ")}')
|
||||
add(f" nOutDirection => {w.direction_target},")
|
||||
add(f" InOutCarrier := {w.jam_db.value}.arCarrier[1]);")
|
||||
if w.n < len(tro.switches):
|
||||
add("")
|
||||
add(" END_REGION")
|
||||
|
||||
|
||||
def render(document: TroDocument, quelle: str, start_id: str = "") -> str:
|
||||
tros = document.tros
|
||||
alle = {t.tro_id: t for t in tros}
|
||||
geordnet, startgrund = flow_order(tros, start_id)
|
||||
def emit_region(add, tro: SclTro) -> None:
|
||||
"""Eine REGION je TRO (heutiger, generischer Aufbau aus den Bauteilen)."""
|
||||
add("")
|
||||
add(f"REGION {tro.region_title}")
|
||||
if tro.position:
|
||||
add(f" // Anlagenkoordinate x/y: {tro.position[0]:.0f} / {tro.position[1]:.0f} mm")
|
||||
add(f" // Vorgaenger: {', '.join(tro.predecessors) or '-'}")
|
||||
add(f" // Nachfolger: {', '.join(tro.successors) or '-'}")
|
||||
if tro.confidence:
|
||||
add(f" // Einstufung: {tro.confidence}")
|
||||
add("")
|
||||
|
||||
emit_separators(add, tro)
|
||||
if tro.switches:
|
||||
emit_switches(add, tro)
|
||||
if tro.has_vario:
|
||||
emit_vario(add, tro)
|
||||
if tro.scanners:
|
||||
emit_barcode(add, tro)
|
||||
if tro.storage_lines:
|
||||
add(f" // {TODO}: Linienspeicher mit {tro.storage_lines} Linien - "
|
||||
f"Parametrierung je Linie")
|
||||
add("")
|
||||
|
||||
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro.id)
|
||||
add("")
|
||||
emit_call(add, tro)
|
||||
if tro.switches:
|
||||
emit_direction(add, tro)
|
||||
add("END_REGION")
|
||||
|
||||
|
||||
def template_usage(context: SclFile, templates: SclTemplates | None
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""FB-Typen mit bzw. ohne Template (je sortiert, ohne Doppelte)."""
|
||||
fbs = sorted({t.fb for t in context.tros})
|
||||
if templates is None:
|
||||
return [], fbs
|
||||
return ([fb for fb in fbs if templates.has(fb)],
|
||||
[fb for fb in fbs if not templates.has(fb)])
|
||||
|
||||
|
||||
def render_context(context: SclFile, templates: SclTemplates | None = None) -> str:
|
||||
"""SCL-Text aus dem fertigen Kontext.
|
||||
|
||||
Mit ``templates`` wird jeder TRO, fuer dessen FB ein Template existiert, aus
|
||||
dem Template erzeugt, alle anderen generisch. Ohne ``templates`` ist die
|
||||
Ausgabe die bisherige (Golden-Files in tests/fixtures/scl/).
|
||||
"""
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
|
||||
add("// ============================================================")
|
||||
add("// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py")
|
||||
add(f"// Quelle : {quelle}")
|
||||
add(f"// Erzeugt: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
add(f"// Quelle : {context.source}")
|
||||
add(f"// Erzeugt: {context.created}")
|
||||
add("//")
|
||||
add("// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum")
|
||||
add("// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem")
|
||||
@@ -319,57 +328,33 @@ def render(document: TroDocument, quelle: str, start_id: str = "") -> str:
|
||||
add("// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.")
|
||||
add("//")
|
||||
add("// Reihenfolge der REGIONs = Materialfluss.")
|
||||
add(f"// Anfang bei {geordnet[0].tro_id}: {startgrund}.")
|
||||
add(f"// Anfang bei {context.tros[0].id}: {context.start_reason}.")
|
||||
if templates is not None:
|
||||
mit, ohne = template_usage(context, templates)
|
||||
add("//")
|
||||
add(f"// Aus Template : {', '.join(mit) or '-'}")
|
||||
add(f"// Generisch : {', '.join(ohne) or '-'} (kein Template - von Hand pruefen)")
|
||||
add("// ============================================================")
|
||||
add("")
|
||||
add('FUNCTION_BLOCK "FB_Main"')
|
||||
add("{ S7_Optimized_Access := 'TRUE' }")
|
||||
add("VERSION : 0.1")
|
||||
add(" VAR ")
|
||||
for tro in geordnet:
|
||||
fb = tro.fb_block or "FB_UNBEKANNT"
|
||||
kommentar = f" // {tro.type_name}"
|
||||
if tro.separators:
|
||||
kommentar += f", Sep {', '.join(tro.separators)}"
|
||||
add(f' {tro.tro_id} : "{fb}";{kommentar}')
|
||||
for tro in context.tros:
|
||||
kommentar = f" // {tro.type}"
|
||||
if tro.layout_separators:
|
||||
kommentar += f", Sep {', '.join(tro.layout_separators)}"
|
||||
add(f' {tro.id} : "{tro.fb}";{kommentar}')
|
||||
add(" END_VAR")
|
||||
add("")
|
||||
add("BEGIN")
|
||||
|
||||
for tro in geordnet:
|
||||
items = tro.items
|
||||
n_sep = items.get(TroItem.SEPARATOR, 1)
|
||||
n_swi = items.get(TroItem.SWITCH, 0)
|
||||
n_scan = items.get(TroItem.SCANNER, 0)
|
||||
|
||||
add("")
|
||||
add(f"REGION {region_title(tro)}")
|
||||
if tro.position:
|
||||
add(f" // Anlagenkoordinate x/y: {tro.x:.0f} / {tro.y:.0f} mm")
|
||||
add(f" // Vorgaenger: {', '.join(tro.predecessors) or '-'}")
|
||||
add(f" // Nachfolger: {', '.join(tro.successors) or '-'}")
|
||||
if tro.confidence:
|
||||
add(f" // Einstufung: {tro.confidence}")
|
||||
add("")
|
||||
|
||||
emit_separators(add, tro, n_sep, hat_scanner=bool(n_scan))
|
||||
if n_swi:
|
||||
emit_switches(add, tro, n_swi, alle)
|
||||
if items.get(TroItem.VARIO_DRIVE):
|
||||
emit_vario(add, tro)
|
||||
if n_scan:
|
||||
emit_barcode(add, tro, n_scan)
|
||||
if items.get(TroItem.STORAGE_LINE):
|
||||
add(f" // {TODO}: Linienspeicher mit {items[TroItem.STORAGE_LINE]} Linien - "
|
||||
f"Parametrierung je Linie")
|
||||
for tro in context.tros:
|
||||
if templates is not None and templates.has(tro.fb):
|
||||
add("")
|
||||
|
||||
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro.tro_id)
|
||||
add("")
|
||||
emit_call(add, tro, items, alle)
|
||||
if n_swi:
|
||||
emit_direction(add, tro, n_swi)
|
||||
add("END_REGION")
|
||||
out.extend(templates.render(tro).split("\n"))
|
||||
else:
|
||||
emit_region(add, tro)
|
||||
|
||||
add("")
|
||||
add("END_FUNCTION_BLOCK")
|
||||
@@ -377,6 +362,12 @@ def render(document: TroDocument, quelle: str, start_id: str = "") -> str:
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def render(document: TroDocument, quelle: str, start_id: str = "",
|
||||
templates: SclTemplates | None = None) -> str:
|
||||
"""TRO-JSON -> SCL-Text (Kontext bauen, dann ausgeben)."""
|
||||
return render_context(build_context(document, quelle, start_id), templates)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
@@ -413,20 +404,42 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = render(document, json_file.name, args.start)
|
||||
templates = None
|
||||
if args.templates is not None:
|
||||
ordner = Path(args.templates)
|
||||
if not ordner.is_dir():
|
||||
print(f"FEHLER: Template-Ordner nicht gefunden: {ordner}", file=sys.stderr)
|
||||
return 1
|
||||
templates = SclTemplates(ordner)
|
||||
|
||||
context = build_context(document, json_file.name, args.start)
|
||||
mit, ohne = template_usage(context, templates)
|
||||
if args.strict and ohne:
|
||||
print(f"FEHLER: kein Template fuer {', '.join(ohne)} (--strict) - "
|
||||
f"keine SCL geschrieben.", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
text = render_context(context, templates)
|
||||
except TemplateError as exc:
|
||||
# StrictUndefined, Syntaxfehler, fehlendes Makro ... - lieber keine SCL
|
||||
# als eine mit stillen Luecken.
|
||||
print(f"FEHLER im Template: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
out_file = results / (args.out or f"{json_file.stem.replace('_tro', '')}_FB_Main.scl")
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
offen = text.count(TODO)
|
||||
geordnet, startgrund = flow_order(document.tros, args.start)
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("FB_MAIN-GERUEST")
|
||||
print("================================================================")
|
||||
print(f"Quelle = {json_file}")
|
||||
print(f"TROs = {len(document.tros)}")
|
||||
print(f"Reihenfolge = {' -> '.join(t.tro_id for t in geordnet)}")
|
||||
print(f"Anfang = {geordnet[0].tro_id} ({startgrund})")
|
||||
print(f"TROs = {len(context.tros)}")
|
||||
print(f"Reihenfolge = {' -> '.join(t.id for t in context.tros)}")
|
||||
print(f"Anfang = {context.tros[0].id} ({context.start_reason})")
|
||||
if templates is not None:
|
||||
print(f"Templates = {len(mit)} von {len(mit) + len(ohne)} Typen "
|
||||
f"(generisch: {', '.join(ohne) or '-'})")
|
||||
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
|
||||
print(f"SCL = {out_file}")
|
||||
print("================================================================")
|
||||
@@ -451,10 +464,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
epilog="Beispiele:\n"
|
||||
" scl_skeleton.bat\n"
|
||||
" scl_skeleton.bat --json export_tro.json --out FB_Main.scl\n"
|
||||
" scl_skeleton.bat --json mubea_tro.json --templates\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
" 1 Eingabe- oder Aufruffehler\n",
|
||||
" 0 ok (auch wenn TROs ohne Template generisch erzeugt wurden)\n"
|
||||
" 1 Eingabe-, Aufruf- oder Templatefehler; mit --strict auch\n"
|
||||
" ein FB-Typ ohne Template\n",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
@@ -475,6 +490,22 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
help="TRO, bei dem die Reihenfolge beginnt. Nur noetig, solange der "
|
||||
"Fluss ein geschlossener Kreis ohne Be-/Entladestation ist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--templates",
|
||||
nargs="?",
|
||||
const=str(TEMPLATE_DIR),
|
||||
default=None,
|
||||
metavar="DIR",
|
||||
help="REGIONs aus den FB-Templates erzeugen (doc/SCL_Templates.md). "
|
||||
"Ohne DIR: lib/scl_templates. FB-Typen ohne Template bleiben "
|
||||
"generisch. Ohne diesen Schalter: bisherige Ausgabe.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Nur mit --templates: abbrechen (Exit 1), wenn ein FB-Typ der "
|
||||
"Anlage kein Template hat.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user