scl_skeleton: Anlagen-JSON als Eingabe (Stufe 4, Namen)
--json erkennt <csv>_anlage.json am Schema. scl_context.HwSource setzt Namen aus bindings (nur "zugeordnet") und conveyors ein: Separator-, Lagerlinien-, Weichen- und Vario-Antriebsnamen, #fbCarousel<NNNN> am Kreisel, Foerderernummer im DB_JamArea-Platzhalter (<k> bleibt offen). Eingesetzte Namen werden gegen <projekt>_hw.json geprueft (--hw, --strict bricht ab). --eplan bleibt fuer die TRO-JSON; Templates und Golden-Files unveraendert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+132
-23
@@ -6,14 +6,19 @@ 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
|
||||
material_flow.py Objekte -> Materialfluss
|
||||
tro_flow.py Fluss -> TROs -> <name>_tro.json (--json)
|
||||
merge_hw_layout.py TRO-JSON + Hardwareplanung -> <name>_anlage.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.
|
||||
Eingabe ist die TRO-JSON (nur Layout) oder die Anlagen-JSON (Layout plus
|
||||
Zuordnungen der Hardwareplanung); erkannt am Schema. Beide werden ueber das
|
||||
gemeinsame Modell lib/model.py (TroDocument / AnlageDocument) 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. Aus der Anlagen-JSON
|
||||
werden zugeordnete Sensoren, Ventile, Antriebe und Foerderernummern
|
||||
eingesetzt (scl_context.HwSource); offene Zuordnungen bleiben TODO.
|
||||
|
||||
Erzeugt wird ein **Geruest**, kein fertiges Programm: je TRO eine REGION mit
|
||||
Instanzaufruf und allen Parameterzeilen in der richtigen Reihenfolge, so wie es
|
||||
@@ -49,11 +54,12 @@ from jinja2 import (
|
||||
TemplateError,
|
||||
TemplateNotFound,
|
||||
)
|
||||
from pydantic import ValidationError
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from e_planning import EPLAN_SCHEMA, EplanDocument
|
||||
from hw_import import HardwareDocument
|
||||
from material_flow import env_dir
|
||||
from model import SCHEMA, TroDocument
|
||||
from model import ANLAGE_SCHEMA, SCHEMA, AnlageDocument, TroDocument
|
||||
from scl_context import TODO, SclFile, SclName, SclTro, build_context
|
||||
|
||||
# Parametrisierte Aufrufstellen je FB (doc/SCL_Templates.md); Dateiname = fbType.
|
||||
@@ -336,6 +342,13 @@ def render_context(context: SclFile, templates: SclTemplates | None = None) -> s
|
||||
add("// Separator-Sensoren und Stopperventile aus der Zuordnung sind")
|
||||
add("// eingesetzt (ohne Marker); was die Zuordnung nicht sicher fand,")
|
||||
add(f"// bleibt '{TODO}'. Details: <csv>_eplan.md (lib/e_planning.py).")
|
||||
if context.hw_source:
|
||||
add("//")
|
||||
add(f"// Hardwareplanung: {context.hw_source}")
|
||||
add("// Zugeordnete Sensoren, Ventile, Antriebe und Foerderernummern")
|
||||
add("// sind eingesetzt (ohne Marker); offene Zuordnungen und")
|
||||
add(f"// Widersprueche bleiben '{TODO}'. Details: <csv>_anlage.md")
|
||||
add("// (lib/merge_hw_layout.py).")
|
||||
if templates is not None:
|
||||
mit, ohne = template_usage(context, templates)
|
||||
add("//")
|
||||
@@ -386,13 +399,23 @@ def main(argv: list[str] | None = None) -> int:
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
schema = _schema_of(json_file)
|
||||
anlage = None
|
||||
try:
|
||||
document = load_document(json_file)
|
||||
if schema == ANLAGE_SCHEMA:
|
||||
anlage = AnlageDocument.from_json(json_file.read_text(encoding="utf-8"))
|
||||
document = anlage.layout
|
||||
else:
|
||||
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 == ANLAGE_SCHEMA:
|
||||
print(f"FEHLER: {json_file} passt nicht zum Anlagen-Modell (lib/model.py):",
|
||||
file=sys.stderr)
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
if schema and schema != SCHEMA:
|
||||
# Anderes Format (z. B. die alte tro_extract-JSON) - die einzelnen
|
||||
# Feldfehler helfen dann nicht weiter.
|
||||
@@ -417,6 +440,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 1
|
||||
templates = SclTemplates(ordner)
|
||||
|
||||
if anlage is not None and args.eplan:
|
||||
print("FEHLER: --eplan geht nicht zusammen mit einer Anlagen-JSON - die "
|
||||
"Zuordnungen stehen schon darin (merge_hw_layout).", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
eplan = None
|
||||
if args.eplan:
|
||||
eplan_file = _in_results(args.eplan, results)
|
||||
@@ -438,12 +466,39 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"TRO-JSON zu '{document.source}'.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
context = build_context(document, json_file.name, args.start, eplan=eplan)
|
||||
context = build_context(document, json_file.name, args.start, eplan=eplan,
|
||||
anlage=anlage)
|
||||
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
|
||||
|
||||
# Gegenpruefung: jeder eingesetzte Name muss in der Hardwareplanung stehen
|
||||
# (doc/Konzept_Layout_Hardware_SCL.md, Stufe 4). Die Namen kommen selbst aus
|
||||
# ihr - ein Fehler hier heisst: Handkorrektur (hw_overrides.ini) mit
|
||||
# Tippfehler oder Anlagen-JSON zu einem anderen Stand der Hardwareplanung.
|
||||
hw_file = None
|
||||
unbekannt: list[str] = []
|
||||
if anlage is not None and (args.hw or anlage.hw_file):
|
||||
hw_file = _in_results(args.hw or Path(anlage.hw_file).name, results)
|
||||
try:
|
||||
hw = HardwareDocument.from_json(hw_file.read_text(encoding="utf-8"))
|
||||
except (OSError, ValidationError) as exc:
|
||||
if args.hw:
|
||||
print(f"FEHLER: Hardwareplanung nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Hinweis: {hw_file} nicht lesbar - Namen nicht gegengeprueft.")
|
||||
hw_file = None
|
||||
else:
|
||||
unbekannt = unknown_names(context, hw)
|
||||
if unbekannt:
|
||||
stufe = "FEHLER" if args.strict else "WARNUNG"
|
||||
print(f"{stufe}: {len(unbekannt)} eingesetzte Namen fehlen in {hw_file.name}: "
|
||||
f"{', '.join(unbekannt)}", file=sys.stderr)
|
||||
if args.strict:
|
||||
print(" (--strict) - keine SCL geschrieben.", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
text = render_context(context, templates)
|
||||
except TemplateError as exc:
|
||||
@@ -451,7 +506,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
# 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")
|
||||
stamm = json_file.stem.replace("_anlage", "").replace("_tro", "")
|
||||
out_file = results / (args.out or f"{stamm}_FB_Main.scl")
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
offen = text.count(TODO)
|
||||
@@ -469,6 +525,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if eplan is not None:
|
||||
print(f"E-Planung = {eplan_file}")
|
||||
print(f"Eingesetzt = {_eingesetzt(context)} Namen aus der E-Planung")
|
||||
if anlage is not None:
|
||||
print(f"Hardwareplanung = {anlage.hw_file or '-'}")
|
||||
print(f"Eingesetzt = {len(known_names(context))} Namen aus der Anlagen-JSON")
|
||||
if hw_file is not None:
|
||||
print(f"Gegenpruefung = {len(unbekannt)} unbekannte Namen ({hw_file.name})")
|
||||
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
|
||||
print(f"SCL = {out_file}")
|
||||
print("================================================================")
|
||||
@@ -498,6 +559,41 @@ def _eingesetzt(context: SclFile) -> int:
|
||||
return sum(not name.todo for name in namen)
|
||||
|
||||
|
||||
def known_names(context: SclFile) -> list[str]:
|
||||
"""Alle eingesetzten (nicht offenen) Namen des Kontexts, ohne Doppelte."""
|
||||
namen: list[str] = []
|
||||
|
||||
def walk(obj) -> None:
|
||||
if isinstance(obj, SclName):
|
||||
if not obj.todo and obj.value not in namen:
|
||||
namen.append(obj.value)
|
||||
elif isinstance(obj, BaseModel):
|
||||
for feld in type(obj).model_fields:
|
||||
walk(getattr(obj, feld))
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
for x in obj:
|
||||
walk(x)
|
||||
elif isinstance(obj, dict):
|
||||
for x in obj.values():
|
||||
walk(x)
|
||||
|
||||
walk(context)
|
||||
return namen
|
||||
|
||||
|
||||
def unknown_names(context: SclFile, hw: HardwareDocument) -> list[str]:
|
||||
"""Eingesetzte Namen, die die Hardwareplanung nicht kennt.
|
||||
|
||||
Geprueft werden Symbole und Konstanten (cInBG0532, MB0533, "FC0202");
|
||||
Instanzzugriffe wie #fbCarousel0102.xOut... sind keine globalen Namen.
|
||||
"""
|
||||
bekannt = {n for d in hw.devices for n in (d.bmk, d.constant) if n}
|
||||
bekannt |= {c.name for c in hw.constants}
|
||||
bekannt |= {n for d in hw.drives for n in (d.motor, d.protection, d.encoder) if n}
|
||||
return [name for name in known_names(context)
|
||||
if not name.startswith("#") and name.strip('"') not in bekannt]
|
||||
|
||||
|
||||
def _schema_of(path: Path) -> str:
|
||||
"""Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar)."""
|
||||
try:
|
||||
@@ -509,26 +605,30 @@ def _schema_of(path: Path) -> str:
|
||||
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.",
|
||||
description="Erzeugt aus der TRO-JSON (tro_flow --json) oder der "
|
||||
"Anlagen-JSON (merge_hw_layout) 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"
|
||||
" scl_skeleton.bat --json mubea_anlage.json --templates\n"
|
||||
" scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json\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",
|
||||
" ein FB-Typ ohne Template oder ein eingesetzter Name, der\n"
|
||||
" in der Hardwareplanung fehlt\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",
|
||||
help="TRO-JSON aus tro_flow.py --json oder Anlagen-JSON aus "
|
||||
"merge_hw_layout.py in %%SKEL_RESULTS%% (oder ein vollstaendiger "
|
||||
"Pfad); erkannt am Schema. Standard: %(default)s",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
@@ -555,15 +655,24 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--eplan",
|
||||
metavar="NAME",
|
||||
help="Zuordnung zur E-Planung aus e_planning.py (<csv>_eplan.json in "
|
||||
"%%SKEL_RESULTS%% oder ein Pfad): Separator-Sensoren und "
|
||||
"Stopperventile werden eingesetzt statt als TODO markiert.",
|
||||
help="Auslaufend, nur mit einer TRO-JSON: Zuordnung zur E-Planung aus "
|
||||
"e_planning.py (<csv>_eplan.json in %%SKEL_RESULTS%% oder ein "
|
||||
"Pfad): Separator-Sensoren und Stopperventile werden eingesetzt "
|
||||
"statt als TODO markiert. Mit der Anlagen-JSON nicht noetig.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hw",
|
||||
metavar="NAME",
|
||||
help="Nur mit einer Anlagen-JSON: Hardwareplanung (<projekt>_hw.json) "
|
||||
"fuer die Gegenpruefung der eingesetzten Namen. Standard: hwFile "
|
||||
"aus der Anlagen-JSON, in %%SKEL_RESULTS%%.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Nur mit --templates: abbrechen (Exit 1), wenn ein FB-Typ der "
|
||||
"Anlage kein Template hat.",
|
||||
help="Abbrechen (Exit 1), wenn ein FB-Typ der Anlage kein Template "
|
||||
"hat (mit --templates) oder ein eingesetzter Name in der "
|
||||
"Hardwareplanung fehlt (mit Anlagen-JSON).",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user