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>
514 lines
21 KiB
Python
514 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
|
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
|
"""
|
|
scl_skeleton.py - FB_Main-Geruest als SCL aus der TRO-Liste erzeugen.
|
|
|
|
Letzter Schritt der Kette:
|
|
|
|
material_flow.py Objekte -> Materialfluss
|
|
tro_flow.py Fluss -> TROs -> <name>_tro.json (--json)
|
|
scl_skeleton.py JSON -> FB_Main.scl <- hier
|
|
|
|
Die TRO-JSON wird ueber das gemeinsame Modell lib/model.py (TroDocument)
|
|
eingelesen und dabei geprueft: ein falscher oder umbenannter Schluessel bricht
|
|
mit einer Fehlermeldung ab, statt still einen Standardwert zu nehmen. Bauteile
|
|
(items) und FB-Typ kommen aus tro_catalog, ueber den TRO-Typ.
|
|
|
|
Erzeugt wird ein **Geruest**, kein fertiges Programm: je TRO eine REGION mit
|
|
Instanzaufruf und allen Parameterzeilen in der richtigen Reihenfolge, so wie es
|
|
die realen Aufrufstellen in doc/TRO_Katalog/scl_templates/*.scl zeigen.
|
|
|
|
Was aus dem Layout kommt, steht drin. Was aus der E-Planung kommen muss, steht
|
|
als Platzhalter mit `TODO(E-Planung)` da - bewusst als offensichtliche Luecke
|
|
und nicht als erfundener Wert, denn eine plausibel aussehende falsche
|
|
Sensoradresse ist schlimmer als eine leere. Alle Platzhalter sind ueber genau
|
|
diesen Marker auffindbar.
|
|
|
|
Ableitbar und daher gefuellt:
|
|
- Reihenfolge der REGIONs (Materialfluss), Instanzliste, FB-Typ je TRO
|
|
- Anzahl der Separator-/Weichen-/Scanner-Parameterbloecke je TRO
|
|
- nTo1Destinations einer Weiche = Anzahl ihrer Abgaenge
|
|
- die Zielliste je Weichenabgang (Erreichbarkeit im TRO-Graphen)
|
|
- Querverweis auf die Bauteile im Layout (Separatornummern, Koordinate)
|
|
|
|
Aufruf ueber bin/scl_skeleton.bat bzw. bin/scl_skeleton.sh.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
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, TroDocument
|
|
from scl_context import TODO, SclFile, SclName, SclTro, build_context
|
|
|
|
# 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.
|
|
DEFAULT_SEP_TIMES = (
|
|
("tDelayToNextItem", '"TimeForSepWaiting_Short"', None),
|
|
("tTrailingTime", "t#600ms", "pruefen"),
|
|
("tHandlingTime", "t#200ms", "pruefen"),
|
|
("tJam", '"TimeForSepJam"', None),
|
|
)
|
|
|
|
|
|
def load_document(path: Path) -> TroDocument:
|
|
"""TRO-JSON einlesen und gegen das Modell pruefen."""
|
|
return TroDocument.from_json(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
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 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 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: 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 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(tro.successors)} "
|
|
f"Abgaenge auf {len(tro.switches)} Weichen - Aufteilung festlegen")
|
|
add("")
|
|
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 tro.switch_exits:
|
|
ziele = ", ".join(abgang.targets) if abgang.targets else "-"
|
|
add(f" // ueber {abgang.via}: {ziele}")
|
|
add("")
|
|
|
|
|
|
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")
|
|
add(f" {pfad}.Settings.nCountCarrier := 1; // {TODO}: pruefen")
|
|
add(f" {pfad}.Settings.tWaitForStop := t#3s;")
|
|
add(f" {pfad}.Settings.tCarrierInPosDelay := t#1200ms;")
|
|
add(f" {pfad}.Settings.tCarrierInPosEdgeDelay := t#100ms;")
|
|
add(f" {pfad}.Settings.xJamSenDelayActive := TRUE;")
|
|
add(f" {pfad}.Settings.tJamSenDelay := T#2s;")
|
|
# 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: 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 '
|
|
f'(#arCognexInterface["cScanner____"])')
|
|
add("")
|
|
|
|
|
|
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.
|
|
"""
|
|
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]] = [
|
|
(f'nInMainTroNo := "{tro.main_tro.value}"', offen(tro.main_tro)),
|
|
(f"xInSftyOk := {tro.safety.value}", offen(tro.safety)),
|
|
("xInAllRdyToStart := TRUE", ""),
|
|
(f"xInRelease := {tro.release}", ""),
|
|
]
|
|
if tro.has_vario:
|
|
zeilen.append((f"xInMotorProtection := {tro.motor_protection.value}",
|
|
offen(tro.motor_protection)))
|
|
zeilen.append(("xInAutomatic := #t_automatik", ""))
|
|
# 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", ""))
|
|
|
|
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 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}("
|
|
einzug = " " * len(kopf)
|
|
for index, (code, kommentar) in enumerate(zeilen):
|
|
trenner = ");" if index == len(zeilen) - 1 else ","
|
|
prefix = kopf if index == 0 else einzug
|
|
text = f"{prefix}{code}{trenner}"
|
|
add(f"{text} // {kommentar}" if kommentar else text)
|
|
|
|
|
|
def emit_direction(add, tro: SclTro) -> None:
|
|
"""Weichenlogik - je Weiche ein FC_Direction-Aufruf."""
|
|
add("")
|
|
add(" REGION Direction")
|
|
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 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 : {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")
|
|
add("// Anlagenlayout und der vor Ort aufgenommenen Topologie.")
|
|
add("//")
|
|
add(f"// Jede Stelle, die aus der E-Planung kommen muss, ist mit")
|
|
add(f"// '{TODO}' markiert - danach suchen, dann ist nichts vergessen.")
|
|
add("// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),")
|
|
add("// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche")
|
|
add("// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.")
|
|
add("//")
|
|
add("// Reihenfolge der REGIONs = Materialfluss.")
|
|
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 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 context.tros:
|
|
if templates is not None and templates.has(tro.fb):
|
|
add("")
|
|
out.extend(templates.render(tro).split("\n"))
|
|
else:
|
|
emit_region(add, tro)
|
|
|
|
add("")
|
|
add("END_FUNCTION_BLOCK")
|
|
add("")
|
|
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")
|
|
|
|
json_file = Path(args.json)
|
|
if not json_file.is_absolute() and json_file.parent == Path("."):
|
|
json_file = results / args.json
|
|
if not json_file.is_file():
|
|
print(f"FEHLER: JSON nicht gefunden: {json_file}", file=sys.stderr)
|
|
print(" Zuerst bin/tro_flow.bat --file <name>.csv --json laufen lassen.",
|
|
file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
document = load_document(json_file)
|
|
except OSError as exc:
|
|
print(f"FEHLER: {json_file} nicht lesbar: {exc}", file=sys.stderr)
|
|
return 1
|
|
except ValidationError as exc:
|
|
schema = _schema_of(json_file)
|
|
if schema and schema != SCHEMA:
|
|
# Anderes Format (z. B. die alte tro_extract-JSON) - die einzelnen
|
|
# Feldfehler helfen dann nicht weiter.
|
|
print(f"FEHLER: {json_file} hat das Schema '{schema}', erwartet wird "
|
|
f"'{SCHEMA}' ({exc.error_count()} Abweichungen).", file=sys.stderr)
|
|
print(" Mit bin/tro_flow.bat --file <name>.csv --json neu erzeugen.",
|
|
file=sys.stderr)
|
|
return 1
|
|
print(f"FEHLER: {json_file} passt nicht zum TRO-Modell (lib/model.py):",
|
|
file=sys.stderr)
|
|
print(exc, file=sys.stderr)
|
|
return 1
|
|
if not document.tros:
|
|
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
|
|
return 1
|
|
|
|
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)
|
|
print("")
|
|
print("================================================================")
|
|
print("FB_MAIN-GERUEST")
|
|
print("================================================================")
|
|
print(f"Quelle = {json_file}")
|
|
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("================================================================")
|
|
print("")
|
|
return 0
|
|
|
|
|
|
def _schema_of(path: Path) -> str:
|
|
"""Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar)."""
|
|
try:
|
|
return str(json.loads(path.read_text(encoding="utf-8")).get("schema", ""))
|
|
except (OSError, ValueError, AttributeError):
|
|
return ""
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
prog="scl_skeleton",
|
|
description="Erzeugt aus der TRO-JSON (tro_flow --json) ein FB_Main-Geruest "
|
|
"als SCL zum Import in TIA Portal.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
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 (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",
|
|
default="export_tro.json",
|
|
metavar="NAME",
|
|
help="TRO-JSON aus tro_flow.py --json in %%SKEL_RESULTS%% "
|
|
"(oder ein vollstaendiger Pfad). Standard: %(default)s",
|
|
)
|
|
parser.add_argument(
|
|
"--out",
|
|
metavar="NAME",
|
|
help="Name der SCL-Datei in %%SKEL_RESULTS%%. Standard: <Quelle>_FB_Main.scl",
|
|
)
|
|
parser.add_argument(
|
|
"--start",
|
|
default="",
|
|
metavar="TRO",
|
|
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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|