# Python Scripts — CLI Tooling Overview **As of:** 2026-10-10 `lib/` currently holds eleven Python modules (six of them runnable CLI tools, five supporting libraries), plus one documentation-generator script that lives under `doc/TRO_Katalog/tro_graphs/`. Together the CLI tools turn a CSV export of the mechanical layout (ILS 2.0) into a material-flow graph, a derived TRO list/diagram, a TRO-JSON (shared model `lib/model.py`), an `FB_Main` SCL skeleton generated from that JSON, and an annotated copy of the BricsCAD drawing for visual checking. `e_planning.py` bridges to the electrical planning: it assigns the sensors/valves of the E-planning drawing to the layout parts, so the skeleton can carry their real names. This document is the practical "what does each script do and which switches does it take" reference. For the underlying domain model (TRO types, JSON layout concept, I/O-list analysis) see `TRO_Typen.md`, `Json_Layout-Konzept.md`, `EA-Listen-Analyse.md`, `BricsCAD_TRO_Symbol.md` and `doc/TRO_Katalog/TRO_Katalog.md`. ## 1. How the pieces fit together ``` CSV export (in %SKEL_DATA%) │ ▼ material_flow.py ──► _material_flow.dot / .svg / .md │ (read_elements, build_graph — reused by tro_flow.py) ▼ tro_flow.py ──► _tro_flow.dot / .svg, _tro_doc.md, _tro_errors.md │ (derives TROs, using the type catalog in tro_catalog.py) │ ├── --json ──► _tro.json (model.TroDocument — MAIN PATH) │ │ │ ▼ │ scl_skeleton.py ──► _FB_Main.scl │ (validates the JSON against model.py; one REGION per TRO; │ E-planning values left as TODO(E-Planung) gaps) │ ▲ │ │ --eplan _eplan.json (optional) │ e_planning.py ◄── _positionsdraw.json (kabellaengen) │ (assigns BG/MB of the E-planning drawing to the layout │ parts by position + numbering rule; see §5c) │ └── same derivation ──► tro_annotate.py ──► _annotated.dxf (VIEWER ONLY) (also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF coordinate offset and tro_catalog.py for marker shape/color per TRO type) parked: tro_extract.py drawing ──► JSON (not extended; see §5a) ``` The drawing is a **view** for checking the result, not a data source: the TRO-JSON is written by `tro_flow.py --json` straight from the CSV, and `tro_annotate.py` derives exactly the same TROs to draw them. - `material_flow.py` models the material flow of the **mechanical** objects only (Gefällestrecke, Strecke, Kreisel). - `tro_flow.py` is its sister program: it derives the **control objects** (TROs) from the same graph and draws their flow instead. - `tro_annotate.py` takes the TROs from `tro_flow.py` and burns them into a **copy** of the BricsCAD drawing as attributed blocks (never touches the original). Supporting libraries (no CLI of their own, imported by the tools above): - **`lib/tro_catalog.py`** — the single source of truth for TRO types: short name, Siemens FB block, sub-components and counts, color group, CAD marker shape. - **`lib/model.py`** — the shared pydantic model of TRO *instances* and of the TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`, `TroDocument`). Written by `tro_flow.py --json`, read and validated by `scl_skeleton.py`. See §6b. - **`lib/dxf_registration.py`** — derives, verifies and persists the rigid (rotation + offset) transform between the CSV export's coordinate system and the DXF drawing's. - **`lib/tro_ids.py`** — the per-plant ID file `%SKEL_CFG%\_tro_ids.ini`: reads it, finds each TRO again by the position of its separators, assigns new IDs, writes/extends the file. Used by `tro_flow.py` (read and write) and `tro_annotate.py` (read only). See §4, "TRO-IDs". - **`lib/tro_overrides.py`** — reads the hand-maintained `%SKEL_CFG%\tro_overrides.ini`; only `material_flow.py --connect` still uses it. See §6a. Separate, documentation-only tool (not part of the generator pipeline): - **`doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py`** — regenerates the schematic per-type SVGs, the edge-type legend and the per-controller (UH01–UH05) topology graphs embedded in `TRO_Katalog.md`, built from `doc/TRO_Katalog/connect.ini`. ## 2. Running the scripts Each CLI tool is invoked through its `bin/.bat` (Windows) / `bin/.sh` (Linux/macOS) wrapper, which: 1. calls `setenv` to set `SPS_SKEL`, `SKEL_DATA`, `SKEL_RESULTS`, `SKEL_CFG`, `SKEL_LIB` and prepend `SKEL_LIB` to `PYTHONPATH`; 2. activates `.venv` if it exists (`bin/install_py` creates it); 3. runs the module with `py` (falling back to `python`). ```bat bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json bin\scl_skeleton.bat --json mubea_tro.json bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json bin\scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend ``` (`--use-cords` lays both graphs out at the real plant coordinates; drop it for Graphviz's computed left-to-right arrangement.) Conventions shared by the CLI tools: - `--file` / `--dxf` accept either a bare filename — resolved against `%SKEL_DATA%` — or a full path. - All generated output is written to `%SKEL_RESULTS%`. - `tro_annotate.py` and `e_planning.py` additionally persist the verified coordinate offset (CSV↔DXF, CSV↔E-planning drawing) to `%SKEL_CFG%\dxf_registration.json`. - Every run prints a `====` bordered report to stdout (counts, warnings, output file paths); non-fatal findings are printed as `?`/`!` lines and also do not stop the run. - Requires the packages pinned in `requirements.txt` (`pydantic>=2.0.0`; `ezdxf>=1.4.0` for `tro_annotate.py` only) — run `bin\install_py.bat` once. - `--tosvg` needs Graphviz's `dot` (or `neato`, for `tro_flow.py --use-cords`) on `PATH`, or the `GRAPHVIZ_DOT` environment variable pointing at the `dot` executable. `bin/material_flow.bat`/`.sh` and `bin/tro_flow.bat`/`.sh` check for the right engine up front when `--tosvg` is passed and print an early `WARNUNG` if it's missing (`GRAPHVIZ_DOT` only covers `dot`, not `neato`) — this is just a heads-up before the CSV is even parsed; the Python script still runs and reports the same failure in detail (exit code `2`) if the SVG step is reached. ## 3. `material_flow.py` — material-flow graph of the mechanical layout Reads the CSV export and builds a directed graph of the mechanical objects: - **Gefällestrecke** / **Strecke** (conveyor segments) → one node each. - **Kreisel** (rotary/turntable) → two nodes (`-L` / `-R`), linked by the `Drehrichtung` feature (`UZS` = clockwise = right→left, `GUZ` = counter-clockwise = left→right). - **Separator** / **Scanner** → no node of their own; shown via `Zuordnung` (assignment) on the label of their owning node. - **`TrackIds`** (added Sept. 2026): a Kreisel/Strecke/Gefällestrecke row lists the `TeileId`s of every fine element (Separator, Scanner, Weiche, Ein-/Ausschleuselement, BTMT Be-/Entladung) that belongs to it. `Zuordnung` on the fine element itself stays authoritative whenever it resolves to a real node — it's more precise than `TrackIds` for a separator sitting right at a Kreisel↔Strecke boundary, which `TrackIds` lists under *both* neighbouring containers (normal, not a conflict). `TrackIds` only fills gaps `Zuordnung` can't resolve: a bare Kreisel id with no lane suffix (attached to *both* lanes then), a missing/empty `Zuordnung`, or (for BTMT stations, which never had a reliable `Zuordnung`) the nearest-neighbour distance guess this replaces. An older export without the `TrackIds` column behaves exactly as before. See `_build_track_owners`/ `_track_owner`/`_attach_devices`/`_attach_stations` in `lib/material_flow.py`. - **Einschleuselement** / **Ausschleuselement** / **Weiche** (from the richer export) → no node of their own; they supply the **direction** of the Kreisel↔Strecke edges (`Ausschleusung` = Kreisel→Strecke at the strecke's *Anfang*, `Einschleusung` = Strecke→Kreisel at its *Ende*, read from their `Kreisel`/`Strecke`/`Streckenende` `Merkmale`) and **mark the Weiche** on a Kreisel↔Kreisel link. A strecke whose two ends touch the *same* Kreisel (return loop) is split across that Kreisel's two lanes. This makes Kreisel↔Strecke direction deterministic instead of height-inferred. - **BTMT Be-/Entladestation** (entry/exit points) → one node each, recognised by `TeileArt`/`Bezeichnung` containing `Beladung`/`BTMT-Beladung`/`Eingang` (→ **entry**, a material *source*) or `Entladung`/`SC_Entladung`/`Abwurf`/`Ausgang` (→ **exit**, a *sink*). Some exports name these same entry/exit points plainly `Eingang`/`Ausgang` instead of the BTMT-specific terms — same role, recognised the same way. An entry gets an outgoing edge, an exit an incoming edge, to the object its owning Kreisel/Strecke names it under in `TrackIds` (a Kreisel picks the Eingangs-/ Ausgangsbahn from `Drehrichtung`, same as `_weiche_lane_roles`); failing that, to the object named in its own `Nachbarn` (ignoring another station — BTMT `Nachbarn` often just names nearby separators or the paired station, not a real flow target); failing that, the nearest Kreisel, then the nearest object by 2-D distance, each with a note printed. Recognition is **sps_skel-side only** — nothing in dxfmakros changes; it simply activates when such a row appears in the CSV. > **Note on BTMT / entry-exit data.** dxfmakros' `EXPORTCSV` currently does **not** > emit the BTMT stations (their block names match no export pattern; documented as > *"Export noch offen"*). Until that is addressed, a freshly exported `mubea.csv` > will contain the 61 conveyor items but **no** BTMT rows, so no entry/exit nodes > appear. The recognition above is ready for when BTMT rows are present. In the rendered graph, **parallel Gefällestrecken between the same two Kreisel lanes with the same slope** (`Hoehe_Von_mm`/`Hoehe_Bis_mm`) — a typical line-storage bank — are drawn as a **single** Gefällestrecke-styled box with a double border (`peripheries=2`) instead of one box per line. Its label states the id range and count of the merged lines plus the combined Separator/Scanner id ranges, e.g. `Gefaellestrecke 0017 - 0036 (20x)` / `Separator (20): 0037 - 0056`. This is purely a rendering simplification (`_group_parallel_gefaelle` in `lib/material_flow.py`) — the underlying graph, `--doc` report and `--connect` output are unaffected and still list every line individually. Flow direction between a Kreisel and a Strecke/Gefällestrecke is set from the **Ausschleus-/Einschleuselement** at each end (`_apply_transfers`): the Ausschleuselement is the Kreisel outlet (Kreisel → Strecke, start), the Einschleuselement the inlet (Strecke → Kreisel, end). A conveyor whose two ends dock on the *same* Kreisel (a return line — Eingang and Ausgang on one Kreisel) is drawn as **two** edges — one onto each lane (`0006-L → VF` on the way out, `VF → 0006-R` on the way back) — so it is never flat/undetermined. The transfer element names its conveyor via the `Strecke` **or** `Foerderer`/`Förderer` attribute (both are read). Where no transfer element resolves the direction, `drive_uphill` applies the physics rule: **only a pure Gefällestrecke** takes its direction from the height (`Hoehe_Von_mm`/`Hoehe_Bis_mm` → flows downhill; a GF with no height runs downhill by definition). A Förderer (VF) can convey up *or* down and a `Strecke` is often a mixed GF/VF chain, so **height is not a direction indicator for them** — they use an unambiguous `Antriebfahrtrichtung` (exactly `"Auf"`/`"Ab"`, not a mixed list like `"horizontal,horizontal,Auf"`) and otherwise stay undetermined (direction is expected to come from the AS/ES ends). Only what nothing resolves is drawn bidirectional and reported as a warning. Entry/exit stations are hung on the Kreisel **lane** they belong to (`_attach_stations`): first the Kreisel whose `TrackIds` umlauf lists the station, then the entry-/exit-lane from its `Drehrichtung` (UZS → entry lane R / exit lane L, GUZ reversed). Sources (`Beladung`/`Eingang`) get `Station → lane`, sinks (`Entladung`/`Ausgang`) `lane → Station`. Neighbours that aren't flow objects (another station, a separator) are skipped, so two BTMT points no longer get wired to each other. `Eingang`/`Ausgang` are the **generic symbols** for the concrete BTMT stations `Beladung`/`Entladung` (same source/sink role). When a symbol lies almost on top of a BTMT station of the same role (≤ `STATION_MERGE_MM` = 600 mm, `x`/`y` from the `Position` column), only **one** node is drawn: the concrete BTMT station is kept, the generic symbol is swallowed (`_merge_station_symbols`). A standalone `Eingang`/`Ausgang` with no nearby BTMT partner is drawn as its own green/red node. `--doc` additionally cross-checks stated `Anzahl_Separator`/`Anzahl_Scanner` counts against what was actually attached. `--track` builds a second, finer-grained graph in addition to the one above: Separator, Ausschleus-/Einschleuselement, Weiche and the entry/exit points (BTMT Be-/Entladung, Eingang/Ausgang) each become their own node (they supply no node in the graph described above), showing the actual branch/merge/crossing topology between Kreisel rather than just the mechanical objects. The ordering comes **directly from the `TrackIds` CSV column** (`Element.track_ids`): every container — Kreisel, Strecke, Gefällestrecke — carries the ordered list of the track elements lying on it, computed by the dxfmakros export (`dxfmakros/doc/csv-export.md`). `build_track_graph` draws one directed chain per container from that list: - **Kreisel** → the circular umlauf in `Drehrichtung` order (the list is already angle-sorted); consecutive entries are linked and the last closes back onto the first (a real, closed loop). Edges are drawn light-blue ("Umlauf"). - **Strecke / Gefällestrecke** → the linear chain `AS → SEP… → ES` in build order, oriented by its ends (Ausschleuselement = start, Einschleuselement = end). Edges are drawn blue ("Bahn"). - An element listed in **two** containers (an AS/ES sits in its Strecke *and* in the docked Kreisel's umlauf; a Kreisel-Kreisel Weiche sits in both Kreisel) is the **same node** and stitches the containers together automatically — no `Nachbarn`-based role/feedback heuristic any more. - A track element that appears in **no** `TrackIds` list (e.g. a free Separator the export didn't chain) is hung off a dashed **placeholder** for its `Zuordnung` with an "unbestimmt" edge and a warning — order on that Bahn left open. If the CSV has no `TrackIds` column at all (older export), every track element falls into this case. See `doc/track.md` for the fuller derivation notes. | Switch | Argument | Default | Meaning | |---|---|---|---| | `--file` | `NAME` | `export.csv` | CSV input file, resolved against `%SKEL_DATA%`, or a full path. | | `--tosvg` | — | off | Also render the `.dot` file to `.svg` via Graphviz `dot`. | | `--use-cords` (alias `--use-coords`) | — | off | Place every node at its plant coordinate and render with `neato -n` (fixed positions) instead of `dot`, so the material graph matches the plant layout — the same option `tro_flow.py` has. The two Kreisel lanes (`-L`/`-R`) are nudged apart by `LANE_NUDGE_MM`; scale is `COORD_SCALE` (0.25 pt/mm, identical to `tro_flow.py`). Only has an effect together with `--tosvg`; fails (exit 1) if any flow node has no coordinate. | | `--doc` | — | off | Also write a Markdown report (`_material_flow.md`) with object list, connections and plausibility findings. | | `--connect` | — | off | Also write the topology as `_connect.ini` (item/flow level): `[…nodes]` (Kreisel lanes, Strecke, Gefällestrecke), `[…connections]` with a `kind` per edge (`normal`/`umlauf`/`weiche`/`einschleusung`/`ausschleusung`), and `[…externals]` — the BTMT entry/exit if present, otherwise the open lane-ends listed as entry candidates. A hand-editable bridge into the TRO step, styled after `doc/TRO_Katalog/connect.ini`. | | `--show-bbs [flow\|all]` | `flow` | off | Also write `_bbs.svg`: a plain 2-D top-down view of every CSV row (not just flow nodes — Separator/Scanner/Ein-/Ausschleuselement too), one axis-aligned rectangle per row from the `Position` column (its center, X/Y) and the `Boundingbox` column (its width/depth, X/Y — the Z extent is ignored), with the row's `Bezeichnung` centered in it. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (`_kreisel_lane_rects()` — short axis divided in half, `Zuordnung`-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with `--show-kx`) still belong to the Kreisel as a whole and are drawn once, not per half. No rotation, no Graphviz — hand-built SVG; the `viewBox` stays in plant millimetres (Y flipped so plant "up" renders as screen "up") but the printed page size is scaled to DIN A3 (landscape or portrait, whichever matches the layout's aspect ratio) instead of the plant's real-world extent. Larger objects are drawn first so small ones (Separator, Scanner, …) stay visible on top. A legend column right of the plant lists every drawn kind with its row count, and each rectangle carries a `` tooltip (row, TeileId, TeileArt, name). Mode `flow` (default, also bare `--show-bbs`) skips the VF/GF chain detail rows exported by dxfmakros since Sept. 2026 (`Bogen`, `Streckenelement`, `Motorstation`, `Umlenkstation` — `BBS_DETAIL_KINDS`), because they fully overlap their Strecke/Gefällestrecke box; `all` draws every row, detail kinds in their own stronger colours. dxfmakros calls this via `bin/show_exportcsv.bat` (mode `all`). | | `--show-kx` | — | off | Only valid together with `--show-bbs` (else exit 1). Additionally draw each object's connection coordinate systems K1–K4 into the `_bbs.svg`: the K1–K4 columns are 12-char base64 strings (three 24-bit fixed-point values `x,y,z`, factor 10, `csv:trans-encode` in `dxfmakros/Lisp/export.lsp`) decoded to a plant-mm X/Y. Each present K-point is drawn as a small red cross (two lines) with a small `K1`…`K4` label, and consecutive points are joined `K1→K2→K3→K4` by a blue line. Empty K columns are skipped, so an object with only K1/K2 gets two crosses and one link. | | `--track` | — | off | Additionally write the finer Separator/Ausschleus/Einschleus/Weiche graph described above (`<name>_track.dot`, and `<name>_track.svg` with `--tosvg`). Does not change the normal `_material_flow.*` output. See `doc/track.md`. | | `--chain-graph` | — | off | Write a "chain-/Lageplan" of the **grobe Objekte** (big items): every Kreisel split into its Bahn L/R halves (`_kreisel_lane_rects()`, same split as `--show-bbs`), every Strecke/Gefällestrecke ("VF"/"GF"), and every Ein-/Ausgabestation (BTMT `Beladung`/`Entladung`, or the generic `Eingang`/`Ausgang` symbol where no BTMT station absorbed it — see `_merge_station_symbols()`) — the plant runs as a closed loop, so these stations are its only material sources/sinks and are drawn like any other node, not skipped. Connected by the already-resolved top-level `graph.edges` (material flow, Kreisel-Umlauf, unresolved). Rendering method and output file are chosen by `--graphtype`. | | `--graphtype` | `coarsebox`\|`compound`\|`subgraph` | `coarsebox` | Only valid together with `--chain-graph` (else exit 1). `coarsebox`: containers only (as above), to real scale, hand-built `<name>_chain.svg`, no Graphviz — the original behaviour. `compound`: the same to-scale picture, **plus** each container's Anbauteile (Separator, Scanner, Ausschleus-/Einschleuselement, Weiche) drawn nested inside it at their real position (`_container_children()` reads them off `Element.track_ids`, the same source `build_track_graph()`/`--track` uses — not a re-derivation from `Nachbarn` like the old `data/gen_chain_svg.py` prototype), linked by the already-resolved `TrackGraph.edges`; still `<name>_chain.svg`, no Graphviz. `subgraph`: the same containment expressed as **nested Graphviz clusters** (`subgraph cluster_*` — one per Kreisel, nested one per Bahn L/R, plus one per Strecke/Gefällestrecke, each containing its Anbauteile as child nodes) — schematic (`dot` auto-layout), **not** to scale; writes `<name>_chain.dot` always, and `<name>_chain.svg` additionally with `--tosvg` (needs Graphviz `dot`, unlike the other two graph types). | **Output** (in `%SKEL_RESULTS%`): `<name>_material_flow.dot` (always, with `pos` attributes when `--use-cords` was used), `<name>_material_flow.svg` (with `--tosvg`), `<name>_material_flow.md` (with `--doc`), `<name>_connect.ini` (with `--connect`), `<name>_bbs.svg` (with `--show-bbs`), `<name>_track.dot` / `<name>_track.svg` (with `--track`), `<name>_chain.svg` (with `--chain-graph --graphtype coarsebox|compound`, or `--graphtype subgraph --tosvg`), `<name>_chain.dot` (with `--chain-graph --graphtype subgraph`). **Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed (Graphviz). ## 4. `tro_flow.py` — derive and draw the TROs Sister program to `material_flow.py` — same `--file`/`--tosvg`/`--doc` switches, but instead of the mechanical objects it derives the **TROs** (Transfer Route Objects, i.e. control-logic units) from the same graph and draws *their* flow. Derivation rules (see `doc/500573_Mubea/TRO_Identifikation_500573.md` for the source analysis): | Host of the separator | TRO type | |---|---| | Gefällestrecke, ≥ 2 lines sharing an in-/outfeed | `PinStore_Auto` (one block per line group) | | Gefällestrecke, single line | `1Sep` | | Strecke (driven) | `Vario` (one per segment) | | Kreisel lane | `1Sep` | | — additionally: 2 or 3 outgoing paths | upgraded to `1Sep1Swi` / `1Sep2Swi` | A scanner mounted on a separator does **not** change the type (stays `1Sep`); the `1Sep_SSCC` special case can't be recognized from the mechanical layout alone and is only raised as a finding. TRO types, FB blocks, sub-components and colors all come from `lib/tro_catalog.py` — nothing is hard-coded here. | Switch | Argument | Default | Meaning | |---|---|---|---| | `--file` | `NAME` | `export.csv` | CSV input file, resolved against `%SKEL_DATA%`, or a full path. | | `--tosvg` | — | off | Also render the TRO flow diagram to `.svg`. | | `--use-cords` (alias `--use-coords`) | — | off | Place every TRO at the centroid of the components it was built from and render with `neato -n` (fixed positions) instead of `dot`'s computed layout, so the diagram matches the plant layout. Only has an effect together with `--tosvg`; fails if any TRO has no coordinate. | | `--doc` | — | off | Also write a Markdown report (`<name>_tro_doc.md`) with the TRO list, connections, findings and the full type catalog. | | `--connect` | — | off | Also write the TRO topology as `<name>_tro_connect.ini` (nodes, connections, system boundaries, predecessors/successors per TRO). Pure output, never read back. | | `--true-scale` | — | off | Only with `--use-cords`: draw to scale. By default empty stretches longer than 1500 mm between nodes are shortened (order and neighbourhood are kept). | | `--json` | — | off | Also write the TRO-JSON `<name>_tro.json` (schema `sps_skel-tro-v4`, model `TroDocument` in `lib/model.py`): TROs with their `slots`, system boundaries (`SYSn`), connections with their kind, `jamAreas`, findings. Input for `scl_skeleton.py`. | | `--ids` | `DATEI` | `<name>_tro_ids.ini` | ID file of the plant (name in `%SKEL_CFG%`, or a path). Use it to run another export of the same plant against the same IDs, e.g. `--file mubea_export.csv --ids mubea_tro_ids.ini`. | | `--renumber` | — | off | Number all TROs afresh along the material flow and rewrite the `[tro]` section of the ID file (early project phase). Renamed IDs are lost; other sections of the file are kept. | | `--id-tolerance` | `MM` (float) | `100.0` | Maximum distance between an ID-file entry and a separator of a TRO for the TRO to count as found again. | TROs, types and connections always come from the layout alone; `cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in `lib/tro_flow.py`, not in a correction file. **TRO-IDs** (`lib/tro_ids.py`). The IDs belong to the SPS programmers and must survive a new layout export; the layout `TeileId` changes between exports, the position does not. So every run reads `%SKEL_CFG%\<name>_tro_ids.ini`: ```ini [tro] TRO01 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L | Sep 0003 K0302_1 = 10310.0, -440.1 ; renamed by hand ``` - Each entry is `ID = x, y` (mm, CSV coordinates) of the TRO's anchor separator. An entry finds the TRO with a separator within `--id-tolerance` (closest first, one entry per TRO), and that TRO keeps the ID — renamed IDs included. An ID must be unique and a valid SCL identifier; `SYSn` is reserved for system boundaries. - No file yet: the TROs are numbered along the material flow (depth-first from the loading stations over the material-flow graph, on a carousel or Strecke in `TrackIds` order) and the file is created. - A TRO not found gets the next free `TRO<n>` and is appended to `[tro]`; an entry without a TRO is reported ("weggefallen", with the TRO that now holds its separator, e.g. after two TROs merged into a `2Sep2Swi`) and stays in the file; its ID is not reused. - The console report lists found / new / dropped IDs. **Output** (in `%SKEL_RESULTS%`): `<name>_tro_flow.dot` (always, with `pos` attributes when `--use-cords` was used), `<name>_tro_flow.svg` (with `--tosvg`), `<name>_tro_doc.md` (with `--doc`), `<name>_tro.json` (with `--json`), `<name>_tro_errors.md` (only if TROs are unconnected or of an unknown type). In `%SKEL_CFG%`: `<name>_tro_ids.ini` (created, extended or, with `--renumber`, rewritten — see above). **Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed (Graphviz) · `3` TRO validation errors found (error file written). ## 5. `tro_annotate.py` — burn the TROs into a copy of the CAD drawing **Viewer only.** The drawing shows the result for a visual check in BricsCAD; it is no longer a data source for the SCL (that is `tro_flow.py --json`). Takes the CSV export, the original BricsCAD drawing (DXF) and the TROs derived with exactly the same call as `tro_flow.py` (including the separator cross-check from `<name>_raw.json`), and writes a **copy** of the drawing with: - one colored marker block per TRO, each type with its own shape (circle, triangle, square, diamond, …; see `TroSymbol` in `tro_catalog.py`) so types are distinguishable even without color; - visible attributes `ID`, `TYPE` (and `FB_BLOCK` with `--fb`); hidden attributes `ITEMS`, `CONFIDENCE`, `SEPARATORS` for downstream tooling; - optional directional flow arrows between TROs (`--flow`) and a legend (`--legend`). The original drawing is never modified. Everything this tool adds lives on its own `TRO_`-prefixed layers and carries XDATA under the app id `SPS_SKEL_TRO`; a re-run removes exactly those tagged objects (not by layer name) and rebuilds them, so hand-drawn content on the same layers survives. Once at least one marker of a given type has been annotated, the `dxfmakros` BricsCAD menu (`SSG_LIB > TRO`) can insert further markers of that type by hand — `TRO_INSERT` reuses the `TRO_SYM_<Typ>` block definition this tool created and only asks for a point, an id and (optionally) a different type; `TRO_EDIT` changes `ID`/`TYPE` on an existing marker. Neither command draws new shapes — a type with no prior annotated instance in the drawing must be run through this tool at least once first. CSV and DXF live in different coordinate systems; the offset between them is handled by `dxf_registration.py` — derived once, verified on every run, and cached per CSV/DXF filename pair in `%SKEL_CFG%\dxf_registration.json` unless overridden. | Switch | Argument | Default | Meaning | |---|---|---|---| | `--file` | `NAME` | `export.csv` | CSV export, resolved against `%SKEL_DATA%`, or a full path. | | `--dxf` | `NAME` | — | Original drawing as DXF (path, or name resolved against `%SKEL_DATA%`). Required unless `--emit-lisp` is used alone. DWG is not supported. | | `--out` | `NAME` | `<csv-stem>_annotated.dxf` | Output filename in `%SKEL_RESULTS%`. | | `--flow` | — | off | Draw directional flow arrows between the TROs. | | `--fb` | — | off | Show the Siemens FB block name as a visible attribute on the marker. | | `--legend` | — | off | Draw a legend of the color groups. | | `--marker-size` | `MM` (float) | `300.0` | Marker radius in mm. | | `--text-height` | `MM` (float) | `220.0` | Text height in mm. | | `--ids` | `DATEI` | `<csv-stem>_tro_ids.ini` | ID file as for `tro_flow.py`, **read only**: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as `tro_flow.py` would. | | `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is `0,0`. | | `--re-register` | — | off | Re-derive the coordinate offset even if a verified one is already cached in `%SKEL_CFG%`. | | `--check` | — | off | Dry run: only analyze and report, write no output file. | | `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` (the type list for the `TRO_INSERT`/`TRO_EDIT` dialogs) from `tro_catalog.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. | Layers always carry the prefix `TRO_`. Like `tro_flow.py`, it never reads `cfg/tro_overrides.ini`, so the markers always match the flow diagram. **Output** (in `%SKEL_RESULTS%`, unless `--check`): `<name>_annotated.dxf`, `<name>_registration.json` (the coordinate transform used, with its proof). **Exit codes:** `0` ok · `1` input/CLI error · `4` coordinate registration could not be proven (or its residual error exceeds 600 mm) — resolve with `--offset`. ## 5a. `tro_extract.py` — read the TROs back out of the drawing into JSON > **Parked since 2026-10-09.** The TRO-JSON for `scl_skeleton.py` now comes > from `tro_flow.py --json`. `tro_extract.py` is kept but not extended; its > output (schema `sps_skel-tro-extract-v1`) is **not** accepted by > `scl_skeleton.py` any more, which says so and asks for `tro_flow --json`. If > hand edits in the drawing are ever needed again, it should write the same > `model.TroDocument` format. The counter-direction to `tro_annotate.py`: ``` tro_annotate.py CSV + derivation -> markers in the drawing tro_extract.py drawing -> JSON ``` It reads **only the drawing**, never the CSV. So what comes back is what is actually in the DXF — including anything adjusted by hand in BricsCAD (markers moved, type changed via `TRO_EDIT`). That is the point: after a review, the drawing is the current state, not the export. It finds the objects `tro_annotate.py` tagged with XDATA (AppID `SPS_SKEL_TRO`): the `TRO_SYM_<type>` block references (attributes `ID`/`TYPE`/`ITEMS`/`CONFIDENCE`/`SEPARATORS`, plus `FB_BLOCK` when annotated with `--fb`), the flow arrows (`"<from>-><to>"`), and the open-point markers (`"OFFEN:<name>"`). Each TRO also gets `predecessors`/`successors` computed from the arrows. Coordinates appear twice: `positionDxf` is the drawing coordinate, `position` the plant coordinate. The conversion uses the registration written by `tro_annotate.py` (`<stem>_registration.json`; the `_annotated` suffix and the file's own `dxf_file` field are both taken into account when locating it). If no registration is found, `position` is **left out rather than guessed**, and a warning says so. Switches: `--dxf NAME` (default `export_annotated.dxf`, looked up in `%SKEL_RESULTS%`), `--out NAME` (default `<drawing>_tro.json`). Exit codes: `0` ok, `1` input error, `2` no TRO markers in the drawing. `fbType` is worth one note: unless the drawing was annotated with `--fb`, the FB block is not an attribute in the DXF. It is then looked up from `tro_catalog.py` and marked `"fbTypeFrom": "tro_catalog"` — so a consumer can tell a value that came out of the drawing from one that was resolved. ## 5b. `scl_skeleton.py` — generate the FB_Main skeleton as SCL Last step of the chain: ``` material_flow.py objects -> material flow tro_flow.py flow -> TROs -> <name>_tro.json (--json) scl_skeleton.py JSON -> FB_Main.scl ``` The JSON is read through `model.TroDocument` and **validated**: an unknown or renamed key, or `items`/`fbType` that do not match the TRO type in `tro_catalog.py`, stops the run with exit code `1` and the validation error — no SCL is written. A file with a different schema (e.g. the old `tro_extract.py` output) gets a short message asking for `tro_flow --json`. Part counts (`items`) and the FB type are taken from the catalog via the TRO type. Produces a **skeleton, not a running program**: one `REGION` per TRO with the instance call and every parameter line in the right order, following the real call sites in `doc/TRO_Katalog/scl_templates/*.scl`. **It will not compile as delivered, and that is deliberate.** Every value that has to come from the electrical planning is a visible gap marked `TODO(E-Planung)` — never an invented value, because a plausible-looking wrong sensor address is far worse than an empty one. Grep for that marker and nothing is missed. | filled from the layout | left as `TODO(E-Planung)` | |---|---| | REGION order (material flow), instance list, FB type per TRO | sensor/actuator addresses (`cInBG`, `MB`, `MA`) | | number of separator / switch / scanner parameter blocks | separator and switch numbers, `cMainTro` | | `nTo1Destinations` when the TRO has exactly one switch | jam-area DB names (`DB_JamArea…`) | | number of jam entries (one per own jam area, not per predecessor) and which jam area each exit leads to (`J<Separator>` in the comment) | | | destination list per switch exit (graph reachability) | times marked "pruefen" | | cross-reference to the layout parts (separator numbers, coordinate) | | Two derivations worth knowing about. **The destination list per switch exit** is computed by walking the TRO graph from each exit without going back through the switch — that is exactly what `FC_Direction` needs to map destination to direction, and it does *not* require the I/O list. And **`nTo1Destinations`** is only auto-filled for a single-switch TRO; where several switches share the exits, how they divide them is a planning decision, so it stays a gap rather than a guess. Switches: `--json NAME` (default `export_tro.json`, looked up in `%SKEL_RESULTS%`; written by `tro_flow.py --json`), `--out NAME` (default `<source>_FB_Main.scl`), `--eplan NAME` (see below), `--start TRO`. The last one matters while the plant has no loading/unloading station: the flow is then a closed loop with no entry, so the order has to be broken somewhere arbitrary. The chosen start and the reason are recorded in the file header. **Stations (`SYSn`).** Loading/unloading stations from `tro_flow.py` appear in the JSON under `boundaries` and as `SYSn` neighbours of the TROs, not in `tros`. They get no REGION and no instance. A TRO fed by a `SYSn` is the start of the order ("Materialeingang ueber SYSn"), so `--start` is no longer needed once the export contains a station. In the jam-area comments they are marked "(Station, kein TRO)", and an exit station shows up as a destination in the per-switch lists. **Internal structure.** `scl_skeleton.py` works in two stages: `lib/scl_context.py` derives everything from the TRO-JSON into a render context (`build_context` → `SclFile` / `SclTro`: flow order, separator/switch/scanner blocks, jam entries/exits, destination lists, names), and `scl_skeleton.py` only turns that context into text (`render_context`). Every name or address is an `SclName` (value + `todo` flag); today all of them are placeholders with `todo=True`. This split prepares the move to parameterised templates and per-project naming rules described in [`SCL_Templates.md`](SCL_Templates.md). **Templates (opt-in, `--templates [DIR]`).** With this switch every TRO whose FB has a template in `lib/scl_templates/<fbType>.scl.j2` (Jinja2) is rendered from it; the others keep the generic text above. The header and the console summary name which FB types came from a template. Today only the pilot `FB_ILS_MTRO_1Sep1Swi` exists — it follows the real call site of TRO107 line for line (priority manager `Dir2`, `stInSenJam<n>`, `xInCarouselRun1`, reference times 100/50 ms), and every value that is still open carries `TODO(E-Planung)`. `--strict` stops (exit `1`, no SCL) if any FB type has no template. Without `--templates` the output is unchanged. **Names from the E-planning (opt-in, `--eplan NAME`).** Reads the assignment `<csv>_eplan.json` written by `e_planning.py` (§5c; bare name looked up in `%SKEL_RESULTS%`). For every separator it assigns, the separator sensor (`stSenInSep := "DB_Inputs".Sensors["cInBG0642"]`) and the stopper valve (`xOutStopper<n> => "MB0643"`) are filled in and lose their `TODO(E-Planung)` marker (`SclName.todo = False`); everything else stays open. The header names the E-planning file. An assignment made for a different CSV (`csv_file` ≠ the TRO-JSON's `source`) stops the run with exit `1` — separator numbers are only unique within one export. Without `--eplan` the output is unchanged. Tests: `tests/test_scl_context.py` (the derivations one by one), `tests/test_scl_templates.py` (each template rendered with the real values of its reference call site must equal that call site, comments and whitespace ignored) and `tests/test_scl_skeleton_golden.py` (the whole output of mubea and HundM05 byte for byte against `tests/fixtures/scl/*_FB_Main.scl` and `*_FB_Main_templates.scl`, timestamp masked; an intended change is written back with `SKEL_UPDATE_GOLDEN=1`). ## 5c. `e_planning.py` — assign the E-planning to the layout parts The bridge between layout and electrical planning (background and findings: [`E-Planung_Bruecke.md`](E-Planung_Bruecke.md)). The layout knows which separators, switches and outfeed elements exist and where; the E-planning knows what their sensors and valves are called. The I/O list alone has no topology — but the E-planning is drawn as a layer over the hardware layout, every sensor or actuator symbol sits next to its part, and the cable-length tool (`kabellaengen/lib/getpositions.py`) writes those symbols with position and attributes to `<drawing>_positionsdraw.json`. ``` <name>.csv (layout) + <drawing>_positionsdraw.json (E-planning) │ ▼ e_planning.py ──► <name>_eplan.json (EplanDocument, schema sps_skel-eplan-v1) ──► <name>_eplan.md (report: assignments, gaps, drives) │ ▼ scl_skeleton.py --eplan <name>_eplan.json ``` Steps: 1. **Read** `sensors` (field devices) and `schaltschrank_elemente` (cabinet) of the positionsdraw file into `EDevice` (BMK, `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG` → controller `UH01` / I/O station `KF107`, position). A BMK listed twice keeps the entry that has a `VERW`. 2. **Registration** E-planning drawing → CSV with `dxf_registration.py`: the E symbols matching `[Registrierung] anker` (default `IN SEP`, the sensor that sits on the separator) against the CSV separators. Stored under the key `<csv>|<positionsdraw>` in `%SKEL_CFG%\dxf_registration.json` and re-verified on every run; `--re-register` forces a new derivation. 3. **Rules** from `%SKEL_CFG%\e_planning.ini`, one section `<kind>.<role>` per rule, applied in file order; each E device is given out at most once, each role of a layout part filled at most once: | key | meaning | |---|---| | `layout` | layout kind pattern (default: the part before the dot): `Separator`, `Ausschleus`, `Einschleus`, `Weiche`, `Scanner`, `Beladung`, `Entladung` | | `verw`, `praefix` | pattern(s) for the E attribute `VERW`, BMK prefix (`BG`, `MB`) | | `max_mm` | spatial: maximum distance after registration | | `soll_mm`, `toleranz_mm` | spatial: the symbol always sits this far from the part; candidates are ranked by their deviation from it, not by the smallest distance | | `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest | Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`, `Ausschleus.stau`, `Ausschleus.ventil`, `Weiche.ventil`. `VERW` texts and the numbering rule are project-specific — in the reference SCL (UH01..UH05) sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`. 4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the report. Uncertain cases stay open on purpose: a gap is better than a plausible wrong name. The report lists, per role, the layout parts without a match, the field devices without a match, and distance and runner-up distance of every match (close values = check by hand). Switches: `--file NAME` (CSV, default `export.csv`), `--eplan NAME` (positionsdraw JSON, required; bare name resolved against `%SKEL_DATA%`, or a full path), `--rules FILE` (default `%SKEL_CFG%\e_planning.ini`), `--re-register`. Exit `0` also when not everything was assigned; `1` on input errors or when the registration cannot be proven. **Today only separator sensor and stopper reach the SCL.** Jam sensors and outfeed/switch valves are assigned in the JSON/report, but the TRO-JSON does not yet say which outfeed element or switch belongs to which TRO exit, so `scl_skeleton` cannot place them. The same holds for the storage lines of a `PinStore` TRO (`arInSep[n]`): their sensors are assigned but the generator does not emit per-line parameters yet. Result on Mubea (`mubea.csv` + `Mubea_positionsdraw.json`): registration dx +50613.6 / dy +8555.8 mm; separator sensor 21/38, stopper 22/38 (all 20 storage lines exact, 0–1 mm), jam sensor 20/23, outfeed valve 20/23, switch valve 1/1. The open separators are on the carousels K0102/K0104/K0105, where the E-planning drawing and the CSV apparently differ. Tests: `tests/test_e_planning.py` (reading, rules, numbering vs. distance on a storage-line grid, target distance, uniqueness, registration, drives, the path into `scl_context` and `scl_skeleton --eplan`). ## 6. `tro_catalog.py` — TRO type catalog (library, no CLI) The single source of truth for everything needed to generate or draw a TRO type: short name (as used in `FB_Main`/`connect.ini`), Siemens FB block, sub-components and their counts, color group (for Graphviz/Mermaid/CAD) and CAD marker shape. **Eleven** types are defined in `TRO_CATALOG`. Ten of them are verified against `data/*.scl` where the source is vendored locally, and against `TRO_Typen.md`'s mapping table otherwise (see the module docstring, `CATALOG_AS_OF`, for provenance per type). > **`2Sep2Swi` is the exception — it is not backed by any SCL.** It was added on > 2026-08-12 for project 500573 (Mubea), to model the transfer point at the > Kreisel2/Kreisel1 touch as a single control object with 2 separators, > 2 scanners and 2 switches. **`FB_ILS_MTRO_2Sep2Swi` does not exist** in this > repository or in `doc/TRO_Katalog/scl_templates/`, and has to be created in > the ILSLib — otherwise a future SCL generator will emit a call to a missing > block. The closest verified type is `2Sep1Swi` (2 separators, 2 scanners, > **1** switch), which matches the hardware actually present in that export. Everything is a Pydantic v2 model (`TroDefinition`/`TroStyle`), so it validates on every assignment and comes with `model_dump()`/`model_validate()` for the planned JSON layout model for free. Consumed by `model.py`, `tro_flow.py`, `tro_annotate.py` and `scl_skeleton.py`; not run directly. ## 6b. `model.py` — shared model of TRO instances and the TRO-JSON (library, no CLI) `tro_catalog.py` says what a TRO **type** is; `model.py` says what one concrete TRO in a plant, and the whole TRO-JSON, look like. It imports only the catalog. ``` tro_catalog.py type level TroDefinition (items, FB block, color, symbol) ▲ get_tro() model.py instance level TroBase ─► DerivedTro, Slot, JamArea, Connection, TroCandidate, Analysis, TroDocument ▲ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws) ``` | Class | What it is | |---|---| | `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. | | `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`, `slots`. Also used for the system boundaries `SYSn` (no slots). | | `Slot` | A place on a TRO where hardware belongs: `slot` (`separator1`, `switch1`, `scanner1`, `drive` — the stable key together with the TRO-ID), `layoutId` (TeileId in the current export, for tracing only; empty for switches, whose element `tro_flow` does not keep yet), `roles` the hardware planning must fill (`sensor`, `stopper`, `jamSensor?` …; `?` = optional). Built by `derive_slots()` from the catalog type. | | `JamArea` | A jam area = the queue in front of a separator, the entry of exactly one TRO: `id` (`J<Separator>`, e.g. `J0042`), `tro`, `entry` (1, 2 for 2Sep types), `separator`, `host`, `predecessors` (all TROs/stations feeding it — merges have several), `note`, `position`. Built by `derive_jam_areas()`; the SCL name `"DB_JamArea<NNNN>".stJam<NNNN>_<k>` needs the conveyor number from the hardware planning and is not stored. | | `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). | | `TroCandidate` | A spot reported in a review where a TRO is **expected** but not derived (formerly `OpenPoint`): `name`, `type` (the expected catalog type), `position`, `belongsTo` (the TRO that holds the part today), `note`. Not a TRO — no separators, no connections, no SCL. Built by `tro_overrides.py` from `[<csv>.openpoint.<Name>]`; empty in the TRO-JSON for now. | | `Analysis` | Working result of `tro_flow.analyse_tros` (moved here from `tro_flow.py`). | | `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v4`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `jamAreas`, `findings`. `from_analysis()` (derives slots and jam areas), `to_json()`, `from_json()`, `jam_areas_or_derived()` (a v3 file without `jamAreas` still renders). | `TroBase` and `TroCandidate` share the `position` handling (JSON `{x,y,z}`, `x`/`y`/`z`, `describe_position()`) through the private mixin `_Positioned`. The same data has the same field name in every class, including `tro_overrides.py`: `tro_id`, `type_name`, `separators`, `scanners`, `hosts`, `label`, `reason`, `confidence`, `position`. All models validate on creation and assignment and reject unknown keys (`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`, `type`, `fbType`, `belongsTo` …); Python code uses the field names (`tro_id`, `type_name`, `fb_block`, `belongs_to`) — both are accepted on input. Schema v3 differs from v2 only in `candidates` replacing `openPoints`; v4 adds `slots` per TRO and `jamAreas`. A v3 file still reads (both default to empty; `scl_skeleton` then derives the jam areas while rendering). Tests: `tests/test_model.py` (round trip, catalog-backed fields, rejection of wrong keys/items, SCL rendering from a document) — run with `python -m pytest tests/`. ## 6a. `tro_overrides.py` — manual corrections to the derived TROs (library, no CLI) > **Since 2026-10-05 only `material_flow.py --connect` reads this file.** > `tro_flow.py` and `tro_annotate.py` no longer apply corrections: the sections > were keyed on running TRO numbers and went stale with every new export. > Errors in the TRO derivation are fixed in `lib/tro_flow.py` instead. The > format below is kept for `material_flow.py`. `tro_flow.py` derives TRO types from the mechanical layout alone. Some things are simply not in the CSV export: a switch that the export does not carry, a scanner that lifts the type to `1Sep_SSCC`, or a spot that a walkthrough identified as its own transfer point. Those findings come from a site review, and they must survive the next run — the `.dot`/`.svg`/`.md`/`.ini` outputs are all regenerated from scratch every time, so editing them is not an option. They therefore live in **`%SKEL_CFG%\tro_overrides.ini`** (hand-maintained, never generated). Section names are prefixed with the CSV file they apply to, so several plants — and several exports of the same plant — can coexist: ```ini [export.csv.tro.SEP0011] ; type correction, keyed on separator 0011 type = 1Sep1Swi ; a type from lib/tro_catalog.py reason = Weiche vor Ort bestaetigt (Review 2026-08-10) confidence = bestaetigt ; default when omitted [mubea.csv.openpoint.Qualitaetskontrolle] ; explicitly NOT a TRO yet x = -1026.00 ; plant coordinates in mm (CSV system) y = 5250.05 z = 1478.50 expect = 1Sep1Swi ; type expected here belongs_to = TRO09 ; TRO that owns the part today note = ... ``` A fourth kind folds several derived TROs into one control object: ```ini [export.csv.merge.Beruehrpunkt_K2_K1_gesamt] type = 2Sep2Swi separators = 0005, 0057 ; at least two, addressed by separator host = 0002-R ; primary flow node; the others are kept label = ... reason = ... ``` A third kind creates a TRO the derivation missed entirely: ```ini [export.csv.split.Beruehrpunkt_K2_K1] ; pull a separator out of its TRO type = 1Sep1Swi separators = 0057 ; comma-separated scanners = ; optional, follow the separators host = 0001-L ; flow node the new TRO sits on label = ... reason = ... ``` Three kinds of entry, deliberately different in weight: - **`tro`** — changes the type of an already-derived TRO. Applied after the TRO numbers are assigned (the sections address them by ID) and before the FB block and component list are pulled from the catalog, so the type change drags both along. A manually set type also suppresses the automatic "check whether `1Sep_SSCC` is needed" hint for that TRO. - **`split`** — creates a new TRO from separators taken out of an existing one. For transfer points the derivation misses because the separator's `Zuordnung` points at a conveyor (Strecke/Gefaellestrecke) instead of the Kreisel lane it actually switches on. Applied *before* the numbers are assigned, so the new TROs slot into the normal ordering — which means **adding a split renumbers everything after it**. That is exactly why type corrections should be keyed `SEP<number>`, not `TRO<number>`. - **`merge`** — folds several TROs into one. Runs *after* the splits, so a freshly split-out TRO can be part of a merge. The first affected TRO survives and absorbs the others' separators, scanners and hosts; `host` picks which flow node is primary, the rest stay attached so the linking still sees both sides of a transition. Like splits, this renumbers. - **`openpoint`** — a spot reported in a review that is **not** turned into a TRO. The structure stays untouched. Read as `model.TroCandidate` (`expect` → `type_name`, `x`/`y`/`z` → `position`, missing `z` = 0). In Python the entries are `TroOverride` (`tro_id` *or* `separator`, from `TRO<n>` / `SEP<n>`), `SplitSpec`/`MergeSpec` (`hosts` is a list; the INI key stays `host`, comma-separated), `model.Connection` for `[<csv>.connections]`, and `model.TroCandidate` — field names as in `model.py`. Consumer: `material_flow.py --connect` (writes the `[<plant>.manual]` section into `connect.ini`). Keep in mind: **TRO IDs are running numbers of one run and are only valid for the CSV named in the section.** Swap the data basis and the numbers shift. ## 7. `dxf_registration.py` — CSV↔DXF coordinate registration (library, no CLI) Derives the rigid transform (rotation + offset, no scaling) between the CSV export's and the DXF drawing's coordinate systems, by voting on the offset between same-role anchor points (`Separator` and `Scanner` blocks, see `ANCHOR_BLOCKS`), refining the best candidates via a Procrustes fit, and picking the candidate with the most exact matches. Raises `RegistrationError` if fewer than `MIN_INLIERS` (5) exact matches are found for any candidate. Persists/loads the verified transform per CSV/DXF filename pair under `%SKEL_CFG%\dxf_registration.json`. Used exclusively by `tro_annotate.py`; not run directly. `ANCHOR_BLOCKS` holds **name patterns** (fnmatch, case-insensitive), not fixed names, because two naming worlds occur in this project: | Drawing | Separator blocks | Scanner blocks | |---|---|---| | original customer drawing (`500573_60_1.dxf`) | `S-SP`, `S-LP` | `SCAN` | | macro-generated (`Mubea.dxf`, from dxfmakros) | `Separator_SP_2D`/`_3D` | `Scanner_2D`/`_3D` | `Staustrecke_Separator_SP_*` is deliberately **not** an anchor: it is a conveyor module with a built-in separator, so its insertion point is the module origin rather than the separator, which would feed a second cluster — offset by exactly that distance — into the vote. Note that the two roles can carry slightly different systematic offsets (in `Mubea.dxf`: separators land on +100/0, scanners on +400/0), so a mixed fit shows a larger `residual_max` than either role alone. That is expected; the mean residual is the number to watch, and only pairs within `FIT_TOL` (50 mm) enter the Procrustes fit. ## 8. `gen_tro_graphs.py` — documentation image generator (doc tooling, not part of the generator) Lives under `doc/TRO_Katalog/tro_graphs/`, not `lib/` — it produces the pictures embedded in `TRO_Katalog.md`, not generator output. Two families: 1. one schematic SVG per TRO type showing the block with its *possible* connection edges (`normal`/`dir2`/`dir3`/`bypass`/`finger`/`plc`/`extern`, each with its own color and line style), plus a shared `edge_legend.svg`; 2. one Graphviz topology graph per controller (`uh01`..`uh05`), built from `doc/TRO_Katalog/connect.ini`, rendered to SVG. No command-line switches — run it directly and re-run after editing `connect.ini`: ``` py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py ``` Needs Graphviz `dot` on `PATH` (or at the default Windows install location); without it the `.dot` files are still written, just not rendered to `.svg`.