diff --git a/lib/tro_catalog.py b/lib/tro_catalog.py new file mode 100644 index 0000000..12215f1 --- /dev/null +++ b/lib/tro_catalog.py @@ -0,0 +1,491 @@ +# -*- 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] diff --git a/requirements.txt b/requirements.txt index a895711..1f402e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Python-Abhaengigkeiten # Installieren mit: pip install -r requirements.txt -# Beispiel-Abhaengigkeiten - anpassen nach Bedarf: -# pydantic >= 2.0.0 +pydantic >= 2.0.0 + +# Noch nicht benoetigt: # pytest >= 9.0.0