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
@@ -0,0 +1,383 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
scl_context.py - Render-Kontext fuer das FB_Main-Geruest.
|
||||
|
||||
Schritt 2 aus doc/SCL_Templates.md: alles, was scl_skeleton aus der TRO-JSON
|
||||
*ableitet*, steht hier - Reihenfolge, Bauteilbloecke, Staubereiche,
|
||||
Weichenziele, Namen. Die Ausgabe (heute die emit_*-Funktionen, spaeter die
|
||||
Templates in lib/scl_templates/) liest nur noch diesen Kontext und rechnet
|
||||
nichts mehr selbst.
|
||||
|
||||
Schichten:
|
||||
|
||||
model.py TroDocument (die TRO-JSON)
|
||||
scl_context.py TroDocument -> SclFile <- hier
|
||||
scl_skeleton.py SclFile -> SCL-Text
|
||||
|
||||
Namen (Separator-, Weichen-, MainTro-Nummer, Adressen ...) sind ein
|
||||
:class:`SclName`: Wert plus Merker, ob er noch aus der E-Planung kommen muss.
|
||||
Heute sind alle Platzhalter mit ``todo=True``; die Namensregeln aus Schritt 4
|
||||
(cfg/scl_naming.ini) ersetzen dann nur den Wert und loeschen den Merker.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from model import TroBase, TroDocument
|
||||
from tro_catalog import TroItem
|
||||
|
||||
__all__ = [
|
||||
"TODO",
|
||||
"SclName",
|
||||
"SclSeparator",
|
||||
"SclSwitch",
|
||||
"SclSwitchExit",
|
||||
"SclScanner",
|
||||
"SclJam",
|
||||
"SclTro",
|
||||
"SclFile",
|
||||
"build_context",
|
||||
"flow_order",
|
||||
"reachable_via",
|
||||
]
|
||||
|
||||
TODO = "TODO(E-Planung)"
|
||||
|
||||
_CONFIG = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class SclName(BaseModel):
|
||||
"""Ein Name/eine Adresse in der SCL und ob er noch offen ist."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
value: str
|
||||
# True: Platzhalter, muss aus der E-Planung kommen -> TODO-Kommentar
|
||||
todo: bool = True
|
||||
|
||||
|
||||
def _offen(platzhalter: str) -> SclName:
|
||||
return SclName(value=platzhalter, todo=True)
|
||||
|
||||
|
||||
class SclSeparator(BaseModel):
|
||||
"""Ein stInSeparator<n>-Block."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Separatornummer aus dem Layout ("?" wenn der Typ mehr Separatoren hat
|
||||
# als im Layout gefunden wurden)
|
||||
layout: str
|
||||
no: SclName
|
||||
sensor: SclName
|
||||
stopper: SclName
|
||||
|
||||
|
||||
class SclSwitchExit(BaseModel):
|
||||
"""Ein Abgang eines Weichen-TRO und die darueber erreichbaren TROs."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
via: str
|
||||
targets: list[str]
|
||||
|
||||
|
||||
class SclSwitch(BaseModel):
|
||||
"""Ein stInSwitch<n>-Block plus sein FC_Direction-Aufruf."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
no: SclName
|
||||
valve: SclName
|
||||
# None: mehrere Weichen teilen sich die Abgaenge - Aufteilung ist eine
|
||||
# Planungsentscheidung und wird nicht geraten
|
||||
to1_destinations: int | None
|
||||
# Ziel von nOutDirection, z. B. "#TRO07.nInDirection" bzw. "...nInDirection2"
|
||||
direction_target: str
|
||||
# Staubereich, dessen erster Traeger in FC_Direction ausgewertet wird
|
||||
jam_db: SclName
|
||||
|
||||
|
||||
class SclScanner(BaseModel):
|
||||
"""Ein stInBarcodeReader-Block ("" als Suffix, wenn es nur einen gibt)."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
suffix: str
|
||||
|
||||
|
||||
class SclJam(BaseModel):
|
||||
"""Ein Staubereich am Ein- oder Ausgang (JamEntr<n> / JamExit<n>)."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Nachbar als Text fuer den Kommentar, SYSn als Station markiert
|
||||
neighbour: str
|
||||
db: SclName
|
||||
# Stausensor des Ausgangs (stInSenJam<n>); None an Eingaengen
|
||||
sensor: SclName | None = None
|
||||
|
||||
|
||||
class SclTro(BaseModel):
|
||||
"""Alles, was eine REGION fuer einen TRO braucht."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
id: str
|
||||
# Nummernteil der ID, wie geschrieben ("TRO01" -> "01"); "" ohne Ziffern
|
||||
nr: str
|
||||
type: str
|
||||
fb: str
|
||||
region_title: str
|
||||
layout_separators: list[str]
|
||||
position: tuple[float, float] | None
|
||||
predecessors: list[str]
|
||||
successors: list[str]
|
||||
confidence: str
|
||||
|
||||
separators: list[SclSeparator]
|
||||
switches: list[SclSwitch]
|
||||
switch_exits: list[SclSwitchExit]
|
||||
has_vario: bool
|
||||
# Vario-Sensoren in Aufrufreihenfolge (Parametername -> Sensor); leer ohne Vario
|
||||
vario_sensors: dict[str, SclName]
|
||||
scanners: list[SclScanner]
|
||||
storage_lines: int
|
||||
jam_entries: list[SclJam]
|
||||
jam_exits: list[SclJam]
|
||||
|
||||
main_tro: SclName
|
||||
safety: SclName
|
||||
release: str
|
||||
motor_protection: SclName
|
||||
vario_motor: SclName
|
||||
machine_state: SclName
|
||||
# Nur in den Templates (Referenz-Aufrufstellen), nicht im generischen Aufbau:
|
||||
# UDT des Prioritaetsmanagers ("udt_TRO107") und ob Richtung 2 Vorrang hat
|
||||
priority_udt: SclName
|
||||
prio_dir2: SclName
|
||||
# Kreisel-Freigabe (xInCarouselRun1). Ob ein TRO am Kreisel liegt, steht
|
||||
# nicht in der TRO-JSON - deshalb immer offen (doc/SCL_Templates.md §6).
|
||||
carousel_run: SclName
|
||||
|
||||
@property
|
||||
def has_scanner(self) -> bool:
|
||||
return bool(self.scanners)
|
||||
|
||||
|
||||
class SclFile(BaseModel):
|
||||
"""Kontext der ganzen Datei: Kopf und die TROs in Fliessrichtung."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
source: str
|
||||
created: str
|
||||
start_reason: str
|
||||
tros: list[SclTro] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Graph
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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 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 _nummer(tro_id: str) -> str:
|
||||
treffer = re.search(r"(\d+)$", tro_id)
|
||||
return treffer.group(1) if treffer else ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kontext bauen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tro_context(tro: TroBase, alle: dict[str, TroBase]) -> SclTro:
|
||||
items = tro.items
|
||||
n_sep = items.get(TroItem.SEPARATOR, 1)
|
||||
n_swi = items.get(TroItem.SWITCH, 0)
|
||||
n_scan = items.get(TroItem.SCANNER, 0)
|
||||
tro_id = tro.tro_id
|
||||
jam_db = '"DB_JamArea____".stJam____'
|
||||
|
||||
separators = [
|
||||
SclSeparator(
|
||||
n=i,
|
||||
layout=tro.separators[i - 1] if i <= len(tro.separators) else "?",
|
||||
no=_offen("cSep_____"),
|
||||
sensor=_offen("cInBG____"),
|
||||
stopper=_offen("MB____"),
|
||||
)
|
||||
for i in range(1, n_sep + 1)
|
||||
]
|
||||
switches = [
|
||||
SclSwitch(
|
||||
n=j,
|
||||
no=_offen("cSwi_____"),
|
||||
valve=_offen("MB____"),
|
||||
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
|
||||
to1_destinations=len(tro.successors) if n_swi == 1 else None,
|
||||
direction_target=(f"#{tro_id}.nInDirection" if n_swi == 1
|
||||
else f"#{tro_id}.nInDirection{j}"),
|
||||
jam_db=_offen(jam_db),
|
||||
)
|
||||
for j in range(1, n_swi + 1)
|
||||
]
|
||||
has_vario = bool(items.get(TroItem.VARIO_DRIVE))
|
||||
vario_sensors = {
|
||||
name: _offen("cInBG____")
|
||||
for name in ("stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam")
|
||||
} if has_vario else {}
|
||||
switch_exits = [
|
||||
SclSwitchExit(via=abgang, targets=reachable_via(tro_id, abgang, alle))
|
||||
for abgang in tro.successors
|
||||
] if n_swi else []
|
||||
|
||||
return SclTro(
|
||||
id=tro_id,
|
||||
nr=_nummer(tro_id),
|
||||
type=tro.type_name,
|
||||
fb=tro.fb_block or "FB_UNBEKANNT",
|
||||
region_title=_region_title(tro),
|
||||
layout_separators=list(tro.separators),
|
||||
position=(tro.x, tro.y) if tro.position else None,
|
||||
predecessors=list(tro.predecessors),
|
||||
successors=list(tro.successors),
|
||||
confidence=tro.confidence,
|
||||
separators=separators,
|
||||
switches=switches,
|
||||
switch_exits=switch_exits,
|
||||
has_vario=has_vario,
|
||||
vario_sensors=vario_sensors,
|
||||
scanners=[
|
||||
SclScanner(n=i, suffix="" if n_scan == 1 else str(i))
|
||||
for i in range(1, n_scan + 1)
|
||||
],
|
||||
storage_lines=items.get(TroItem.STORAGE_LINE, 0),
|
||||
# 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.
|
||||
jam_entries=[
|
||||
SclJam(n=n, neighbour=_nachbar(vor, alle), db=_offen(jam_db))
|
||||
for n, vor in enumerate(tro.predecessors, start=1)
|
||||
],
|
||||
jam_exits=[
|
||||
SclJam(n=n, neighbour=_nachbar(nach, alle), db=_offen(jam_db),
|
||||
sensor=_offen("cInBG____"))
|
||||
for n, nach in enumerate(tro.successors, start=2)
|
||||
],
|
||||
main_tro=_offen("cMainTro____"),
|
||||
safety=_offen('"DB_InterfaceSafety".stFromSafety.EStopMain.Q'),
|
||||
release=f'"DB_TRO_TEST".{tro_id}',
|
||||
motor_protection=_offen('"FC____"'),
|
||||
vario_motor=_offen("MA____"),
|
||||
# Schaltschrank ist anlagenspezifisch ("...OF1" in der Referenz)
|
||||
machine_state=_offen("#stInOutControlUnitCabinet"),
|
||||
priority_udt=_offen("udt_____"),
|
||||
# Vorschlag aus der Referenz TRO107, Prioritaet bleibt Planungsentscheidung
|
||||
prio_dir2=_offen("TRUE"),
|
||||
carousel_run=_offen("#fbCarousel____.xOutConveyorFullSpeed"),
|
||||
)
|
||||
|
||||
|
||||
def build_context(document: TroDocument, source: str, start_id: str = "",
|
||||
created: str | None = None) -> SclFile:
|
||||
"""TroDocument -> SclFile (TROs in Fliessrichtung)."""
|
||||
alle = {t.tro_id: t for t in document.tros}
|
||||
geordnet, startgrund = flow_order(document.tros, start_id)
|
||||
return SclFile(
|
||||
source=source,
|
||||
created=created or datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
start_reason=startgrund,
|
||||
tros=[_tro_context(t, alle) for t in geordnet],
|
||||
)
|
||||
+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)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_1Sep1Swi - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl
|
||||
(TRO 107, =A01+UH01-KF00 FB_Main). Zeilen und Reihenfolge folgen
|
||||
dieser Aufrufstelle; geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
stInSenJam<n> je Ausgang einer, Sensor offen
|
||||
stInSettings.xPrioDir2 TRUE wie Referenz, aber offen (Planungsentscheidung)
|
||||
PriorityManagerDir2 immer, UDT-Name offen
|
||||
xInCarouselRun1 immer, offen - ob der TRO am Kreisel liegt, steht
|
||||
nicht in der TRO-JSON; sonst Zeile loeschen
|
||||
xInRelease TRUE wie Referenz (generisch: "DB_TRO_TEST")
|
||||
Separatorzeiten 100/50 ms wie Referenz, "pruefen"
|
||||
stInOutMachineState Schaltschrank offen (anlagenspezifisch)
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
{{ m.separator(t, s) }}
|
||||
{% endfor %}
|
||||
{{ m.jam_sensors(t) }}
|
||||
|
||||
{% for w in t.switches %}
|
||||
{{ m.switch(t, w) }}
|
||||
{% endfor %}
|
||||
|
||||
{{ m.ziele(t) }}
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroIn', m.note(t.priority_udt))) %}
|
||||
{% set _ = z.append(('stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroOut', m.note(t.priority_udt))) %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(("xInCarouselRun1 := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
|
||||
{% set _ = z.append(("xInRelease := TRUE", "")) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, "Staubereich von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, "Staubereich nach " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% for s in t.separators %}
|
||||
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
|
||||
{% endfor %}
|
||||
{% for w in t.switches %}
|
||||
{% set _ = z.append(("xOutSw" ~ w.n ~ 'ExTo3 => "' ~ w.valve.value ~ '"', m.note(w.valve))) %}
|
||||
{% endfor %}
|
||||
#{{ t.id }}.stInSettings.xPrioDir2 := {{ t.prio_dir2.value }};{{ m.todo(t.prio_dir2) }}
|
||||
{{ m.call(t, z) }}
|
||||
|
||||
{{ m.direction(t) }}
|
||||
END_REGION
|
||||
@@ -0,0 +1,103 @@
|
||||
{#-
|
||||
_macros.scl.j2 - gemeinsame Bausteine der FB-Templates (doc/SCL_Templates.md §5.2)
|
||||
|
||||
Jede Zeile ist eine der Klassen aus §6: L (Layout, aus dem Kontext),
|
||||
N (Name, heute Platzhalter + TODO), R (Referenzwert aus der realen Aufrufstelle),
|
||||
E (E-Planung, Platzhalter + TODO), C (Bedingung).
|
||||
|
||||
Makros liefern ihre Zeilen OHNE abschliessenden Zeilenumbruch; der Aufrufer
|
||||
setzt ihn. Globale Variable: TODO (Marker "TODO(E-Planung)").
|
||||
-#}
|
||||
|
||||
{#- TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist -#}
|
||||
{% macro todo(name, abstand=" ") -%}
|
||||
{% if name.todo %}{{ abstand }}// {{ TODO }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Kommentartext fuer eine Parameterzeile des Aufrufs -#}
|
||||
{% macro note(name, text="") -%}
|
||||
{% if name.todo %}{{ TODO }}{% if text %} - {{ text }}{% endif %}{% else %}{{ text }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Kommentarkopf der REGION: Lage und Nachbarn im Layout (L) -#}
|
||||
{% macro kopf(t) %}
|
||||
{% if t.position %}
|
||||
// Anlagenkoordinate x/y: {{ "%.0f"|format(t.position[0]) }} / {{ "%.0f"|format(t.position[1]) }} mm
|
||||
{% endif %}
|
||||
// Vorgaenger: {{ t.predecessors|join(", ") or "-" }}
|
||||
// Nachfolger: {{ t.successors|join(", ") or "-" }}
|
||||
{% if t.confidence %}
|
||||
// Einstufung: {{ t.confidence }}
|
||||
{% endif %}
|
||||
// Aufbau nach Template {{ t.fb }}.scl.j2 (Referenz: doc/TRO_Katalog/scl_templates)
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSeparator<n>: Zeiten R aus der Referenz (TRO107), Nummer N, Sensor E -#}
|
||||
{% macro separator(t, s) %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#100ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#50ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ todo(s.sensor) }}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSenJam<n>: ein Stausensor je Ausgang (E) -#}
|
||||
{% macro jam_sensors(t) %}
|
||||
{% for j in t.jam_exits %}
|
||||
#{{ t.id }}.stInSenJam{{ j.n }} := "DB_Inputs".Sensors["{{ j.sensor.value }}"];{{ todo(j.sensor) }}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSwitch<n>: Nummer N; nTo1Destinations L, aber Bedeutung noch zu klaeren -#}
|
||||
{% macro switch(t, w) %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}";{{ todo(w.no) }}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen3 := FALSE;
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen4 := FALSE;
|
||||
{% if w.to1_destinations is not none %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := {{ w.to1_destinations }}; // Abgaenge laut Topologie - {{ TODO }}: pruefen (Info fuer HMI; Referenz TRO107 hat 3 bei 2 Abgaengen)
|
||||
{% else %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := _; // {{ TODO }}: {{ t.successors|length }} Abgaenge auf {{ t.switches|length }} Weichen - Aufteilung festlegen
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Zielliste je Weichenabgang (L, nur Kommentar) -#}
|
||||
{% macro ziele(t) %}
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
{% for e in t.switch_exits %}
|
||||
// ueber {{ e.via }}: {{ e.targets|join(", ") or "-" }}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#-
|
||||
Instanzaufruf aus einer Liste von (Code, Kommentar). Das Komma wird hier
|
||||
gesetzt und steht damit VOR dem Kommentar - sonst waere es Teil des
|
||||
Kommentars und die Parameterliste uebersetzt nicht.
|
||||
-#}
|
||||
{% macro call(t, zeilen) %}
|
||||
{% set einzug = " " * (6 + t.id|length) %}
|
||||
{% for code, kommentar in zeilen %}
|
||||
{{ (" #" ~ t.id ~ "(") if loop.first else einzug }}{{ code }}{{ ");" if loop.last else "," }}{% if kommentar %} // {{ kommentar }}{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- REGION Direction: je Weiche ein FC_Direction-Aufruf -#}
|
||||
{% macro direction(t) %}
|
||||
REGION Direction
|
||||
{% for w in t.switches %}
|
||||
"FC_Direction"(nInSwitchNo := "{{ w.no.value }}",{{ todo(w.no) }}
|
||||
nOutDirection => {{ w.direction_target }},
|
||||
InOutCarrier := {{ w.jam_db.value }}.arCarrier[1]);{{ todo(w.jam_db) }}
|
||||
{% if not loop.last %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
END_REGION
|
||||
{%- endmacro %}
|
||||
Reference in New Issue
Block a user