Files
sps_skel/lib/tro_catalog.py
T
2026-08-20 11:38:51 +02:00

557 lines
20 KiB
Python

# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
tro_catalog.py - Typdefinition der TROs (Transfer Route Objects).
Dieses Modul ist die **einzige** Quelle fuer alles, was zur Erzeugung eines TRO
gebraucht wird. Es liest keine Dateien und braucht kein Markdown zur Laufzeit -
die Herkunft der Daten steht nur noch in den Kommentaren.
Ein TRO-Typ wird durch fuenf Dinge beschrieben:
1. Name - Kurztyp, wie in FB_Main und connect.ini verwendet ("1Sep")
2. FB-Baustein - der Siemens-Funktionsbaustein ("FB_ILS_MTRO_1Sep")
3. Bauteile - welche Elemente in diesem TRO stecken und wie viele
(1Sep = 1x Separator, 1Sep1Swi = 1x Separator + 1x Weiche)
4. Farbe - Farbgruppe fuer Zeichnungen (Graphviz, Mermaid, CAD)
5. Symbol - Form des CAD-Markers, je Typ eine eigene (Kreis, Dreieck,
Rechteck, Raute ...)
Herkunft der Daten (Stand :data:`CATALOG_AS_OF`)
-----------------------------------------------
Einmalig uebernommen aus - danach hier gepflegt, nicht mehr nachgelesen:
doc/TRO_Katalog/TRO_Katalog.md
"Overview of all TRO types" -> Typname und FB-Baustein
"Color legend" -> Farbgruppen (siehe TroStyle)
doc/TRO_Typen.md
"Baukasten-Analyse" -> Bauteile je Typ (Sub-Bausteine/STRO)
doc/HundM/Json_Layout-Konzept.md
classDef-Farben -> die konkreten Farbwerte
doc/TRO_Katalog/scl_templates/*.scl
VAR-Deklarationen der lokal vorliegenden FBs - dort sind die Bauteile
direkt nachweisbar
Belegbarkeit der Bauteillisten
------------------------------
Nur vier FBs liegen als Quelle im Repo; deren Bauteile sind aus den
VAR-Deklarationen **verifiziert** (Kommentar "aus SCL belegt"). Die uebrigen
Typen sind `.liblink`-Referenzen auf die externe ILSLib - ihre Bauteile stammen
aus der Mapping-Tabelle in TRO_Typen.md (Kommentar "laut TRO_Typen.md").
Verwendung
----------
from tro_catalog import TroItem, get_tro, tro_names
tro = get_tro("1Sep1Swi")
tro.get_fb_block() -> "FB_ILS_MTRO_1Sep1Swi"
tro.get_item_count(TroItem.SWITCH) -> 1
tro.get_items() -> {"Separator": 1, "Switch": 1}
tro.describe_items() -> "1x Separator, 1x Switch"
tro.get_style().fill -> "#ffe4a0"
tro.get_dot_attrs() -> {"fillcolor": ..., "color": ...}
tro.set_item(TroItem.SCANNER, 1) -> Bauteil ergaenzen/aendern
TroDefinition ist ein Pydantic-Modell (v2). Neben den get_*/set_*-Methoden gibt
es damit Validierung bei jeder Zuweisung sowie model_dump()/model_dump_json()
und model_validate() fuer das geplante JSON-Layoutmodell.
Benoetigt `pydantic >= 2.0.0` (siehe requirements.txt, bin/install_py).
"""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field, field_validator
__all__ = [
"CATALOG_AS_OF",
"TroItem",
"TroStyle",
"TroSymbol",
"TroDefinition",
"ITEM_FB",
"STYLES",
"TRO_CATALOG",
"get_tro",
"tro_names",
"set_tro",
"catalog",
"mermaid_classdefs",
]
# Stand der Dokumente, aus denen die Typdefinition uebernommen wurde
CATALOG_AS_OF = "2026-07-20"
class TroItem:
"""
Bauteilarten, aus denen ein TRO zusammengesetzt ist.
Konstanten statt freier Strings, damit Tippfehler auffallen.
:data:`ITEM_FB` ordnet jeder Art ihren Sub-Funktionsbaustein zu.
"""
SEPARATOR = "Separator"
SWITCH = "Switch"
SCANNER = "Scanner"
VARIO_DRIVE = "VarioDrive"
WORK_STATION = "WorkStation"
STORAGE_LINE = "StorageLine"
ACCUMULATE = "Accumulate"
ENCODER = "Encoder"
SSCC = "SSCC"
BOOM = "Boom"
FOOT = "Foot"
TILT_SENSOR = "TiltSensor"
@classmethod
def all(cls) -> tuple[str, ...]:
"""Alle bekannten Bauteilarten."""
return tuple(
value
for key, value in vars(cls).items()
if key.isupper() and isinstance(value, str)
)
# Sub-Funktionsbaustein je Bauteilart. Leer = kein eigener FB (Hardware bzw.
# im Rahmenwerk des TRO umgesetzt).
ITEM_FB: dict[str, str] = {
TroItem.SEPARATOR: "FB_ILS_STRO_Sep",
TroItem.SWITCH: "FB_ILS_STRO_Switch",
TroItem.SCANNER: "FB_BarcodeReaderCognex",
TroItem.VARIO_DRIVE: "FB_ILS_STRO_Vario",
TroItem.WORK_STATION: "FB_ILS_STRO_Vario_WorkStation",
TroItem.STORAGE_LINE: "FB_ILS_STRO_Sep", # als Array[1..N]
TroItem.ACCUMULATE: "FB_CarrAccumulate1Sep",
TroItem.ENCODER: "",
TroItem.SSCC: "",
TroItem.BOOM: "FB_BoomWorkStation",
TroItem.FOOT: "FB_DistanceFoot",
TroItem.TILT_SENSOR: "FB_Tilt_TEST",
}
class TroSymbol:
"""
Zeichensymbole fuer die Marker in der Zeichnung.
Die Farbe gruppiert (siehe TroStyle), das Symbol unterscheidet den einzelnen
Typ. Alle TROs eines Typs bekommen dasselbe Symbol. Die Formen sind absichtlich
einfach - Kreis, Vielecke, Rechteck - damit sie in jedem Zoom erkennbar
bleiben und als LWPOLYLINE bzw. CIRCLE in den Block passen.
"""
CIRCLE = "circle" # Kreis
DOUBLE_CIRCLE = "double_circle" # Kreis mit zweitem Kreis innen
TRIANGLE = "triangle" # Dreieck, Spitze oben
TRIANGLE_DOWN = "triangle_down" # Dreieck, Spitze unten
SQUARE = "square" # Quadrat
DIAMOND = "diamond" # Raute
PENTAGON = "pentagon" # Fuenfeck
HEXAGON = "hexagon" # Sechseck
RECT = "rect" # liegendes Rechteck
ARROW = "arrow" # Pfeil/Fahne
# Dreieck mit zweitem Dreieck innen - fuer 2Sep2Swi, sichtbar verwandt mit
# TRIANGLE_DOWN (2Sep1Swi) und trotzdem davon unterscheidbar.
DOUBLE_TRIANGLE_DOWN = "double_triangle_down"
@classmethod
def all(cls) -> tuple[str, ...]:
return tuple(
value
for key, value in vars(cls).items()
if key.isupper() and isinstance(value, str)
)
class TroStyle(BaseModel):
"""
Darstellung einer TRO-Typgruppe.
Farbwerte uebernommen aus der "Color legend" in TRO_Katalog.md bzw. den
classDef-Zeilen in Json_Layout-Konzept.md:
sep blau Separator (1Sep)
swi orange Weiche (1Sep1Swi, 1Sep2Swi, 2Sep1Swi)
sscc rot Scanner-Sonderfall (1Sep_SSCC)
vario violett Kettenfoerderer (Vario, Vario_workStation)
store gruen Speicher/Puffer (PinStore_Auto, EmptyCarrBuffer)
ext grau Systemgrenze, extern, Loading Boom
Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist
violett.
"""
model_config = ConfigDict(validate_assignment=True, extra="forbid",
str_strip_whitespace=True)
group: str = Field(min_length=1, description="Kurzname der Farbgruppe, z. B. 'vario'")
label: str = Field(min_length=1, description="Klartext, z. B. 'Kettenfoerderer'")
fill: str = Field(min_length=4, description="Fuellfarbe als Hex, z. B. '#e8d4f0'")
stroke: str = Field(min_length=4, description="Randfarbe als Hex")
font: str = Field(default="#111111", min_length=4, description="Schriftfarbe")
def get_group(self) -> str:
return self.group
def get_fill(self) -> str:
return self.fill
def get_stroke(self) -> str:
return self.stroke
def get_font(self) -> str:
return self.font
def dot_attrs(self) -> dict[str, str]:
"""Graphviz-Knotenattribute."""
return {"fillcolor": self.fill, "color": self.stroke, "fontcolor": self.font}
def mermaid_classdef(self) -> str:
"""Mermaid-classDef-Zeile fuer diese Gruppe."""
return (
f"classDef {self.group} fill:{self.fill},"
f"stroke:{self.stroke},color:{self.font}"
)
# Farbgruppen - eine Instanz je Gruppe, von den Typen unten referenziert.
STYLE_SEP = TroStyle(group="sep", label="Separator",
fill="#d4e8ff", stroke="#3388cc")
STYLE_SWI = TroStyle(group="swi", label="Weiche",
fill="#ffe4a0", stroke="#cc8800")
STYLE_SSCC = TroStyle(group="sscc", label="Scanner (SSCC)",
fill="#ffd4d4", stroke="#cc3333")
STYLE_VARIO = TroStyle(group="vario", label="Kettenfoerderer",
fill="#e8d4f0", stroke="#883388")
STYLE_STORE = TroStyle(group="store", label="Speicher / Puffer",
fill="#d4f0d4", stroke="#338833")
STYLE_EXT = TroStyle(group="ext", label="Systemgrenze / extern",
fill="#f0f0f0", stroke="#888888", font="#404040")
STYLES: tuple[TroStyle, ...] = (
STYLE_SEP, STYLE_SWI, STYLE_SSCC, STYLE_VARIO, STYLE_STORE, STYLE_EXT,
)
class TroDefinition(BaseModel):
"""
Ein TRO-Typ: Name, FB-Baustein, Bauteile und Darstellung.
Pydantic-Modell - die Felder werden bei Erzeugung *und* bei jeder Zuweisung
geprueft (`validate_assignment`). Damit ist ein TroDefinition-Objekt nie in
einem ungueltigen Zustand:
name nicht leer
fb_block nicht leer
items {Bauteilart: Anzahl}, Anzahl >= 1, Art nicht leer;
Eintraege mit Anzahl <= 0 werden verworfen
Positionsargumente sind weiter moeglich (`TroDefinition("1Sep", "FB_...")`),
ebenso benannte (`TroDefinition(name="1Sep", fb_block="FB_...")`).
Weil es ein BaseModel ist, gibt es Serialisierung gratis - nuetzlich fuer
das geplante JSON-Layoutmodell:
tro.model_dump() -> dict
tro.model_dump_json(indent=2) -> JSON
TroDefinition.model_validate(d) -> aus dict
TroDefinition.model_json_schema() -> JSON-Schema
"""
model_config = ConfigDict(
validate_assignment=True, # set_* darf nichts Ungueltiges hinterlassen
extra="forbid", # Tippfehler in Feldnamen fallen auf
str_strip_whitespace=True,
)
name: str = Field(
min_length=1,
description="Kurztyp, wie in FB_Main und connect.ini verwendet, z. B. '1Sep'",
)
fb_block: str = Field(
min_length=1,
description="Name des Siemens-Funktionsbausteins, z. B. 'FB_ILS_MTRO_1Sep'",
)
items: dict[str, int] = Field(
default_factory=dict,
description="Bauteilart -> Anzahl, z. B. {'Separator': 1, 'Switch': 1}",
)
style: TroStyle = Field(
default=STYLE_EXT,
description="Farbgruppe fuer Zeichnungen (Graphviz, Mermaid, CAD-Symbol)",
)
symbol: str = Field(
default=TroSymbol.CIRCLE,
min_length=1,
description="Zeichensymbol des Markers im CAD, z. B. 'triangle'",
)
def __init__(
self,
name: str | None = None,
fb_block: str | None = None,
items: dict[str, int] | None = None,
style: TroStyle | None = None,
symbol: str | None = None,
**data,
) -> None:
# Positionsargumente auf Feldnamen abbilden, damit die bisherige
# Aufrufform TroDefinition("1Sep", "FB_...", {...}) erhalten bleibt.
if name is not None:
data["name"] = name
if fb_block is not None:
data["fb_block"] = fb_block
if items is not None:
data["items"] = items
if style is not None:
data["style"] = style
if symbol is not None:
data["symbol"] = symbol
super().__init__(**data)
@field_validator("items", mode="after")
@classmethod
def _clean_items(cls, value: dict[str, int]) -> dict[str, int]:
"""Leere Bauteilarten ablehnen, Anzahlen <= 0 verwerfen."""
cleaned: dict[str, int] = {}
for item, count in value.items():
key = str(item).strip()
if not key:
raise ValueError("Bauteilart darf nicht leer sein")
if int(count) > 0:
cleaned[key] = int(count)
return cleaned
# -- get ---------------------------------------------------------------
def get_name(self) -> str:
"""Kurztyp, z. B. '1Sep1Swi'."""
return self.name
def get_fb_block(self) -> str:
"""Name des Siemens-Funktionsbausteins."""
return self.fb_block
def get_items(self) -> dict[str, int]:
"""Alle Bauteile als {Art: Anzahl} (Kopie - Aenderungen wirken nicht)."""
return dict(self.items)
def get_item_names(self) -> list[str]:
"""Enthaltene Bauteilarten, alphabetisch."""
return sorted(self.items)
def get_item_count(self, item: str) -> int:
"""Anzahl einer Bauteilart; 0 wenn nicht enthalten."""
return self.items.get(item, 0)
def has_item(self, item: str) -> bool:
"""Ist die Bauteilart enthalten?"""
return self.get_item_count(item) > 0
def get_item_fb(self, item: str) -> str:
"""Sub-Funktionsbaustein einer Bauteilart ('' wenn keiner)."""
return ITEM_FB.get(item, "")
def get_total_items(self) -> int:
"""Summe aller Bauteile."""
return sum(self.items.values())
def get_style(self) -> TroStyle:
"""Darstellung (Farbgruppe) dieses Typs."""
return self.style
def get_symbol(self) -> str:
"""Zeichensymbol des Markers, z. B. 'triangle'."""
return self.symbol
def get_group(self) -> str:
"""Kurzname der Farbgruppe, z. B. 'vario'."""
return self.style.group
def get_dot_attrs(self) -> dict[str, str]:
"""Graphviz-Knotenattribute dieses Typs."""
return self.style.dot_attrs()
# -- set ---------------------------------------------------------------
#
# Jede Zuweisung laeuft durch die Pydantic-Validierung, weil
# validate_assignment aktiv ist.
def set_name(self, name: str) -> None:
"""Kurztyp setzen."""
self.name = name
def set_fb_block(self, fb_block: str) -> None:
"""FB-Baustein setzen."""
self.fb_block = fb_block
def set_items(self, items: dict[str, int]) -> None:
"""Bauteilliste vollstaendig ersetzen."""
self.items = dict(items)
def set_style(self, style: TroStyle) -> None:
"""Darstellung (Farbgruppe) setzen."""
self.style = style
def set_symbol(self, symbol: str) -> None:
"""Zeichensymbol des Markers setzen."""
self.symbol = symbol
def set_item(self, item: str, count: int) -> None:
"""
Anzahl einer Bauteilart setzen.
count <= 0 entfernt das Bauteil. Unbekannte Arten sind erlaubt, damit
neue Bauteile ohne Aenderung an TroItem ergaenzt werden koennen.
"""
updated = dict(self.items)
key = str(item).strip()
if not key:
raise ValueError(f"{self.name}: Bauteilart darf nicht leer sein")
if int(count) <= 0:
updated.pop(key, None)
else:
updated[key] = int(count)
self.items = updated
def add_item(self, item: str, count: int = 1) -> None:
"""Anzahl einer Bauteilart erhoehen (oder Bauteil neu aufnehmen)."""
self.set_item(item, self.get_item_count(item) + int(count))
def remove_item(self, item: str) -> None:
"""Bauteilart entfernen."""
self.set_item(item, 0)
# -- Darstellung -------------------------------------------------------
def describe_items(self) -> str:
"""Bauteile als Text, z. B. '1x Separator, 1x Switch'."""
if not self.items:
return "-"
return ", ".join(f"{self.items[i]}x {i}" for i in self.get_item_names())
# ---------------------------------------------------------------------------
# Statische Typdefinition
# ---------------------------------------------------------------------------
#
# Quellen: doc/TRO_Typen.md (Baukasten-Analyse, Mapping-Tabelle) und
# doc/TRO_Katalog/scl_templates/*.scl (VAR-Deklarationen)
TRO_CATALOG: tuple[TroDefinition, ...] = (
# Basistyp: 1x Separator plus das Rahmenwerk, das jeder Typ besitzt
# (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState).
TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", {
TroItem.SEPARATOR: 1,
}, STYLE_SEP, TroSymbol.CIRCLE),
# laut TRO_Typen.md: Basis + 1x Switch-Modul
TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", {
TroItem.SEPARATOR: 1,
TroItem.SWITCH: 1,
}, STYLE_SWI, TroSymbol.TRIANGLE),
# laut TRO_Typen.md: Basis + 2x Switch-Modul + Scanner-Modul
TroDefinition("1Sep2Swi", "FB_ILS_MTRO_1Sep2Swi", {
TroItem.SEPARATOR: 1,
TroItem.SWITCH: 2,
TroItem.SCANNER: 1,
}, STYLE_SWI, TroSymbol.PENTAGON),
# laut TRO_Typen.md: Basis + Scanner-Modul + SSCC-Modul (Messsensor + WCS)
TroDefinition("1Sep_SSCC", "FB_ILS_MTRO_1Sep_SSCC", {
TroItem.SEPARATOR: 1,
TroItem.SCANNER: 1,
TroItem.SSCC: 1,
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE),
# laut TRO_Typen.md: Basis + Vario-Antriebsmodul
TroDefinition("Vario", "FB_ILS_MTRO_Vario", {
TroItem.SEPARATOR: 1,
TroItem.VARIO_DRIVE: 1,
}, STYLE_VARIO, TroSymbol.RECT),
# laut TRO_Typen.md: Basis (Pin-Separator) + 19x Storage-Line + Scanner
# + Encoder + StorageRemoval-Schnittstelle
TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", {
TroItem.SEPARATOR: 1,
TroItem.STORAGE_LINE: 19,
TroItem.SCANNER: 1,
TroItem.ENCODER: 1,
}, STYLE_STORE, TroSymbol.SQUARE),
# aus SCL belegt (FB_ILS_MTRO_Vario_workStation.scl, VAR ab Zeile 77):
# fbSeparator1, fbConvVario, fbSepWait, fbBarcodeReader.
# Der Vario-Antrieb ist hier die WorkStation-Variante des STRO-Bausteins.
TroDefinition("Vario_workStation", "FB_ILS_MTRO_Vario_workStation", {
TroItem.SEPARATOR: 1,
TroItem.VARIO_DRIVE: 1,
TroItem.WORK_STATION: 1,
TroItem.ACCUMULATE: 1,
TroItem.SCANNER: 1,
}, STYLE_VARIO, TroSymbol.HEXAGON),
# aus SCL belegt (FB_EmptyCarrBuffer.scl): arfbSeparatorStorage als
# Array[1..5] of FB_ILS_STRO_Sep + 5x fbCarrierWaitStore.
# Kein eigener Eingangs-Separator - der "inTro" ist ein separater 1Sep-TRO
# (in der Referenzanlage z. B. TRO101_IN neben TRO101).
TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", {
TroItem.STORAGE_LINE: 5,
TroItem.ACCUMULATE: 5,
}, STYLE_STORE, TroSymbol.DIAMOND),
# aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor,
# fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche -
# der einzige nicht additive Typ.
TroDefinition("LoadingBoom", "FB_LoadingBoom_INBOUND", {
TroItem.BOOM: 1,
TroItem.FOOT: 1,
TroItem.TILT_SENSOR: 1,
}, STYLE_EXT, TroSymbol.ARROW),
# aus SCL belegt (FB_ILS_MTRO_2Sep1Swi.scl, VAR ab Zeile 115):
# fbSeparator1, fbSeparator2, fbSwitch1, fbBarcodeReader1, fbBarcodeReader2.
# Die Mapping-Tabelle in TRO_Typen.md nennt die beiden Scanner nicht -
# hier gilt der Code.
TroDefinition("2Sep1Swi", "FB_ILS_MTRO_2Sep1Swi", {
TroItem.SEPARATOR: 2,
TroItem.SWITCH: 1,
TroItem.SCANNER: 2,
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN),
# ACHTUNG - NICHT aus SCL belegt. Alle Typen oberhalb stammen aus
# vorhandenem Code bzw. der Mapping-Tabelle in TRO_Typen.md; dieser hier
# wurde am 2026-08-12 auf Anforderung aus dem Projekt 500573 (Mubea)
# aufgenommen, um zwei Uebergabestellen am Beruehrpunkt Kreisel2/Kreisel1
# zu einem Steuerobjekt zusammenzufassen.
# Der Baustein FB_ILS_MTRO_2Sep2Swi existiert in diesem Repository nicht
# und ist auch in doc/TRO_Katalog/scl_templates/ nicht hinterlegt - er muss
# in der ILSLib angelegt werden, sonst erzeugt ein spaeterer
# SCL-Generator einen Aufruf auf einen fehlenden Baustein.
TroDefinition("2Sep2Swi", "FB_ILS_MTRO_2Sep2Swi", {
TroItem.SEPARATOR: 2,
TroItem.SWITCH: 2,
TroItem.SCANNER: 2,
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN),
)
_BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG}
def get_tro(name: str) -> TroDefinition | None:
"""Typdefinition nach Namen; None wenn unbekannt."""
return _BY_NAME.get(name)
def tro_names() -> list[str]:
"""Alle Typnamen in Katalogreihenfolge."""
return [t.get_name() for t in TRO_CATALOG]
def set_tro(definition: TroDefinition) -> None:
"""Typdefinition ergaenzen oder ersetzen (nur zur Laufzeit)."""
_BY_NAME[definition.get_name()] = definition
def catalog() -> dict[str, TroDefinition]:
"""Alle Typdefinitionen als {Name: TroDefinition} (Kopie der Zuordnung)."""
return dict(_BY_NAME)
def mermaid_classdefs() -> list[str]:
"""classDef-Zeilen fuer ein Mermaid-Diagramm, eine je Farbgruppe."""
return [style.mermaid_classdef() for style in STYLES]