From 02db9a95b006d3e53717b74eb59f99fa96cdf6ff Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 1 Oct 2026 10:50:40 +0200 Subject: [PATCH] TRO-Fluss: tro_overrides.ini standardmaessig aus, ungelistete Strecken-Separatoren lagerichtig verketten - tro_flow/tro_annotate: cfg/tro_overrides.ini nur noch mit --use-overrides (die Abschnitte haengen an laufenden TRO-Nummern und am CSV-Namen und veralten mit jedem Export; [export.csv.connections] ersetzte alle abgeleiteten Kanten und liess TRO13-20 unverbunden) - link_tros: Separatoren mit Zuordnung auf eine Strecke, die in keiner TrackIds-Liste stehen, per Projektion AS->ES in die Strecken-Kette einsortieren (Mubea VF_3: 0037/0038 erzeugten je 9 Fehlkanten; mubea 38 -> 22 Verbindungen, HundM05 unveraendert) - tro_catalog: typ-eigene Fuellfarbe je TRO-Typ, Mermaid-classDefs je Typ - tro_overrides/tro_flow: connect.ini zurueck einlesen (--from-connect) Co-Authored-By: Claude Opus 5.5 --- lib/tro_annotate.py | 9 +- lib/tro_catalog.py | 70 ++++++++++--- lib/tro_flow.py | 227 +++++++++++++++++++++++++++++++++++++++++-- lib/tro_overrides.py | 101 +++++++++++++++++++ 4 files changed, 381 insertions(+), 26 deletions(-) diff --git a/lib/tro_annotate.py b/lib/tro_annotate.py index d5d466e..5e335aa 100644 --- a/lib/tro_annotate.py +++ b/lib/tro_annotate.py @@ -102,7 +102,7 @@ from dxf_registration import ( from material_flow import build_graph, env_dir, read_elements, resolve_input from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro from tro_flow import Analysis, analyse_tros -from tro_overrides import load_overrides +from tro_overrides import Overrides, load_overrides # --------------------------------------------------------------------------- # Konstanten @@ -782,6 +782,10 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument("--emit-lisp", action="store_true", help="cad/tro_types.lsp aus dem Typkatalog neu schreiben " "(Auswahlliste des Dialogs TROEDIT) und beenden.") + parser.add_argument("--use-overrides", action="store_true", + help="Manuelle Korrekturen aus cfg/tro_overrides.ini " + "anwenden. Standard ist AUS - wie bei tro_flow, " + "damit Marker und Flussdiagramm zusammenpassen.") return parser.parse_args(argv) @@ -834,7 +838,8 @@ def main(argv: list[str] | None = None) -> int: print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr) return 1 graph = build_graph(elements, csv_file.name, read_warnings) - overrides = load_overrides(csv_file.name, cfg_dir) + overrides = (load_overrides(csv_file.name, cfg_dir) if args.use_overrides + else Overrides()) analysis = analyse_tros(graph, elements, overrides) if not analysis.tros: print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr) diff --git a/lib/tro_catalog.py b/lib/tro_catalog.py index f7013a0..83964c8 100644 --- a/lib/tro_catalog.py +++ b/lib/tro_catalog.py @@ -231,6 +231,11 @@ class TroStyle(BaseModel): Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist violett. + + Innerhalb einer Gruppe hat jeder Typ zusaetzlich eine eigene Fuellfarbe + (TroDefinition.fill, ein Ton im Farbton der Gruppe); der Rand bleibt die + Gruppenfarbe (stroke). So ist jeder Typ an Farbe UND Form eindeutig, und + die Gruppe bleibt am Farbton/Rand lesbar. """ model_config = ConfigDict(validate_assignment=True, extra="forbid", @@ -337,6 +342,12 @@ class TroDefinition(BaseModel): min_length=1, description="Zeichensymbol des Markers im CAD, z. B. 'triangle'", ) + fill: str = Field( + default="", + description="Typ-eigene Fuellfarbe als Hex; leer = Fuellfarbe der Farbgruppe. " + "Der Rand bleibt immer in der Gruppenfarbe, damit die Gruppe " + "trotz eigener Typfarbe erkennbar bleibt.", + ) def __init__( self, @@ -345,6 +356,7 @@ class TroDefinition(BaseModel): items: dict[str, int] | None = None, style: TroStyle | None = None, symbol: str | None = None, + fill: str | None = None, **data, ) -> None: # Positionsargumente auf Feldnamen abbilden, damit die bisherige @@ -359,6 +371,8 @@ class TroDefinition(BaseModel): data["style"] = style if symbol is not None: data["symbol"] = symbol + if fill is not None: + data["fill"] = fill super().__init__(**data) @field_validator("items", mode="after") @@ -420,14 +434,32 @@ class TroDefinition(BaseModel): """Kurzname der Farbgruppe, z. B. 'vario'.""" return self.style.group + def get_fill(self) -> str: + """Fuellfarbe dieses Typs: die typ-eigene, sonst die der Farbgruppe.""" + return self.fill or self.style.fill + def get_dot_attrs(self) -> dict[str, str]: - """Graphviz-Knotenattribute dieses Typs: Fuell-/Randfarbe der Farbgruppe - plus die typ-eigene Form (siehe TroSymbol/dot_shape_attrs) - Farbe - gruppiert verwandte Typen, Form unterscheidet den einzelnen Typ.""" + """Graphviz-Knotenattribute dieses Typs: typ-eigene Fuellfarbe, Rand in + der Farbe der Gruppe, plus die typ-eigene Form (siehe TroSymbol/ + dot_shape_attrs) - Rand und Farbton gruppieren verwandte Typen, + Fuellung und Form unterscheiden den einzelnen Typ.""" attrs = self.style.dot_attrs() + attrs["fillcolor"] = self.get_fill() attrs.update(dot_shape_attrs(self.symbol)) return attrs + def mermaid_class(self) -> str: + """Mermaid-Klassenname dieses Typs (Typnamen beginnen teils mit einer + Ziffer, die Mermaid als Klassenname nicht erlaubt).""" + return f"t_{self.name}" + + def mermaid_classdef(self) -> str: + """Mermaid-classDef-Zeile fuer diesen Typ (eigene Fuellung, Gruppenrand).""" + return ( + f"classDef {self.mermaid_class()} fill:{self.get_fill()}," + f"stroke:{self.style.stroke},color:{self.style.font}" + ) + # -- set --------------------------------------------------------------- # # Jede Zuweisung laeuft durch die Pydantic-Validierung, weil @@ -453,6 +485,10 @@ class TroDefinition(BaseModel): """Zeichensymbol des Markers setzen.""" self.symbol = symbol + def set_fill(self, fill: str) -> None: + """Typ-eigene Fuellfarbe setzen ('' = Farbe der Gruppe).""" + self.fill = fill + def set_item(self, item: str, count: int) -> None: """ Anzahl einer Bauteilart setzen. @@ -503,29 +539,29 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( # (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState). TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", { TroItem.SEPARATOR: 1, - }, STYLE_SEP, TroSymbol.CIRCLE), + }, STYLE_SEP, TroSymbol.CIRCLE, "#d4e8ff"), # laut TRO_Typen.md: Basis + 1x Switch-Modul TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", { TroItem.SEPARATOR: 1, TroItem.SWITCH: 1, - }, STYLE_SWI, TroSymbol.TRIANGLE), + }, STYLE_SWI, TroSymbol.TRIANGLE, "#ffe4a0"), # 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), + }, STYLE_SWI, TroSymbol.PENTAGON, "#ffc766"), # 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), + }, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE, "#ffd4d4"), # laut TRO_Typen.md: Basis + Vario-Antriebsmodul TroDefinition("Vario", "FB_ILS_MTRO_Vario", { TroItem.SEPARATOR: 1, TroItem.VARIO_DRIVE: 1, - }, STYLE_VARIO, TroSymbol.RECT), + }, STYLE_VARIO, TroSymbol.RECT, "#e8d4f0"), # laut TRO_Typen.md: Basis (Pin-Separator) + 19x Storage-Line + Scanner # + Encoder + StorageRemoval-Schnittstelle TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", { @@ -533,7 +569,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroItem.STORAGE_LINE: 19, TroItem.SCANNER: 1, TroItem.ENCODER: 1, - }, STYLE_STORE, TroSymbol.SQUARE), + }, STYLE_STORE, TroSymbol.SQUARE, "#d4f0d4"), # 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. @@ -543,7 +579,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroItem.WORK_STATION: 1, TroItem.ACCUMULATE: 1, TroItem.SCANNER: 1, - }, STYLE_VARIO, TroSymbol.HEXAGON), + }, STYLE_VARIO, TroSymbol.HEXAGON, "#caa6e0"), # 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 @@ -551,7 +587,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", { TroItem.STORAGE_LINE: 5, TroItem.ACCUMULATE: 5, - }, STYLE_STORE, TroSymbol.DIAMOND), + }, STYLE_STORE, TroSymbol.DIAMOND, "#a6dca6"), # aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor, # fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche - # der einzige nicht additive Typ. @@ -559,7 +595,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroItem.BOOM: 1, TroItem.FOOT: 1, TroItem.TILT_SENSOR: 1, - }, STYLE_EXT, TroSymbol.ARROW), + }, STYLE_EXT, TroSymbol.ARROW, "#d9d9d9"), # 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 - @@ -568,7 +604,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroItem.SEPARATOR: 2, TroItem.SWITCH: 1, TroItem.SCANNER: 2, - }, STYLE_SWI, TroSymbol.TRIANGLE_DOWN), + }, STYLE_SWI, TroSymbol.TRIANGLE_DOWN, "#ffd6ad"), # 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) @@ -582,7 +618,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = ( TroItem.SEPARATOR: 2, TroItem.SWITCH: 2, TroItem.SCANNER: 2, - }, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN), + }, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#f2a65a"), ) _BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG} @@ -610,5 +646,7 @@ def catalog() -> dict[str, TroDefinition]: def mermaid_classdefs() -> list[str]: - """classDef-Zeilen fuer ein Mermaid-Diagramm, eine je Farbgruppe.""" - return [style.mermaid_classdef() for style in STYLES] + """classDef-Zeilen fuer ein Mermaid-Diagramm: eine je Farbgruppe (u. a. + 'ext' fuer die Systemgrenzen) plus eine je Typ (t_).""" + return ([style.mermaid_classdef() for style in STYLES] + + [definition.mermaid_classdef() for definition in TRO_CATALOG]) diff --git a/lib/tro_flow.py b/lib/tro_flow.py index a923292..bd12b7f 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -142,6 +142,7 @@ from tro_overrides import ( apply_merges, apply_overrides, apply_splits, + load_connect_ini, load_overrides, ) @@ -1013,6 +1014,62 @@ def _tro_bahn(tro: Tro, graph: Graph, sep_assignment: dict[str, str]) -> str | N return None +def _chain_mit_ungelisteten_separatoren( + strecke: Element, + chain: list[str], + elements: list[Element], + elements_by_id: dict[str, Element], +) -> list[str]: + """Separatoren der Strecke, die in KEINER TrackIds-Liste stehen, lagerichtig + in die (bereits AS -> ES orientierte) Kette einsortieren. + + Hintergrund (Mubea, VF_3): Separatoren 0037/0038 sind per Zuordnung der + Strecke 0015 zugeordnet und liegen physisch zwischen 0062 und 0063, fehlen + aber in deren TrackIds. Ohne Platz in der Kette bekommen ihre TROs keinen + praezisen Nachfolger, behalten die groben Contraction-Walk-Kanten und + zeigen auf jeden TRO hinter Kreisel2 Bahn R (je 9 Fehlkanten). + + Einsortiert wird ueber die Projektion auf die Gerade erstes -> letztes + Kettenelement. Steht ein Separator in irgendeiner TrackIds-Liste (z. B. + eines Kreisels), gehoert er dorthin und bleibt hier draussen. + """ + if len(chain) < 2: + return chain + gelistet = {tid for e in elements for tid in e.track_ids} + fehlend = [ + e for e in elements + if e.kind == "Separator" and e.assignment == strecke.teile_id + and e.teile_id not in gelistet and e.position + ] + if not fehlend: + return chain + anfang = elements_by_id.get(chain[0]) + ende = elements_by_id.get(chain[-1]) + if not (anfang and ende and anfang.position and ende.position): + return chain + dx = ende.position[0] - anfang.position[0] + dy = ende.position[1] - anfang.position[1] + if dx == 0 and dy == 0: + return chain + + def _t(pos) -> float: + return (pos[0] - anfang.position[0]) * dx + (pos[1] - anfang.position[1]) * dy + + # Kettenelemente ohne Position behalten ihren Platz: sie erben den Wert des + # Vorgaengers, damit die Sortierung stabil bleibt. + werte: list[tuple[float, int, str]] = [] + letzter = float("-inf") + for i, tid in enumerate(chain): + el = elements_by_id.get(tid) + if el is not None and el.position: + letzter = _t(el.position) + werte.append((letzter, i, tid)) + for e in fehlend: + werte.append((_t(e.position), len(chain), e.teile_id)) + werte.sort(key=lambda w: (w[0], w[1])) + return [tid for _, _, tid in werte] + + def link_tros( graph: Graph, tros: list[Tro], elements: list[Element] ) -> tuple[list[tuple[str, str]], dict[tuple[str, str], str]]: @@ -1133,6 +1190,7 @@ def link_tros( # er ist der Kreisel-Bahn zugehoerig und dort schon (Schritt 1 unten) # verkettet. Ohne diesen Ausschluss wuerde er faelschlich als erster # oder letzter "Vario" gelten (Review 2026-09-21). + chain = _chain_mit_ungelisteten_separatoren(el, chain, elements, elements_by_id) owned = [ elem_owner[tid] for tid in chain if tid in elem_owner and not ((d := by_id[elem_owner[tid]].definition()) and d.has_item(TroItem.SWITCH)) @@ -1550,7 +1608,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str # Kantenarten. Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0 # mitten in der Anlage - daher unterhalb links vom Anlagenrand. rows = "".join( - f' {d.get_name()} ' + f' {d.get_name()} ' f'{d.describe_symbol()}' for d in TRO_CATALOG ) @@ -1584,6 +1642,90 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str return "\n".join(out) + "\n" +# --------------------------------------------------------------------------- +# connect.ini (TRO-Ebene) - --connect schreibt, --from-connect liest +# --------------------------------------------------------------------------- + + +def render_connect_ini(analysis: Analysis, section: str, source: str) -> str: + """ + TRO-Topologie als connect.ini, angelehnt an doc/TRO_Katalog/connect.ini + (hmf-connect-v1). Zurueck eingelesen von tro_overrides.load_connect_ini + (--from-connect): geaenderte Typen und Verbindungen gehen dann in DOT/SVG. + + Abweichung vom Referenzformat: Systemgrenzen stehen nicht als EXTERN, + sondern mit ihrer ID (SYSn), damit die Datei verlustfrei zurueckgelesen + werden kann. [.tros] ist eine reine Uebersicht (Vorgaenger/ + Nachfolger je TRO) und wird beim Einlesen ignoriert. + """ + out: list[str] = [] + add = out.append + add("; ============================================================") + add("; connect.ini - Routing / Zusammenhalt der TROs (TRO-Ebene)") + add(f"; Automatisch erzeugt von lib/tro_flow.py aus '{source}'.") + add("; Wird bei jedem Lauf mit --connect neu geschrieben. Eine von Hand") + add("; geaenderte Fassung wird mit --from-connect gezeichnet; dauerhafte") + add("; Korrekturen gehoeren nach cfg/tro_overrides.ini.") + add(";") + add("; [.nodes] TRO = | | ") + add("; SYSn = Materialeingang/-ausgang (kein TRO)") + add("; [.connections] cNNN = -> | | ") + add("; kind: normal | transfer | bahn | weiche | umlauf | unbestimmt | gesetzt") + add("; [.externals] eNNN = SYSn -> | | (Zulauf)") + add("; eNNN = -> SYSn | | (Ablauf)") + add("; [.tros] TRO = | prev: ... | next: ...") + add("; nur Uebersicht - beim Einlesen ignoriert") + add("; ============================================================") + add("") + + add(f"[{section}.nodes]") + for tro in analysis.tros: + teile = [] + if tro.separators: + teile.append(f"Sep {', '.join(tro.separators)}") + if tro.scanners: + teile.append(f"Scan {', '.join(tro.scanners)}") + add(f"{tro.tro_id} = {tro.type_name} | {tro.label}" + + (f" | {'; '.join(teile)}" if teile else "")) + for boundary in analysis.boundaries: + add(f"{boundary.tro_id} = {boundary.type_name} | {boundary.label}") + add("") + + boundary_ids = {b.tro_id for b in analysis.boundaries} + intern = [e for e in analysis.edges + if e[0] not in boundary_ids and e[1] not in boundary_ids] + extern = [e for e in analysis.edges + if e[0] in boundary_ids or e[1] in boundary_ids] + + add(f"[{section}.connections]") + for i, (src, dst) in enumerate(intern, start=1): + kind = analysis.edge_kinds.get((src, dst)) or "normal" + add(f"c{i:03d} = {src} -> {dst} | | {kind}") + add("") + + add(f"[{section}.externals]") + if not extern and boundary_ids: + add(f"; Systemgrenzen ohne Verbindung: {', '.join(sorted(boundary_ids))}") + elif not extern: + add("; keine Be-/Entladestation im Layout - Materialein-/ausgang offen") + for i, (src, dst) in enumerate(extern, start=1): + kind = analysis.edge_kinds.get((src, dst)) or "normal" + add(f"e{i:03d} = {src} -> {dst} | | {kind}") + add("") + + add(f"[{section}.tros]") + for tro in analysis.tros: + prev = ", ".join(tro.predecessors) or "-" + nxt = ", ".join(tro.successors) or "-" + line = f"{tro.tro_id} = {tro.type_name} | prev: {prev} | next: {nxt}" + if tro.colocated: + line += f" | gleiche Bahn: {', '.join(tro.colocated)}" + add(line) + add("") + + return "\n".join(out) + "\n" + + # --------------------------------------------------------------------------- # Fehlerdatei # --------------------------------------------------------------------------- @@ -1669,9 +1811,9 @@ def _tro_mermaid(analysis: Analysis) -> list[str]: for tro in analysis.tros: definition = tro.definition() - group = definition.get_group() if definition else "ext" + css_class = definition.mermaid_class() if definition else "ext" label = f"{tro.tro_id}
{tro.type_name}
{_mermaid_text(tro.label)}" - out.append(f' {tro.tro_id}["{label}"]:::{group}') + out.append(f' {tro.tro_id}["{label}"]:::{css_class}') for boundary in analysis.boundaries: label = f"{boundary.tro_id}
{boundary.type_name}
{_mermaid_text(boundary.label)}" @@ -1921,12 +2063,12 @@ def render_doc( add(f"Typen, FB-Bausteine, Bauteile und Farben stammen vollstaendig aus " f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):") add("") - add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol |") - add("|---:|---|---|---|---|---|") + add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol | Fuellfarbe |") + add("|---:|---|---|---|---|---|---|") for index, definition in enumerate(TRO_CATALOG, start=1): add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` " f"| {definition.describe_items()} | {definition.get_group()} " - f"| {definition.describe_symbol()} |") + f"| {definition.describe_symbol()} | `{definition.get_fill()}` |") add("") add("| Bauteil | Sub-FB |") add("|---|---|") @@ -2085,6 +2227,25 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: help="Dokumentation der TROs als Markdown in %%SKEL_RESULTS%% erzeugen " "(TRO-Liste, Verbindungen, Typdefinition).", ) + parser.add_argument( + "--connect", + action="store_true", + help="TRO-Topologie als _tro_connect.ini nach %%SKEL_RESULTS%% " + "schreiben: Knoten (TRO = Typ), Verbindungen, Systemgrenzen und je " + "TRO Vorgaenger/Nachfolger.", + ) + parser.add_argument( + "--from-connect", + dest="from_connect", + nargs="?", + const="", + default=None, + metavar="INI", + help="Typen und Verbindungen aus einer (von Hand geaenderten) connect.ini " + "uebernehmen und damit DOT/SVG/Doku erzeugen. Ohne Wert: " + "_tro_connect.ini in %%SKEL_RESULTS%%. Die connect.ini wird " + "dabei nicht ueberschrieben.", + ) parser.add_argument( "--geometry", action="store_true", @@ -2102,6 +2263,21 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: "Anschlusspunkte als derselbe Uebergabepunkt gelten. " "Standard: %(default)s", ) + parser.add_argument( + "--use-overrides", + dest="use_overrides", + action="store_true", + help="Manuelle Korrekturen aus cfg/tro_overrides.ini anwenden. Standard " + "ist AUS: TROs, Typen und Verbindungen kommen rein aus dem Layout " + "(die INI-Abschnitte haengen an laufenden TRO-Nummern und veralten " + "mit jedem neuen Export).", + ) + parser.add_argument( + "--no-overrides", + dest="use_overrides", + action="store_false", + help=argparse.SUPPRESS, + ) return parser.parse_args(argv) @@ -2165,7 +2341,28 @@ def main(argv: list[str] | None = None) -> int: if raw_json_file is not None: sepliste_counts = load_sepliste_counts(raw_json_file, warnings) - overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg")) + if args.use_overrides: + overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg")) + else: + overrides = Overrides() + + stem = csv_file.stem + ("_geo" if args.geometry else "") + connect_target = results / f"{stem}_tro_connect.ini" + connect_source: Path | None = None + if args.from_connect is not None: + connect_source = Path(args.from_connect) if args.from_connect else connect_target + if not connect_source.is_absolute() and connect_source.parent == Path("."): + connect_source = results / connect_source + if not connect_source.is_file(): + print(f"FEHLER: connect.ini nicht gefunden: {connect_source}", file=sys.stderr) + print(" Erst mit --connect erzeugen.", file=sys.stderr) + return 1 + try: + overrides = load_connect_ini(connect_source, overrides) + except ValueError as exc: + print(f"FEHLER: {exc}", file=sys.stderr) + return 1 + analysis = analyse_tros(graph, elements, overrides, resolve_host=resolve_host, count_exits=count_exits, method=method, sepliste_counts=sepliste_counts) @@ -2174,7 +2371,6 @@ def main(argv: list[str] | None = None) -> int: f"ableitbar.", file=sys.stderr) return 1 - stem = csv_file.stem + ("_geo" if args.geometry else "") dot_file = results / f"{stem}_tro_flow.dot" svg_file = results / f"{stem}_tro_flow.svg" doc_target = results / f"{stem}_tro_doc.md" @@ -2194,6 +2390,14 @@ def main(argv: list[str] | None = None) -> int: encoding="utf-8", ) + connect_file = None + if args.connect and connect_source is None: + connect_target.write_text( + render_connect_ini(analysis, section=stem, source=csv_file.name), + encoding="utf-8", + ) + connect_file = connect_target + has_errors = write_error_file(analysis, csv_file, error_target) error_file = error_target if has_errors else None @@ -2217,6 +2421,13 @@ def main(argv: list[str] | None = None) -> int: report(analysis, csv_file, dot_file, created_svg, error_file, doc_file, warnings, engine=engine) + if connect_file: + print(f"connect.ini = {connect_file}") + if connect_source: + print(f"Gezeichnet aus = {connect_source}") + if args.connect and connect_source is not None: + print("HINWEIS: --connect zusammen mit --from-connect - die connect.ini " + "wird nicht ueberschrieben.") if svg_error: print(f"FEHLER: SVG nicht erzeugt: {svg_error}", file=sys.stderr) diff --git a/lib/tro_overrides.py b/lib/tro_overrides.py index eb9c891..3a72018 100644 --- a/lib/tro_overrides.py +++ b/lib/tro_overrides.py @@ -490,3 +490,104 @@ def apply_overrides(tros, overrides: Overrides) -> None: overrides.applied.append( f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})" ) + + +# --------------------------------------------------------------------------- +# connect.ini (TRO-Ebene) zurueck einlesen - tro_flow.py --from-connect +# --------------------------------------------------------------------------- + +# Vertrauensangabe fuer Typen, die aus einer (evtl. von Hand geaenderten) +# connect.ini stammen - in der Doku von Layout und Begehung unterscheidbar. +CONNECT_CONFIDENCE = "connect.ini" + + +def _connect_pair(raw: str) -> tuple[str, str, str] | None: + """' -> | | ' -> (von, nach, kind); None wenn + kein Pfeil. Die JamArea wird (noch) nicht ausgewertet.""" + pfeil, _, rest = str(raw).partition("|") + src, sep, dst = (t.strip() for t in pfeil.partition("->")) + if not sep or not src or not dst: + return None + felder = [f.strip() for f in rest.split("|")] + kind = felder[1] if len(felder) > 1 else "" + return src, dst, kind + + +def load_connect_ini(path: Path, base: Overrides | None = None) -> Overrides: + """ + Eine von tro_flow.py --connect geschriebene connect.ini als Korrekturen lesen. + + Die Datei wird dabei nicht als eigenes Modell behandelt, sondern auf die + vorhandenen Korrekturarten abgebildet - so laeuft sie durch dieselbe + Pipeline wie cfg/tro_overrides.ini: + + [.nodes] TRO-ID = | ... -> Typkorrektur je TRO-ID + [.connections] cNNN = von -> nach | JamArea | kind + [.externals] eNNN = SYSn -> TRO | ... bzw. TRO -> SYSn | ... + -> zusammen die gesetzte Topologie, die die + abgeleiteten Kanten vollstaendig ersetzt + + [.tros] (Vorgaenger/Nachfolger je TRO) ist nur eine Uebersicht und + wird ignoriert - massgeblich sind die Verbindungen. + + `base` (die Korrekturen aus cfg/tro_overrides.ini) bleibt erhalten; Typen + aus der connect.ini gewinnen, die Topologie der connect.ini ersetzt eine + dort gesetzte. + """ + result = base if base is not None else Overrides() + parser = configparser.ConfigParser(inline_comment_prefixes=(";",), + interpolation=None) + parser.optionxform = str # TRO-IDs nicht in Kleinschreibung wandeln + # utf-8-sig: von Hand gespeicherte Dateien (Windows-Editoren, PowerShell) + # tragen oft ein BOM, an dem configparser sonst in Zeile 1 scheitert. + try: + parser.read(path, encoding="utf-8-sig") + except configparser.Error as exc: + raise ValueError(f"{path.name} nicht lesbar: {exc}") from exc + + connections: list[ConnectionSpec] = [] + for section in parser.sections(): + art = section.rsplit(".", 1)[-1].lower() + values = parser[section] + if art == "nodes": + for tro_id, raw in values.items(): + type_name = str(raw).partition("|")[0].strip() + # Systemgrenzen (SYSn) sind kein TRO, ihr "Typ" ist nur Anzeige + if tro_id.upper().startswith("SYS") or not type_name: + continue + if get_tro(type_name) is None: + result.warnings.append( + f"{path.name} [{section}] {tro_id}: Typ '{type_name}' steht " + f"nicht im TRO-Katalog - Typ bleibt wie abgeleitet" + ) + continue + result.tros[tro_id.upper()] = TroOverride( + key=tro_id.upper(), type_name=type_name, + reason=f"aus {path.name}", confidence=CONNECT_CONFIDENCE, + ) + elif art in ("connections", "externals"): + for key, raw in values.items(): + pair = _connect_pair(raw) + if pair is None: + result.warnings.append( + f"{path.name} [{section}] {key}: '{raw}' ist keine " + f"Verbindung ' -> '" + ) + continue + src, dst, kind = pair + if "EXTERN" in (src.upper(), dst.upper()): + result.warnings.append( + f"{path.name} [{section}] {key}: EXTERN wird nicht " + f"ausgewertet - Systemgrenze als SYSn angeben" + ) + continue + connections.append(ConnectionSpec(src=src, dst=dst, kind=kind)) + + if connections: + result.connections = connections + else: + result.warnings.append( + f"{path.name}: keine Verbindungen gefunden - Topologie bleibt wie abgeleitet" + ) + result.applied.append(f"connect.ini eingelesen: {path}") + return result