# -*- 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 vier 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. Darstellung - Farbe/Gruppe fuer Zeichnungen (Graphviz, Mermaid, spaeter das BricsCAD-Symbol) 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", "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 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)", ) def __init__( self, name: str | None = None, fb_block: str | None = None, items: dict[str, int] | None = None, style: TroStyle | 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 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_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_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), # laut TRO_Typen.md: Basis + 1x Switch-Modul TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", { TroItem.SEPARATOR: 1, TroItem.SWITCH: 1, }, STYLE_SWI), # 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), # 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), # laut TRO_Typen.md: Basis + Vario-Antriebsmodul TroDefinition("Vario", "FB_ILS_MTRO_Vario", { TroItem.SEPARATOR: 1, TroItem.VARIO_DRIVE: 1, }, STYLE_VARIO), # 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), # 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), # 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), # 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), # 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), ) _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]