Compare commits

...
46 Commits
Author SHA1 Message Date
Samer AyadiandClaude Opus 5.5 7ec2b88b79 scl_skeleton --eplan: E-Planung-Namen auch in den PinStore-Lagerlinien
Sensor und Stopper jeder Lagerlinie (und des Pin-Separators) kommen aus der
E-Planung wie bei den Separatorbloecken (scl_context._sep_namen); die
Zaehlung der eingesetzten Namen beruecksichtigt die Linien. Dazu die
ID-Datei cfg/mubea_tro_ids.ini und .gitignore fuer lokale Claude-Code-
Einstellungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:16:22 +02:00
Samer AyadiandClaude Opus 5.5 77c5aac343 tro_flow.link_tros: Uebergang durch eine Weiche zwischen zwei Kreiseln
Der Weichenknoten-TRO leitet ueber jede seiner Weichen auf den
Partnerkreisel um; dort faehrt der Traeger zum naechsten Halt nach der
Weiche. Ohne diese Kante fehlte z. B. in der Testanlage test_system
TRO(0011) -> TRO(0010), der Ausgang war vom Eingang aus unerreichbar.
Test tests/test_tro_link.py mit Fixture test_system_sep22_export.csv;
ID-Datei cfg/export_tro_ids.ini der Testanlage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:16:21 +02:00
Samer AyadiandClaude Opus 5.5 df24cf5adb hw_import + merge_hw_layout: Hardwareplanung einlesen und mit dem Layout zusammenfuehren
Stufen 2 und 3 aus doc/Konzept_Layout_Hardware_SCL.md:

- lib/hw_import.py: TIA-Import (*_TIA.xlsx, ohne openpyxl gelesen) +
  Positionsdatei (*_positionsconv.json) -> <projekt>_hw.json: je BMK
  Adresse, Index-Konstante, VERW, Station, Lage, Foerderer (K0102, #106 ->
  K0106); Antriebe MA/FC/GF; Gegenpruefung beider Quellen.
- lib/merge_hw_layout.py: <csv>_tro.json + <projekt>_hw.json ->
  <csv>_anlage.json (model.AnlageDocument): bindings je TRO-ID + Slot +
  Rolle mit Konstante und Adresse, Foerderer per Mehrheit (Mubea: K0102 =
  Kreisel3, K0104 = Kreisel2, K0105 = Kreisel1), Handkorrekturen
  cfg/hw_overrides.ini. Zuordnungslogik aus e_planning.py hierher
  verschoben; e_planning re-exportiert sie, Ergebnis unveraendert.
- TRO-JSON traegt optional `parts` (Lage/Huellbox, tro_flow --json),
  PinStore bekommt Slots line<n>.
- Gespeicherter Lagebezug mit SHA-256 der Positionsdatei; Unterordner in
  Eingabenamen relativ zu SKEL_DATA.
- Doku: Python_Scripts.md 5d/5e, Konzept, E-Planung_Bruecke (Abgleich
  mubea_layout.dxf / CSV / E-Planung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:16:10 +02:00
Samer AyadiandClaude Opus 5.5 c4122b4497 [FIX] Materialfluss: Kreisel als geschlossener Umlauf
Jeder Kreisel bekam nur EINE Bahnkante aus der Drehrichtung (UZS R->L,
GUZ L->R). Die Gegenbahn hatte damit keinen Abgang bzw. Zugang, und der
Sackgassen-Check meldete an fast jedem Kreisel "Sackgasse"/"nicht
erreichbar", obwohl die Anlage durchlaufbar ist.

- build_graph: immer L->R UND R->L; die Gegenkante traegt
  CIRCLE_RETURN_NOTE und wird in render_dot nicht gezeichnet
- Sackgassen-Pruefung ersetzt durch Erreichbarkeit im Gesamtgraphen
  (von Beladung erreichbar / Entladung erreichbar); unbestimmte Kanten
  zaehlen in beide Richtungen
- Weiche: Merkmal "Beruehrung" (Kreis/Kreis bzw. Tangente) hat Vorrang
  vor der Geometrie, die am Kappenuebergang per Rundung entschied
- Test test_kreisel_umlauf_graph.py mit Fixture test_system_export.csv

TRO-Graph (nach Hosts) fuer dematic/hundm_05/mubea/showroom unveraendert,
HundM05 komplett unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:40:53 +02:00
Samer AyadiandClaude Opus 5.5 550bdeb464 Weichenknoten zwischen Kreiseln, Track-Mouseover, Parameter in cfg/parameter.ini
TRO-Typen an Kreisel-Beruehrpunkten (lib/kreisel_junctions.py, derive_tros
Schritt 3c): jeder Kreisel als Stadion-Form mit Laufrichtung; jeder
gleichlaufende Beruehrpunkt ist eine Weiche (exportierte ILS Weiche oder
abgeleitet W<a>_<b>), Beruehrpunkte derselben Umlenkung bilden einen
Knoten. Ein Separator steuert ihn, wenn er in Laufrichtung unmittelbar
davor liegt (keine andere Weiche dazwischen). Typ = Separatoren x Weichen.
export.csv: 1Sep1Swi (9/1, 9/8, 8/7), 2Sep1Swi (7/2), 2Sep2Swi (2/3/4),
3Sep2Swi (4/5/6). Neuer Katalogtyp 3Sep2Swi (nicht aus SCL belegt).
Gegenlaeufige Beruehrpunkte (Mubea, Dematic) bleiben bei der alten Regel;
Mubea/HundM05/Dematic unveraendert, Showroom 0018+0019 jetzt 2Sep1Swi.

material_flow --track: Mouseover je Knoten mit Zugehoerigkeit zu
Kreisel/VF/GF (Zuordnung, TrackIds-Kette, Lage laut Geometrie), Weiche mit
beiden Kreiseln, Be-/Entladung mit ihrem Kreisel; Widersprueche mit "!".

Zahlenwerte (Toleranzen, Schwellen, Massstaebe, Groessen) aus allen
lib-Modulen nach cfg/parameter.ini (lib/parameter.py, Abschnitt je Modul,
Ueberschreiben je Anlage ueber %SKEL_CFG%/parameter.ini). Ausgaben aller
Anlagen bytegleich.

tro_flow.py: Docstrings mit Parametern/Rueckgabe, fehlende Typangaben.

Geprueft: 90 Tests gruen; tro_flow/material_flow/scl_skeleton auf 5 CSVs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:20:30 +02:00
Samer AyadiandClaude Opus 5.5 0c69faf3db Merge e-planung-bruecke: stabile TRO-IDs, TRO-JSON v4, E-Planung-Bruecke
Uebernimmt Michael Stangls Zweig (tro_ids, Slots/jamAreas v4, e_planning
--eplan, Konzept- und Datenquellen-Doku) in master, auf dem seitdem die
SCL-Templates fuer alle 7 Typen mit Referenz-Aufrufstelle liegen.

Konfliktloesung:
- lib/scl_context.py: beide Seiten zusammengefuehrt - Michaels _separator
  (E-Planung-Namen) und Staubereiche (_entries/_exit_area, ein JamEntr je
  eigenem Staubereich) plus die Template-Felder (_scanner, _store,
  carriers/data, part_sensor). _jam() fuehrt jetzt auch jam_id. Die beim
  automatischen Zusammenfuehren verlorene Property SclJam.title
  wiederhergestellt.
- Templates 1Sep, 1Sep2Swi, 1Sep_SSCC, Vario, Vario_workStation, PinStore_Auto
  auf v4 nachgezogen: Kommentar "Staubereich J.... von/vor ..." wie im
  Pilot; PinStore-TODO zaehlt Staubereiche statt Nachbarn (mubea: 2 Zulaeufe
  laufen in einen Eingang).
- Golden-Files *_templates neu erzeugt; geaendert sind ausschliesslich
  Staubereichs-Zeilen (Kennung im Kommentar, ein Eingang statt einem je
  Vorgaenger). Generische Golden-Files = Michaels Stand.
- Neuer Test: E-Planung-Namen kommen auch in der Template-Ausgabe an.
- Doku: README Schema v4, SCL_Templates.md Staubereiche und TODO-Zahlen.

Geprueft: 71 Tests gruen; tro_flow --json + scl_skeleton --templates auf
allen 8 CSVs ohne Fehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:24:20 +02:00
Samer AyadiandClaude Opus 5.5 e68ded7e55 Referenz: Staubereichs-DBs DB_JamArea4501 / DB_JamArea4104
Zwei reale Datenbausteine vom SPS-Programmierer als Referenzmaterial fuer
die spaetere Erzeugung der Staubereichs-DBs (Konzept_Layout_Hardware_SCL.md,
Stufe 4): ein DB je Foerderer, je Staubereich ein Struct stJam<NNNN>_<k> mit
arCarrier (Groesse ueber Konstante cJam<NNNN>_<k>) und stData, Kapazitaet im
BEGIN-Teil. Offene Fragen an die SPS im Kopf der Datei.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:49:49 +02:00
Samer AyadiandClaude Opus 5.5 41df841317 SCL-Templates fuer alle Typen mit Referenz-Aufrufstelle (Schritt 5)
- neue Templates: FB_ILS_MTRO_1Sep (TRO100), _Vario (TRO115),
  _Vario_workStation (TRO421), _1Sep2Swi (TRO267), _1Sep_SSCC (TRO105),
  _PinStore_Auto (TRO208); zusammen mit dem Pilot 1Sep1Swi 7 von 11 Typen.
  2Sep1Swi, EmptyCarrBuffer, LoadingBoom, 2Sep2Swi bleiben generisch
  (keine reale Aufrufstelle)
- scl_context: Felder nur fuer die Templates - Cognex-Verdrahtung je
  Scanner, part_sensor, carriers/data je Staubereich, jam_in/jam_out (dieselben
  Objekte wie erster Ein-/Ausgang), error_quit, automatic, is_border,
  correction_station, Bloecke sscc/work_station/store (PinStore: eine Linie je
  Speicherbahn aus dem Layout statt der 19 aus dem Katalog)
- _macros: cognex_in/cognex_out/cognex, correction, todo_mehr
- tests/test_scl_templates.py: Rundlauf je Template gegen seine Referenz
  (PinStore: von der Referenz abgekuerzte Teile ausgenommen); Normalisierung
  vereinheitlicht ":="/"=>"-Abstaende
- Golden-Files *_templates neu geschrieben (gewollt: mehr TROs aus Templates);
  generische Golden-Files unveraendert
- Doku: SCL_Templates.md (§6 Entscheidungen je Template, §11 Schritt 5, §13
  offene Fragen 9-12), Python_Scripts.md, CLAUDE.md, README.md

Zeilenentscheidungen sind Vorschlaege, noch mit den SPS-Kollegen zu
bestaetigen. Geprueft: 39 Tests gruen, jede Rundlauf-Pruefung erkennt eine
geaenderte Zeile; tro_flow --json + scl_skeleton --templates auf allen 8 CSVs
ohne Fehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:13:55 +02:00
Michael StanglandClaude Opus 5.5 0115ef68b7 TRO-JSON v4: Slots je TRO und Staubereiche (jamAreas)
- model: Slot (separator/switch/scanner/drive + Rollen), JamArea (J<Separator>,
  Eingang eines TRO mit allen Vorgaengern), derive_slots, derive_jam_areas;
  Schema sps_skel-tro-v4, v3-Dateien werden weiter gelesen
- scl_context: ein JamEntr je eigenem Staubereich statt je Vorgaenger (wie
  UH01-UH05), Ausgaenge nennen den Staubereich des Nachfolgers
- Golden Files neu erzeugt, Tests fuer Slots/Staubereiche, Doku nachgezogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:44:02 +02:00
Michael StanglandClaude Opus 5.5 dd2d98f2ee Stabile TRO-IDs: ID-Datei je Anlage, Wiederfinden ueber die Lage
- lib/tro_ids.py: cfg/<csv>_tro_ids.ini lesen/schreiben, TROs ueber die
  Lage ihrer Separatoren wiederfinden, neue IDs vergeben, Weggefallene melden
- tro_flow: Nummerierung entlang des Materialflusses, Schalter --ids,
  --renumber, --id-tolerance; Bericht der IDs
- tro_annotate: liest dieselbe ID-Datei (nur lesend)
- Tests tests/test_tro_ids.py, Doku Python_Scripts.md, Konzept (Stufe 1 erledigt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:31:18 +02:00
Michael StanglandClaude Opus 5.5 ef57a2218e Konzept: Staubereiche nach Separator, SCL-Namen nach UH01-UH05
- Kennung der Staubereiche in der JSON nach ihrem Separator (J<Sep>,
  wie im Mubea-Layout J1024 fuer Sep1024)
- SCL-Name festgelegt auf den Standard UH01-UH05:
  "DB_JamArea<NNNN>".stJam<NNNN>_<k> (Foerderernummer, Reihenfolge
  entlang des Materialflusses)
- Separator- und Foerderernummern wie TRO-IDs in der ID-Datei

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:51:04 +02:00
Michael StanglandClaude Opus 5.5 e2a80e6de1 Doku: Datenquellen Layout/Hardwareplanung, Konzept Layout+Hardware -> SCL
- Klassenhierarchie.md, Kapitel Daten: vollstaendige Rohangaben aus
  Anlagen-Hardware (CSV-Spalten, Merkmale je TeileArt) und Hardwareplanung
  (EA-Liste, TIA-Import, WSCAD, positionsdraw)
- Datenquellen_Layout_Hardwareplanung.md: was TROs/SCL aus Layout,
  Hardwareplanung und Defaults bekommen; 1Sep1Swi- und Vario-Aufruf
  zeilenweise mit Herkunft
- Konzept_Layout_Hardware_SCL.md: Zielablauf tro_flow -> hw_import ->
  hw_assign -> scl_skeleton, Rolle der bestehenden Skripte, Festlegungen
  zu TRO-IDs (stabil ueber Lage, --renumber) und Staubereichen
- CLAUDE.md: beide neuen Dokumente in der Leseliste

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:33:26 +02:00
Michael StanglandClaude Opus 5.5 c736288476 E-Planung -> Layout: e_planning.py, scl_skeleton --eplan, Kapitel Daten
- doc/Klassenhierarchie.md: Kapitel "Daten" - alle Daten der SCL-Referenzen
  nach Quelle (Layout, E-Planung, Namenskonvention, Parameter)
- doc/E-Planung_Bruecke.md: Bruecke ueber *_positionsdraw.json des
  Kabellaengen-Tools, Befunde an Mubea
- lib/e_planning.py + cfg/e_planning.ini + bin/e_planning.bat/.sh:
  Lagebezug E-Zeichnung -> CSV, Zuordnung BMK -> Layout-Bauteil nach Regeln
  (Abstand, Sollabstand, Nummernregel MB = BG + 1), <csv>_eplan.json + Bericht
- scl_skeleton --eplan: Separator-Sensor und Stopperventil eingesetzt,
  ohne Schalter unveraenderte Ausgabe (Golden-Files gleich)
- tests/test_e_planning.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:30:56 +02:00
Michael StanglandClaude Opus 5.5 5764df1ac8 Doku: README auf Hauptweg CSV -> TRO-JSON -> SCL, Klassenhierarchie ausgebaut
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:31:05 +02:00
Michael StanglandClaude Opus 5.5 29077caa81 model: TroCandidate statt OpenPoint, tro_overrides nutzt model-Klassen (Schema v3)
- OpenPoint -> model.TroCandidate (type, position, belongsTo, note);
  TroDocument.candidates ersetzt openPoints, Schema sps_skel-tro-v3
- Positionsbehandlung von TroBase und TroCandidate im Mixin _Positioned
- tro_overrides.py: eigene ConnectionSpec/OpenPoint entfallen, stattdessen
  model.Connection / model.TroCandidate; einheitliche Feldnamen
  (tro_id/separator, hosts als Liste)
- Fixtures, Beispiel-JSON und Doku auf v3 nachgezogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:31:05 +02:00
Michael Stangl de8ecb9460 Merge branch 'master' of https://gitea.schoenenberger.de/Schoenenberger_Systeme_GmbH/sps_skel 2026-10-10 10:08:33 +02:00
Michael Stangl ba58c98851 Klassenhierarchie dazu gebaut 2026-10-10 09:24:49 +02:00
Samer AyadiandClaude Opus 5.5 4895ebf5ed SCL aus parametrisierten Templates: Golden-Files, Render-Kontext, Pilot 1Sep1Swi
Schritte 1-3 aus doc/SCL_Templates.md (neu: Konzept, Wertklassen je Zeile,
Namensregeln, Tests, Migrationsplan, Vertrag fuer den CSV->SCL-Workflow).

- Golden-Files (tests/test_scl_skeleton_golden.py, tests/fixtures/scl/):
  komplette FB_Main-Ausgabe von mubea und HundM05 byte-genau festgeschrieben,
  Zeitstempel maskiert; gewollte Aenderung per SKEL_UPDATE_GOLDEN=1
- lib/scl_context.py (neu): alles, was scl_skeleton aus der TRO-JSON ableitet
  (Reihenfolge, Separator-/Weichen-/Scanner-Bloecke, Staubereiche, Ziellisten,
  Namen) als pydantic-Kontext SclFile/SclTro; Namen als SclName (Wert +
  todo-Merker) als Andockstelle fuer die Namensregeln (Schritt 4).
  scl_skeleton schreibt nur noch Text; Ausgabe byte-identisch (Golden-Files,
  zusaetzlich dematic/showroom mit und ohne --start alt/neu verglichen)
- lib/scl_templates/ (neu, Jinja2): _macros.scl.j2 + Pilot
  FB_ILS_MTRO_1Sep1Swi.scl.j2 nach der realen Aufrufstelle TRO107
  (PriorityManagerDir2, stInSenJam<n>, xInCarouselRun1, xInRelease TRUE,
  Separatorzeiten 100/50 ms). Ortsgebundene Zeilen als Vorschlag umgesetzt,
  jede offene Stelle mit TODO(E-Planung) - Bestaetigung durch SPS steht aus
- scl_skeleton: --templates [DIR] (opt-in, ohne Schalter Ausgabe unveraendert),
  --strict (Exit 1 ohne SCL, wenn ein FB-Typ kein Template hat); Kopf und
  Zusammenfassung nennen Template-/generische Typen; Templatefehler
  (StrictUndefined) -> Exit 1 ohne SCL
- tests/test_scl_templates.py: Rundlauf Template mit Referenzwerten ==
  Referenz-REGION (normalisiert); jede Platzhalterzeile traegt den TODO-Marker
- requirements.txt: jinja2 >= 3.1 (bestehende .venv: pip install -r requirements.txt)
- Doku: SCL_Templates.md, Python_Scripts.md §5b, Commandlines.md §2.5, CLAUDE.md

Befunde: xInCarouselRun1 laesst sich nicht aus der TRO-JSON ableiten (Kanten-
art "umlauf" kommt praktisch nicht vor) -> immer als TODO; nTo1Destinations
ist in der Referenz 3 bei 2 Abgaengen (zaehlt evtl. Ziele) -> "pruefen".
Mit --templates nur die 1Sep1Swi-REGIONs (+ Kopf) anders als generisch;
32 Tests gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:06:18 +02:00
Samer AyadiandClaude Opus 5.5 97a0db6707 model.py: gemeinsames TRO-Modell; Hauptweg CSV -> tro_flow --json -> scl_skeleton
- lib/model.py (neu, pydantic): TroBase -> DerivedTro, Connection, OpenPoint,
  Analysis (aus tro_flow verschoben), TroDocument = TRO-JSON (Schema
  sps_skel-tro-v2). items/fbType werden immer ueber den Typ aus tro_catalog
  gelesen, nie gespeichert; abweichende Werte in einer JSON sind ein Fehler.
  JSON-Schluessel wie bisher (id, type, fbType ...), extra="forbid".
- tro_flow: Tro/Analysis aus model.py; neuer Schalter --json schreibt
  <name>_tro.json (TroDocument)
- scl_skeleton: liest die JSON ueber TroDocument statt per Dict-Zugriff,
  Bauteile ueber TroItem-Konstanten; ungueltige JSON -> Exit 1 ohne SCL,
  alte tro_extract-JSON (v1) mit Hinweis auf tro_flow --json
- tro_annotate: nur noch Ansicht; derselbe analyse_tros-Aufruf wie tro_flow
  (inkl. sepliste-Gegenprobe), damit Zeichnung und JSON uebereinstimmen
- tro_extract: geparkt (unveraendert)
- tests/test_model.py + conftest.py, pytest in requirements.txt aktiviert
- Doku: Python_Scripts.md (§6b model.py), Commandlines.md, CLAUDE.md,
  Aufrufkommentare in bin/ (scl_skeleton.bat zeigte tro_flow-Aufrufe)

Geprueft: alle tro_flow/tro_annotate-Ausgaben (8 CSVs) bis auf Zeitstempel
identisch zum vorherigen Stand; scl_skeleton alt/neu auf derselben JSON
byte-identisch (export, mubea, HundM05); 10 Tests gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:42:46 +02:00
Samer AyadiandClaude Opus 5.5 90cafec99c tro_flow: Weichen aus der Umlaufreihenfolge, 1Sep2Swi, keine "gleiche Bahn"-Notiz an Weichen-TROs
- _weichen_aus_umlauf: ein Separator, auf den in Laufrichtung ein Ausschleuselement folgt (ohne Separator dazwischen), ist dessen Weiche - auch ohne CSV-Nachbarschaft (Dematic 0043 -> VF_5, Nachbarn nur Einschleuselemente); Speicherlinien ausgenommen
- Schritt 3b': weitere Ausschleuselemente bis zum naechsten Separator gehoeren zum selben Separator, zwei -> 1Sep2Swi (Dematic 0048 -> VF_4 + VF_6), Finding bei fehlendem Scanner bzw. mehr als zwei Weichen
- Weichenkante je Host; Weichen-TRO ohne Bahn-Nachfolger (einziger Separator seines Kreisels) zaehlt als praezise Quelle - entfernt die Contraction-Walk-Kanten an VF_4/VF_5/VF_6 vorbei
- keine gestrichelte "gleiche Bahn"-Notiz an Weichen-TROs (Host ist die Strecke, in die sie abzweigen, nicht ihre Bahn)
- HundM05 (28 Kanten) und Mubea (26) inhaltlich unveraendert, --use-cords-Ausgabe byte-identisch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:35:59 +02:00
Michael StanglandClaude Sonnet 5 e91b754d90 material_flow: Kreisel-Umlauf als Kreispfeil, Weiche-Bahnzuordnung ueber Geometrie
material_flow.svg: der UZS/GUZ-Pfeil zwischen Bahn R/L eines Kreisels wird nach
dem Graphviz-Rendern durch einen 270-Grad-Kreispfeil mit dem Kuerzel in der
Mitte ersetzt (Graphviz kennt keine kreisfoermigen Pfeile/Labels).

Weiche zwischen zwei Kreiseln: neue _kreisel_touch_lane() bestimmt per
Boundingbox (Stadion-Form) und Weiche-Position geometrisch, ob der
Beruehrpunkt an einer Stirnseite oder an der Laengsseite jedes beteiligten
Kreisels liegt. Nur am reinen Stirnseiten-Treffer (Kreis/Kreis) ist der
Bahnwechsel L<->R eindeutig und wird weiterhin per Drehrichtung gerichtet
gezeichnet; an einer Laengsseite (Kreis/Gerade) kommt die betroffene Bahn
direkt aus der Geometrie, die Richtung bleibt aber unbestimmt, da sie vom
(unbekannten) Schaltzustand der Weiche abhaengt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 09:28:22 +02:00
Samer AyadiandClaude Opus 5.5 7a71e12272 Dematic: DXF-Registrierung dematic.csv <-> dematic_TST.dxf; doc/Commandlines.md: Uebersicht aller Kommandozeilen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:08:17 +02:00
Samer AyadiandClaude Opus 5.5 f0c3102492 TRO-Katalog: Identifikationskriterien je TRO-Typ und Entscheidungsreihenfolge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:03:26 +02:00
Samer AyadiandClaude Opus 5.5 a741dd0157 tro_flow: Speicherbloecke je Steuer-Separator, Ring-Schluss bis zum ersten Kettenglied, Weichen nur in Laufrichtung
- _speicher_bloecke: Schwerkraftlinien nach Umlaufreihenfolge in Bloecke schneiden (Separator dazwischen = neuer Block); der Weichen-Separator vor dem Block ist Steuer-Separator des PinStore_Auto statt eigener 1Sep1Swi (Dematic: 3 Speicher 10/7/10 statt einem mit 27)
- Umlauf schliessen bis zum ersten Element mit TRO, nicht nur ueber Stationen hinweg (Dematic Kreisel2 beginnt mit Einschleuselement: TRO01 -> Speicher fehlte; Mubea TRO06 -> TRO10 statt Fallback TRO06 -> TRO18)
- _weichen_nach_umlauf: Separator steuert ein Ausschleuselement nur als letzter Separator davor in Laufrichtung (Dematic 0047/0046 waren ueber den Ring-Schluss bzw. hinter dem Block "benachbart")
- _kreisel_bahn: Kreisel-Separator mit Gefaellestrecke als Zuordnung auf die Kreisel-Bahn haengen (Dematic 0046 zaehlte als Speicherlinie)
- HundM05 unveraendert, Mubea 26 Kanten (eine korrigiert)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:11:25 +02:00
Samer AyadiandClaude Opus 5.5 e0defad011 tro_flow: --use-overrides, --from-connect und --geometry entfernt
TROs, Typen und Verbindungen kommen nur noch aus dem Layout. Fehler der
Ableitung werden im Code behoben, nicht in einer Korrekturdatei.

- tro_flow: Optionen --use-overrides/--no-overrides, --from-connect,
  --geometry, --geo-tol-mm entfernt; analyse_tros/derive_tros ohne
  overrides/resolve_host/count_exits-Parameter, Analysis.overrides entfernt,
  Override-Schritte 4b/4c/5b, Doku-Abschnitt "6. Manuelle Korrekturen" und
  die Override-Zeilen im Bericht entfernt; connect.ini ist reine Ausgabe
- lib/tro_geometry.py geloescht (nur von --geometry genutzt), shapely aus
  requirements.txt
- tro_overrides: apply_splits/_merges/_connections/_overrides und
  load_connect_ini entfernt - nur noch load_overrides fuer
  material_flow.py --connect
- doc/Python_Scripts.md nachgezogen

Gegenprobe: DOT (beide Layouts) und connect.ini fuer HundM05 und Mubea
unveraendert (28 bzw. 26 Verbindungen), Doku nur ohne den leeren
Abschnitt 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:02:16 +02:00
Samer AyadiandClaude Opus 5.5 a5fab7e2dc tro_annotate: selten gebrauchte Optionen entfernt
- --max-residual: feste Grenze DEFAULT_MAX_RESIDUAL (600 mm), Meldung
  verweist auf --offset
- --layer-prefix: Layer immer mit Praefix TRO_
- --use-overrides: cfg/tro_overrides.ini wird nie gelesen, Marker passen
  damit immer zum TRO-Fluss
- damit toter Code entfernt: draw_open_points, Layer TRO_OPENPOINT,
  Berichtszeilen Offene Punkte/Typkorrekturen (Daten kamen nur aus der INI)
- --emit-lisp bleibt (einziger Weg, tro_types.lsp fuer TRO_INSERT/TRO_EDIT
  nach einem neuen Katalogtyp neu zu schreiben)
- doc/Python_Scripts.md nachgezogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:54:13 +02:00
Samer AyadiandClaude Opus 5.5 e2f1a558a2 SCL-Geruest: Stationen (SYSn) als Materialeingang; kurze TRO-Verbindungen nicht mehr verwerfen
scl_skeleton.py brach mit KeyError ab, sobald der TRO-Fluss Stationen
(SYSn) enthielt. Stationen bekommen jetzt keine REGION/Instanz, der von
einer Station gespeiste TRO ist der Anfang der Reihenfolge, in den
Staubereich-Kommentaren sind sie als Station markiert.

tro_annotate.py liess Verbindungen unter 2,2 Markergroessen still weg
(HundM05 TRO03->TRO04, 500 mm) - sie fehlten dann in tro_extract.py und
in der SCL. Kurze Verbindungen werden jetzt Mitte-zu-Mitte gezeichnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:34:18 +02:00
Samer AyadiandClaude Opus 5.5 7b98c8b8d4 TRO-Beschriftung wie das TRO-Fluss-SVG; Stationspfeile zeigen von der Kette weg
tro_flow:
- _station_positionen: Richtung des Zu-/Ablaufpfeils mit groesstem
  Winkelabstand zu allen Kanten des Anker-TROs und zu bereits gesetzten
  Stationspfeilen (15-Grad-Raster), echte Stationsrichtung nur bei
  Gleichstand - vorher lag z. B. Mubea TRO02 -> SYS2 fast auf TRO02 -> TRO13

tro_annotate:
- Symbole mit typ-eigener Fuellfarbe (Solid-Hatch) unter der Gruppenkontur
- Kantenfarben/-layer wie im SVG: Uebergabe blau, Weiche orange,
  Materialeingang gruen, Materialausgang rot (TRO_WEICHE/_EINGANG/_AUSGANG)
- Stationen als farbige Beschriftung am Pfeilende, Lage ueber
  tro_flow._station_positionen (ohne CONNECTION_ARROW-Block)
- Legende wie im SVG: je vorkommendem Typ Fuellung/Rand/Form, dann Kantenfarben
- CONNECTION_ARROW: FROM_TRO/TO_TRO unsichtbar (Daten bleiben), KIND auch weiche

cfg/dxf_registration.json: mubea.csv|mubea.dxf und HundM05.csv|HundM05.dxf
mit Versatz 0 (CSV aus derselben dxfmakros-Zeichnung; Auto-Ermittlung sieht
die verschachtelten Separatoren nicht und war mehrdeutig);
mubea.csv|500573_60_1.dxf neu ermittelt (36/46 Punkte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:30:47 +02:00
Samer AyadiandClaude Opus 5.5 d0a3ac7e5a TRO-Fluss: Stationen nicht mehr in der Kette, nur als Zu-/Ablaufpfeil
- link_tros: Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind kein
  Kettenglied mehr - die Umlaufkette verbindet die TROs direkt an ihnen
  vorbei. Entladung/Ausgang haengt am TRO direkt davor, Beladung/Eingang
  am TRO direkt danach. HundM05: TRO19 -> TRO20, SYS1/SYS2 -> TRO20,
  TRO19 -> SYS3. Mubea: TRO02 -> TRO13, TRO04 -> TRO03, TRO02 -> SYS2/SYS3,
  SYS1 -> TRO03.
- Umlauf schliessen ueber Stationen am Listenanfang hinweg (HundM05 Kreisel1
  beginnt mit den Beladungen, sonst fehlte TRO19 -> TRO20)
- render_dot: Station als farbige Beschriftung am Ende eines Pfeils (gruen
  Eingang, rot Ausgang) statt Ellipse, Legende entsprechend; lagerichtig
  feste Pfeillaenge ab dem angehaengten TRO, mehrere Stationen am selben TRO
  aufgefaechert (_station_positionen ersetzt _anzeige_position)
- HundM05 28 Verbindungen, Mubea 26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:15:23 +02:00
Samer AyadiandClaude Opus 5.5 fcfc4f1278 TRO-Fluss --use-cords: Leerstrecken kuerzen fuer kompaktere Darstellung
Je Achse bleiben Reihenfolge und echte Abstaende der Knoten bis
COORD_MAX_LUECKE (1500 mm) erhalten, groessere Leerstrecken (lange Foerderer
ohne TRO) werden darauf gekuerzt (_achse_kompakt/_kompakte_positionen,
_zeichen_positionen). Legende naeher an der Anlage. Reine Darstellung:
Knoten, Kanten, Labels und Tooltips (mit echten Koordinaten) unveraendert -
DOT ohne pos-Attribute fuer HundM05 und Mubea identisch. Mubea 2929x3333 ->
2212x2411 px. --true-scale zeichnet weiterhin masstaeblich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:49:31 +02:00
Samer AyadiandClaude Opus 5.5 9ac6f88123 TRO-Fluss: Linienspeicher liegt im Kreisel-Umlauf
Die Ausschleuselemente der Speicherlinien zaehlen in der Umlaufkette als
Glied des PinStore-TROs, die Einschleuselemente liefern - wie bei einer
Vario-Strecke - den Ablauf zum naechsten Halt. Ersetzt die Regel
"letzter Halt vor dem Speicher speist ihn" aus f9c9d88.

Mubea Kreisel3: TRO05 -> TRO18 -> TRO07 (keine direkte Kante TRO05 -> TRO07
mehr, TRO05 bleibt 1Sep - kein ILS Ausschleuselement einer Strecke),
TRO12 (VF_2 muendet direkt vor den Speicherlinien) -> TRO18,
TRO18 -> TRO10 jetzt aus den Einschleuselementen auf Kreisel2.
Im Review 2026-10-01 bestaetigt. HundM05 unveraendert (28), Mubea 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:29:18 +02:00
Samer AyadiandClaude Opus 5.5 f9c9d88a08 TRO-Fluss: Kreisel-Umlauf laeuft durch alle Stationen, Speicher-Abzweig aus der Umlaufkette
- _add_chain_pair: Be- und Entladestationen sind in der Umlaufkette
  gewoehnliche Glieder - der Kreisel laeuft immer weiter. HundM05 Kreisel1:
  TRO19 -> SYS3 -> SYS1 -> SYS2 -> TRO20 (vorher endete der Fluss an SYS3).
  Mubea: TRO04 -> SYS1 statt der groben Fehlkante TRO04 -> TRO18.
- link_tros: der letzte Halt vor dem ersten Ausschleuselement einer
  Speicherlinie speist den PinStore-TRO (Mubea Kreisel3: TRO05 -> TRO18)
- HundM05 28 Verbindungen, Mubea 26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:01:39 +02:00
Samer AyadiandClaude Opus 5.5 b09493f8ab TRO-Fluss --use-cords: ohne "gleiche Bahn"-Punktlinien und Legendenzeile
Lagerichtig liegen TROs derselben Bahn/Strecke ohnehin sichtbar
nebeneinander - die Punktlinien waren dort nur Unruhe. Gezeichnet werden
nur noch echte Verbindungen (HundM05 27, Mubea 25). Die berechnete
Anordnung ohne --use-cords behaelt Linien und Legendenzeile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:33:58 +02:00
Samer AyadiandClaude Opus 5.5 2471cbf268 TRO-Fluss: Entladung auf der Kreisel-Bahn wird durchfahren, Legende ohne Kreisel-Umlauf
- link_tros/_add_chain_pair: eine Materialausgang-Systemgrenze darf in der
  Kreisel-Umlaufkette Quelle sein - was nicht entladen wird, faehrt weiter.
  Mubea Kreisel2: TRO02 -> SYS3 -> SYS2 -> TRO13 (vorher endete der Fluss an
  SYS3, TRO13 hatte keinen Vorgaenger). Beladung bleibt reine Quelle.
- Legende: Zeile "blau gestrichelt - Kreisel-Umlauf" entfernt
- HundM05 unveraendert (27 Verbindungen), Mubea 25 Verbindungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:32:19 +02:00
Samer AyadiandClaude Opus 5.5 43543915ca TRO-Fluss: ILS Weiche als echte Weiche, 2Sep2Swi am Kreisel-Beruehrpunkt, lesbare lagerichtige Darstellung
- _find_weiche_separators: ILS Weiche (TeileArt) ist ein echtes Weichen-
  Bauteil -> 1Sep1Swi; steuernder Separator = CSV-Nachbar mit Scanner
  (Mubea 0125 -> 0036), mehrdeutig -> Finding statt Raten
- _find_switch_separators: Separator mit Zuordnung auf eine Strecke, der in
  der TrackIds-Kette eines Kreisels steht, gilt als Kreisel-Separator
  (Mubea 0005 vor Ausschleuselement 0057)
- derive_tros 3d: Ausschleuselement-Weiche und ILS Weiche direkt
  hintereinander in der TrackIds-Kette -> ein 2Sep2Swi (Mubea TRO10:
  Sep 0005/0036, Scan 0006/0041); FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib
- --use-cords: feste Symbolgroesse je Form, nur TRO-Nummer im Symbol,
  Typ/Separator als xlabel, Details als Tooltip, Massstab 0.1 statt 0.25 pt/mm,
  Legende rechts mit nur den vorkommenden Typen, blasse Linien fuer
  gleiche Bahn, Systemgrenzen dicht an einem TRO werden in der Zeichnung
  beiseitegeschoben
- HundM05 unveraendert (27 Verbindungen), Mubea 18 TROs / 23 Verbindungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:16:30 +02:00
Samer AyadiandClaude Opus 5.5 02db9a95b0 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 <noreply@anthropic.com>
2026-10-01 10:50:40 +02:00
Michael Stangl 1479519bdc Merge branch 'show-bbs-all' 2026-09-25 22:19:19 +02:00
Michael StanglandClaude Opus 5.5 a1bba2b20d material_flow: --show-bbs [flow|all] mit Legende und Tooltips
- neuer Wert fuer --show-bbs: 'flow' (Default, auch ohne Wert) blendet
  die VF/GF-Einzelbausteine (Bogen, Streckenelement, Motor-/Umlenkstation)
  aus, 'all' zeichnet jede CSV-Zeile
- eigene Farben fuer die Einzelbausteine, Legende mit Anzahl je Objektart,
  Tooltip je Rechteck (Zeile, TeileId, TeileArt, Bezeichnung)
- genutzt von dxfmakros/bin/show_exportcsv.bat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:25:38 +02:00
Samer AyadiandClaude Sonnet 5 058c60e23d TRO-Fluss: Weichenerkennung ueber CSV-Nachbarn statt Distanzschwelle
_find_switch_separators() ermittelt Weichen-Separatoren jetzt topologisch
(Ausschleuselement unter den CSV-Nachbarn, gleiche Kreisel-Bahn) statt ueber
eine Distanzschwelle - die Fallback-Schwelle paarte an Mubea unbeteiligte
Separatoren faelschlich mit dem naechsten freien Ausschleuselement, u.a. 20
PinStore_Auto-Speicherlinien wurden so faelschlich als 1Sep1Swi erkannt.

Materialfluss: _sole_kreisel_neighbour() als Fallback in _apply_transfers()
fuer AS/ES-Elemente ohne eigenes Merkmal "Kreisel" (Mubea), aufloesbar wenn
die Strecke genau einen Kreisel-Nachbarn hat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 15:06:25 +02:00
Samer AyadiandClaude Sonnet 5 e4bfad4dcc TRO-Fluss: Vario/1Sep alternierend rueckwaerts statt nur letzter Separator
Bisher wurde auf einer Streckengruppe nur der jeweils letzte Separator (in
TrackIds-Reihenfolge) auf 1Sep umgestuft, alle davor blieben pauschal Vario.
Eine Streckengruppe ist baulich aber meist eine alternierende VF/GF-Kette
(angetriebener Abschnitt, Vereinzeler, unbetriebener Abschnitt, Vereinzeler,
...), nicht nur am Ende - fuer 2 Separatoren macht das keinen Unterschied,
ab 3 schon. Die Umstufung laeuft jetzt vom letzten Separator rueckwaerts und
alterniert: letzter = 1Sep, davor Vario, davor wieder 1Sep usw.

Bestaetigt an HundM05 VF_1 (4 Separatoren): der neue Positions-Nachweis stuft
den dritten von hinten (0035) unabhaengig auf 1Sep ein - deckungsgleich mit
dem bereits vorhandenen (aber als moeglicherweise unkorrigierte CAD-Vorgabe
mit Vorbehalt behafteten) Hoehen-/Antriebsnachweis (_incoming_drop). Der
Hoehen-/Antriebsnachweis bleibt als unabhaengige Zusatzpruefung erhalten und
kann eine laut Muster erwartete Vario-Position auf 1Sep umstufen, wenn dort
tatsaechlich ein unbetriebenes Gefaelle gemessen wird - nicht umgekehrt.

TRO-Typenzahl auf beiden Referenzanlagen (HundM05, Mubea) unveraendert,
nur die Finding-Texte wurden praeziser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:55:26 +02:00
Samer AyadiandClaude Sonnet 5 ef3d8170fe TRO-Fluss: sepliste (Roh-JSON) als Gegenprobe fuer die Separatorenzahl je Host
Optional: findet tro_flow.py neben der CSV ein <stamm>_raw.json (dxfmakros-
Rohattribute), vergleicht es je Strecke/Gefaellestrecke-Host die aus der CSV
abgeleitete Separatorenzahl mit der Anzahl 'SEP'-Eintraege in dessen eigener
'sepliste' und meldet Abweichungen als Finding - ohne TRO-Daten zu aendern,
die CSV bleibt die massgebliche Quelle. Faengt an HundM05 sofort einen
echten Fall: VF_5 hat laut CSV 3 zugeordnete Separatoren, sepliste nennt nur
2 - passt zum bereits gemeldeten Separator 0018 ohne TrackIds-Mitgliedschaft.

Bewusst NICHT verwendet: die sepliste-Koordinaten (der jeweils erste
SEP-Eintrag jedes VF-Blocks traegt durchgehend eine kaputte Position, ein
Bug im DXF-Export) und das Attribut "ANZAHL_SEPARATOR" (weicht in beiden
bisher geprueften Anlagen von TrackIds UND sepliste ab, vermutlich veraltet/
doppelt gezaehlt). Fehlt die Roh-JSON-Datei, entfaellt die Gegenprobe
stillschweigend; ist sie vorhanden aber kaputt (z. B. unvollstaendig
geschrieben), gibt es eine Warnung statt eines Abbruchs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 15:40:43 +02:00
Samer AyadiandClaude Sonnet 5 f4b0e4d508 TRO-Fluss: Separatoren ohne TrackIds-Mitgliedschaft als eigenstaendige 1Sep
Ein per Zuordnung einer Streckengruppe zugeordneter Separator, der nicht Teil
von deren eigener TrackIds-Kette ist, wird direkt als 1Sep eingestuft statt
ueber die Vario/letzter-Separator-Logik zu laufen - vor Ort bei Mubea
VF_1-VF_3 bestaetigt: jede Streckengruppe hat genau einen echten
Kettenseparator, alle weiteren Zuordnung-Treffer sind eigenstaendige
Vereinzeler ohne eigenes VF-Teilstueck. Damit ein solcher Separator nicht
zugleich faelschlich als Weichen-Indiz zaehlt (engster Treffer ueberhaupt bei
Mubea 0005, 163mm), werden echte VF-Kettenmitglieder vor der
Weichenerkennung (_find_switch_separators) ausgenommen - Naehe zum eigenen
Aus-/Einschleuselement ist bei ihnen der normale, erwartete Aufbau, kein
Weichenbeleg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 13:16:36 +02:00
Samer AyadiandClaude Sonnet 5 97965e8ec7 TRO-Fluss: Weichenerkennung ohne Bahn-Sperre, Vario/1Sep-Sonderfaelle je Separator
_find_switch_separators: Regel 2 (naechster freier Separator <= 1500mm) lief
bisher nur auf einer Bahn, die Regel 1 (<=500mm) schon bestaetigt hatte. Bei
HundM05 lagen nach einer CAD-Aktualisierung alle 5 echten Weichen-Separatoren
gleichmaessig bei ca. 1013-1016mm - ausserhalb von Regel 1, wodurch die
Bahn-Sperre fuer jede Bahn zublieb und keine einzige Weiche mehr erkannt
wurde. Die Sperre entfaellt; SWITCH_FALLBACK_MM bleibt die alleinige Schranke
gegen Fehlzuordnungen (der eigentliche Schutz vor dem urspruenglichen
Mubea-Fehlfund, der 5-9m entfernte Separatoren betraf).

derive_tros, angetriebene Strecken (Schritt 2): zwei Faelle stufen einen
Separator trotz Host "Strecke" als 1Sep statt Vario ein:

- Letzter Separator vor dem Wiedereinschleusen auf die Kreisel-Bahn (in
  TrackIds-Reihenfolge) - reiner Vereinzeler ohne eigene VarioDrive-Strecke,
  die Kreisel-Bahn treibt ab dort selbst an (HundM05 VF_3 vor Ort bestaetigt).
- Ein nicht-letzter Separator, dessen vorangehendes Segment "horizontal"
  (keine aktive Antriebsrichtung) gefuehrt wird, dabei aber tatsaechlich an
  Hoehe verliert (> 30mm) - ein unbetriebenes Gefaelle-Teilstueck innerhalb
  einer gemischten VF/GF-Streckengruppe (HundM05 VF_1/0035).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:20:18 +02:00
Samer AyadiandClaude Sonnet 5 df6d56090e Materialfluss: Ein-/Ausgabestationen im Chain-Graph zeichnen
BTMT Beladung/Entladung bzw. die generischen Eingang/Ausgang-Symbole waren
bislang von --chain-graph ausgenommen und wurden als Kantenziel einfach
uebersprungen. Die Anlage laeuft als Kreislauf, diese Stationen sind ihre
einzigen Materialquellen/-senken und gehoeren als eigene Knoten in jeden
Graphtyp (coarsebox/compound/subgraph).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:20:02 +02:00
Samer AyadiandClaude Sonnet 5 0477f05180 Materialfluss: --graphtype fuer --chain-graph (coarsebox/compound/subgraph)
--chain-graph zeichnete bisher immer nur die groben Objekte (Kreisel-Bahnen,
Strecke/Gefaellestrecke). Neuer Schalter --graphtype waehlt die Methode:
coarsebox (bisheriges Verhalten, unveraendert), compound (dieselbe
massstabsgetreue SVG, zusaetzlich die Anbauteile - Separator, Scanner,
Ausschleus-/Einschleuselement, Weiche - verschachtelt im Container an ihrer
echten Position, verbunden ueber die bereits aufgeloesten TrackGraph-Kanten)
und subgraph (dieselbe Verschachtelung als geschachtelte Graphviz-Cluster,
schematisch statt lagerichtig, <name>_chain.dot / mit --tosvg auch .svg).

_container_children() liest die Anbauteile direkt aus Element.track_ids -
derselben Quelle, die build_track_graph()/--track schon nutzt - statt sie wie
im Prototyp data/gen_chain_svg.py erneut aus der Nachbarn-Spalte herzuleiten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:26:02 +02:00
Samer AyadiandClaude Sonnet 5 b197bf8150 TRO-Fluss: Reihenfolge auf der Kreisel-Bahn auch fuer Weichen-TROs
Seit der Weichenerkennung (c788647/fa96a6e) bekommt ein Weichen-TRO als Host
die gefuetterte Strecke statt die Kreisel-Bahn (derive_tros Schritt 3b) - fachlich
richtig fuer den Materialfluss, aber die Positions-Verkettung in link_tros()
gruppierte bisher rein nach Host: sobald nur noch ein einzelner "echter"
Separator auf einer Bahn uebrig blieb (HundM05 Kreisel1 Bahn L: 0020, nachdem
0008/0010/0012/0014 eigene Weichen-TROs wurden), verschwand die Reihenfolge
0008->0010->0012->0014->0020 komplett - im TRO-Flussdiagramm sah es aus, als
haenge jede der 5 Abzweigungen direkt und gleichrangig an genau diesem einen
TRO (Review 2026-09-21: "es gibt viele unnoetige Linien").

_tro_bahn() bestimmt die Kreisel-Bahn eines TROs jetzt zusaetzlich ueber die
Zuordnung seines eigenen Separators, wenn der TRO-Host selbst keine Bahn ist -
link_tros() verkettet danach alle TROs derselben Bahn per Position, nicht nur
die mit identischem Host. Die bestehenden Materialfluss-Kanten (Weiche/Vario
-> Ruecklauf-Bahn) bleiben unveraendert bestehen; die Verkettung ergaenzt nur
die bislang fehlende Reihenfolge der Abzweige.

Die Colocated-Markierung ("gleiche Bahn", gepunktete Linie) wird jetzt pro
Paar statt pro TRO unterdrueckt (verkettete_paare statt verkettet als TRO-Id-
Menge) - sonst waere z. B. TRO01 (Weiche, auf der Bahn verkettet) faelschlich
auch von seiner Colocated-Markierung mit TRO02 (Vario, gleiche Strecke)
ausgenommen worden, obwohl das zwei unabhaengige Beziehungen sind.

HundM05.csv: 24 -> 28 Kanten (4 neue Bahn-Kanten 0008->0010->0012->0014->0020).
Mubea unveraendert (13 TROs, 55 Kanten), keine Fehler in beiden Projekten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 13:01:59 +02:00
77 changed files with 21351 additions and 1758 deletions

No files matched your search

+5
View File
@@ -68,3 +68,8 @@ ipython_config.py
/work
/log
/results
# Claude Code: persoenliche Einstellungen und TRO-Kriterien-Waechter
.claude/settings.local.json
.claude/hooks/
.claude/tro_criteria_baseline
+100 -37
View File
@@ -10,28 +10,39 @@ of Siemens SCL code** ("TRO" — Transfer Route Object — blocks for a conveyor
control system) that can be imported directly into TIA Portal.
The repository has moved past pure analysis: `lib/` now holds working Python tooling that
runs the **full DXF → JSON → SCL pipeline in a first version**. From a CSV export (ILS 2.0)
it derives a material-flow graph and a TRO list, annotates a copy of the BricsCAD drawing
with the result, reads the (possibly hand-edited) drawing back into a TRO-JSON, and emits an
`FB_Main` **SCL skeleton** from that JSON. The skeleton is deliberately not a running
runs the **full CSV → TRO-JSON → SCL pipeline in a first version**. From a CSV export (ILS
2.0) it derives a material-flow graph and a TRO list, writes the TROs as a TRO-JSON defined
by the shared pydantic model `lib/model.py`, and emits an `FB_Main` **SCL skeleton** from
that JSON. A copy of the BricsCAD drawing annotated with the same TROs serves as a viewer
for visual checking; it is no longer a data source. The skeleton is deliberately not a running
program — every value that must come from the electrical planning is left as a visible
`TODO(E-Planung)` gap rather than guessed. What is **not** done yet is the fuller layout-JSON
schema (`doc/HundM/Json_Layout-Konzept.md`) as an intermediate format and the generation of
the other per-controller blocks (`FB_CallSensors`, `FC_Direction`/`FC_Call_Jams` as
standalone files); see "Roadmap" below. `tests/` and `examples/` are still empty scaffolding
(see "Standard Programm Template" below). What exists today is:
standalone files); see "Roadmap" below. `tests/` holds the first tests
(`tests/test_model.py`); `examples/` is still empty scaffolding (see "Standard Programm
Template" below). What exists today is:
- `bin/` — environment/venv management scripts, plus one `.bat`/`.sh` wrapper pair per CLI
tool in `lib/` (see "Environment scripts" below)
- `lib/*.py` — CLI tools and libraries that turn a CSV export + BricsCAD DXF into a
material-flow graph, a derived TRO list/diagram, an annotated copy of the drawing, a
TRO-JSON read back out of that drawing, and an `FB_Main` SCL skeleton.
- `lib/*.py` — CLI tools and libraries that turn a CSV export into a material-flow graph,
a derived TRO list/diagram, a TRO-JSON (`lib/model.py`), an `FB_Main` SCL skeleton, and
(with the BricsCAD DXF) an annotated copy of the drawing for visual checking;
`lib/e_planning.py` assigns the E-planning's sensors/valves (from the cable-length
tool's `*_positionsdraw.json`) to the layout parts so the skeleton can carry real names.
See **`doc/Python_Scripts.md`** for what each script does and which switches it takes.
- `data/` — input CSV exports (gitignored, not committed)
- `cad/tro_types.lsp` — generated type list for the BricsCAD `TROEDIT` dialog (written by
`tro_annotate.py --emit-lisp`)
- `cfg/dxf_registration.json` — persisted, verified CSV↔DXF coordinate transform;
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`)
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`);
`cfg/e_planning.ini` — project-specific rules for assigning E-planning symbols to layout parts
(read by `merge_hw_layout.py`, also for `e_planning.py`);
`cfg/hw_overrides.ini` — hand corrections of `merge_hw_layout.py` (bindings per TRO slot role, conveyors);
`cfg/parameter.ini` — tolerances, thresholds, scales and drawing sizes of the `lib/` modules
(read by `lib/parameter.py`; no numeric settings in the code, see `doc/Python_Scripts.md` §7a);
`cfg/<csv>_tro_ids.ini` — per-plant TRO-ID file (versioned, IDs renamed by the SPS
programmers), created/extended by `tro_flow.py` via `lib/tro_ids.py`
- `doc/*.md` — analysis documents that reverse-engineer the SCL patterns and propose the
fuller JSON layout schema for the parts of the pipeline still to be built out
@@ -73,13 +84,12 @@ source bin/activate_venv.sh
```
Requires Python 3.10+. `requirements.txt` pins `pydantic>=2.0.0` (used throughout `lib/`
for validated models) and `ezdxf>=1.4.0` (DXF read/write, `tro_annotate.py` only); `pytest`
is commented out as not yet needed.
for validated models), `ezdxf>=1.4.0` (DXF read/write, `tro_annotate.py` only) and
`pytest>=9.0.0`.
There is no build/lint/test command configured yet — `tests/` is empty and no test
runner or linter is set up. Once tests exist, use `pytest` (already anticipated in
`requirements.txt`, just commented out) and set `PYTHONPATH` via `bin/setenv` first so
imports resolve against `lib/`.
Tests: `python -m pytest tests/` with `.venv` active. `tests/conftest.py` puts `lib/` on
`sys.path`, so `bin/setenv` is not required for the tests. No build step or linter is set
up.
## Domain model (from doc/)
@@ -89,8 +99,9 @@ this order for onboarding:
0. **`doc/Python_Scripts.md`** — the existing CLI tooling (`lib/material_flow.py`,
`lib/tro_flow.py`, `lib/tro_annotate.py`, `lib/tro_extract.py`, `lib/scl_skeleton.py`,
plus the `lib/tro_catalog.py`, `lib/dxf_registration.py` and `lib/tro_overrides.py`
libraries they share): what each script does, its switches, inputs/outputs. This is
`lib/e_planning.py`, plus the `lib/tro_catalog.py`, `lib/model.py`, `lib/dxf_registration.py`,
`lib/kreisel_junctions.py` (switches between touching Kreisel, from geometry and direction
of travel) and `lib/tro_overrides.py` libraries they share): what each script does, its switches, inputs/outputs. This is
working code, not a proposal — read it before touching `lib/`.
1. **`doc/HundM/Json_Layout-Konzept.md`** — the core proposal: a JSON file as single source
of truth (`plc`, `controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
@@ -134,6 +145,16 @@ this order for onboarding:
its generated images come from `doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py`
(doc-tooling, not part of the generator — see `doc/Python_Scripts.md` §8) built from
`doc/TRO_Katalog/connect.ini`.
9. **`doc/Datenquellen_Layout_Hardwareplanung.md`** — which TRO/SCL data comes from the
mechanical layout (CSV), which from the hardware planning (EA list, TIA import,
WSCAD, `positionsdraw`) and which is a default; two reference call sites annotated
line by line. The full raw field lists of both sources are in
`doc/Klassenhierarchie.md`, chapter "Daten".
10. **`doc/Konzept_Layout_Hardware_SCL.md`** — the target workflow (planned, not built):
layout → `<csv>_tro.json` (with slots, `jamAreas`), hardware planning →
`hw_import.py` → `<projekt>_hw.json`, `merge_hw_layout.py` → `<csv>_anlage.json`,
then `scl_skeleton.py`; what happens to `e_planning.py` (split),
`tro_annotate.py` (viewer reading the JSON) and `tro_extract.py` (dropped).
### Used libraries (referenced but not vendored)
@@ -146,19 +167,34 @@ local, fully-vendored SCL source (`FB_ILS_MTRO_Vario_workStation`, `FB_EmptyCarr
`doc/TRO_Katalog/scl_templates/*.scl` as **read-only reference material** for pattern
extraction, not code to execute or modify.
## Roadmap: DXF → JSON → SCL (first version implemented)
## Roadmap: CSV → TRO-JSON → SCL (first version implemented)
The DXF → JSON → SCL pipeline now runs end-to-end in a first version. After `tro_annotate.py`
burns the TROs into a copy of the drawing, the user can hand-edit that DXF in BricsCAD (via
the `TRO_INSERT`/`TRO_EDIT` commands, see `doc/HundM/BricsCAD_TRO_Symbol.md`) — moving TROs,
adding new ones, changing a type — and two further `lib/` tools carry the drawing the rest of
the way to importable SCL:
Since 2026-10-09 the main path is **CSV → `tro_flow.py --json` → TRO-JSON →
`scl_skeleton.py` → SCL**. Layer order (who may import whom): `tro_catalog.py` (TRO *types*)
← `lib/model.py` (TRO *instances* and the TRO-JSON: `TroBase` → `DerivedTro`, `Connection`,
`TroCandidate`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ←
outputs (`scl_skeleton.py`, `tro_annotate.py`, reports). `items`/`fbType` are always read
from the catalog via the TRO type, never stored. See `doc/Python_Scripts.md` §6b.
Inside the SCL output, `lib/scl_context.py` (TRO-JSON → render context, all derivations)
sits between `model.py` and `scl_skeleton.py` (context → text only). With `--templates`
(opt-in) REGIONs come from Jinja2 templates in `lib/scl_templates/<fbType>.scl.j2`
(all 7 types with a reference call site: `1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`,
`Vario`, `Vario_workStation`, `PinStore_Auto`; `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom`,
`2Sep2Swi` stay generic until a call site exists), each round-trip tested against its reference call site
in `doc/TRO_Katalog/scl_templates/` (`tests/test_scl_templates.py`); every line holding a
placeholder must carry `TODO(E-Planung)`. Migration steps, decisions per template line
and status: `doc/SCL_Templates.md`. Golden files in `tests/fixtures/scl/` pin the SCL output byte for
byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended change.
1. **`lib/tro_extract.py`** (implemented) — reads the (possibly hand-edited) annotated DXF
**only** (not the CSV) and derives a TRO-JSON (`<drawing>_tro.json`): the TROs tagged with
XDATA, their `predecessors`/`successors` computed from the flow arrows, and plant
coordinates resolved through the persisted registration. See `doc/Python_Scripts.md` §5a.
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON and emits an `FB_Main` SCL
1. **`lib/tro_flow.py --json`** (implemented) — writes `<csv>_tro.json` (schema
`sps_skel-tro-v4`, `model.TroDocument`): TROs with their hardware `slots`, system
boundaries `SYSn`, connections with their kind, `jamAreas` (one per TRO entry, key
`J<Separator>`, all predecessors that feed it), findings. TRO-IDs come from `cfg/<csv>_tro_ids.ini` (`lib/tro_ids.py`): each
TRO is found again by the position of its separators, so IDs survive a new layout export
(TeileIds do not); `--renumber` renumbers along the material flow, `--ids` points another
export of the same plant at the same file. See `doc/Python_Scripts.md` §4.
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON through `TroDocument` (a
wrong/renamed key or `items` not matching the catalog stops the run) and emits an `FB_Main` SCL
skeleton (`<source>_FB_Main.scl`): one `REGION` per TRO with the instance call and every
parameter line in the right order, plus inline `FC_Direction` calls for switch TROs.
Everything derivable from the layout is filled (REGION order, FB type, parameter-block
@@ -166,20 +202,47 @@ the way to importable SCL:
everything that must come from the electrical planning is a `TODO(E-Planung)` gap.
Switches `--json`, `--out`, `--start` (the last picks an entry point while the plant is a
closed loop with no loading/unloading station). See `doc/Python_Scripts.md` §5b.
3. **`lib/tro_annotate.py`** — **viewer only**: draws the same TROs (same `analyse_tros`
call as `tro_flow.py`) into a copy of the DXF for a visual check in BricsCAD.
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
write `model.TroDocument`. See `doc/Python_Scripts.md` §5a.
5. **`lib/e_planning.py`** (implemented, first version) — CSV + E-planning
`*_positionsdraw.json` → `<csv>_eplan.json` (`EplanDocument`): registers the E drawing to
the CSV (`dxf_registration.py`) and assigns BMKs to layout parts by rules in
`cfg/e_planning.ini` (distance, target distance, numbering rule `MB = BG + 1`).
`scl_skeleton.py --eplan` fills separator sensor and stopper valve from it; jam sensors
and outfeed/switch valves are assigned but not yet placed in the SCL. See
`doc/Python_Scripts.md` §5c and `doc/E-Planung_Bruecke.md`. Since 2026-10-15 its model
and assignment logic live in `hw_import.py`/`merge_hw_layout.py` (re-exported here).
6. **`lib/hw_import.py`** (implemented, stage 2 of `doc/Konzept_Layout_Hardware_SCL.md`) —
TIA import `*_TIA.xlsx` (PLC Tags + Constants, read without openpyxl) + positions file
`*_positionsconv.json` → `<projekt>_hw.json` (`HardwareDocument`): per BMK address,
index constant `cIn…`/`cOut…`, VERW, station, position, conveyor (`K0102`; `#106` →
`K0106`); drives MA↔FC (+ encoder GF); cross-check of both sources. EA list and WSCAD
are not needed. See `doc/Python_Scripts.md` §5d.
7. **`lib/merge_hw_layout.py`** (implemented, stage 3) — `<csv>_tro.json` + `<projekt>_hw.json`
→ `<csv>_anlage.json` (`model.AnlageDocument`: layout unchanged + `bindings[]` keyed by
TRO-ID + slot + role, `conveyors[]`). Reads no CSV: the TRO-JSON carries `parts`
(positions/bounding boxes, written by `tro_flow --json`). Conveyors are matched by
majority vote of the devices naming them; corrections in `cfg/hw_overrides.ini`,
numbers in `cfg/parameter.ini [merge_hw_layout]`. `scl_skeleton` does not read the plant JSON
yet (stage 4). See `doc/Python_Scripts.md` §5e.
What is **not** built out yet, relative to the original two-tool design:
- the fuller **layout-JSON schema** of `doc/HundM/Json_Layout-Konzept.md` (`plc`,
`controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
`emptyCarrBuffers[]`, `routing`, `jamAreas[]`, `scanners[]`, `connections[]`,
`destinations[]`) as the intermediate format — `tro_extract.py` currently emits a
TRO-focused JSON, not this full schema;
`destinations[]`) as the intermediate format — `model.TroDocument` is a TRO-focused
JSON, not this full schema yet;
- the **other per-controller blocks** (`FB_CallSensors`, `FC_Direction`/`FC_Call_Jams` as
standalone files) — only `FB_Main` is generated today;
- **type-based timing defaults** from `doc/TRO_Typen.md` and the `OVERRIDE_TIMING_JSON` CAD
attribute (timings are emitted as `TODO(E-Planung): pruefen` placeholders for now);
- a **`--skip-json`** convenience path that would read DXF + CSV directly and emit SCL
without an intermediate JSON file.
- `Component` classes per part type (Separator, Switch, Scanner, VarioDrive, …) on the TRO,
driving the per-part SCL emitters in `scl_skeleton.py`.
Note the `create_skel.py` / `guess_fbtype()` generator sketched in `doc/HundM/suggestion.md`
and `doc/HundM/Json_Layout-Konzept.md` §14.5 is a **separate concept** and is not implemented
@@ -203,12 +266,12 @@ sps_skel/
domain-model documents listed above
examples/ example files — empty for now
lib/ Python source, importable via SKEL_LIB on PYTHONPATH — CLI tools and
libraries that derive material flow / TRO lists / CAD annotations from a
layout, read the TROs back into JSON, and emit an FB_Main SCL skeleton
(see doc/Python_Scripts.md)
libraries that derive material flow / TRO lists from a layout, write
them as TRO-JSON (model.py), emit an FB_Main SCL skeleton, and draw
the TROs into the CAD drawing (see doc/Python_Scripts.md)
log/ gitignored
results/ gitignored — CLI tool output (.dot/.svg/.md/.dxf/.scl/.json)
tests/ unit tests — empty for now
tests/ unit tests (pytest) — test_model.py for the TRO-JSON model
```
When extending the pipeline (the remaining pieces in "Roadmap" above), put new modules under
+159 -100
View File
@@ -9,61 +9,110 @@
## Stand des Projekts
Aus einem CSV-Export der mechanischen Planung (ILS 2.0) werden automatisch
Die Kette **CSV → TRO-JSON → SCL** läuft in einer ersten Version durch. Aus
einem CSV-Export der mechanischen Planung (ILS 2.0) entstehen automatisch:
- der **Materialfluss-Graph** der mechanischen Objekte (Strecken, Gefällestrecken,
Kreisel),
- der **Materialfluss-Graph** der mechanischen Objekte (Strecken,
Gefällestrecken, Kreisel),
- die daraus abgeleitete **TRO-Liste** samt eigenem Flussdiagramm,
- eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem Marker-Symbol pro
TRO (danach in BricsCAD von Hand nachbearbeitbar — TROs verschieben,
ergänzen, Typ ändern),
- daraus zurückgelesen ein **JSON-Modell der TROs** (liest nur die Zeichnung,
erfasst also auch von Hand vorgenommene Korrekturen), und
- die **TRO-JSON** `<name>_tro.json` (Schema `sps_skel-tro-v4`, Modell
`lib/model.py`) — die Schnittstelle zwischen Ableitung und SCL-Erzeugung,
- daraus ein **SCL-Gerüst für `FB_Main`** — ein `REGION` pro TRO mit
Instanzaufruf und allen Parametern in der richtigen Reihenfolge, aber
bewusst nicht lauffähig: jeder Wert, der aus der E-Planung stammen muss
(Sensor-/Aktoradressen, Separator-/Weichennummern, Staubereiche, ungeprüfte
Zeiten), ist als `TODO(E-Planung)` markiert statt geraten.
Zeiten), ist als `TODO(E-Planung)` markiert statt geraten. Optional
(`--templates`) kommen die REGIONs aus Jinja2-Templates
(`lib/scl_templates/<fbType>.scl.j2`, für alle 7 Typen mit Referenz-Aufrufstelle),
- zur Sichtkontrolle eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem
Marker-Symbol pro TRO. Die Zeichnung ist nur Ansicht, keine Datenquelle für
die SCL.
Diese fünf Werkzeuge liegen als CLI-Tools in `lib/` und sind über
`bin/`-Wrapper aufrufbar (siehe unten). Details, Domänenmodell und die
verbleibende Roadmap stehen in `doc/`, allen voran `doc/Python_Scripts.md`.
Details stehen in `doc/`, allen voran
[`doc/Python_Scripts.md`](doc/Python_Scripts.md) (alle Tools und Schalter) und
[`doc/Klassenhierarchie.md`](doc/Klassenhierarchie.md) (Klassen und wozu sie da
sind).
## Roadmap
## Ablauf
Offen ist vor allem die JSON-Layout-Datei nach dem in
`doc/HundM/Json_Layout-Konzept.md` beschriebenen Schema (`plc`,
`controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
`emptyCarrBuffers[]`, `routing`, `jamAreas[]`, `scanners[]`, `connections[]`,
`destinations[]`) als von Hand editierbares Zwischenformat vor der
SCL-Erzeugung, sowie die Generierung der übrigen Bausteine je Steuerung
(`FB_CallSensors`, `FC_Direction`, `FC_Call_Jams`) — `scl_skeleton.py` deckt
bisher nur `FB_Main` ab. Siehe `CLAUDE.md` (Abschnitt "Roadmap") für den
aktuellen Stand der Planung dazu.
```text
data/<name>.csv (ILS-2.0-Export)
│
▼
material_flow ──► <name>_material_flow.dot/.svg/.md Materialfluss der Mechanik
│
▼
tro_flow --json ──► <name>_tro.json (+ .dot/.svg/.md) TROs ableiten
│ │
│ ▼
│ scl_skeleton ──► <name>_FB_Main.scl SCL-Gerüst
│
└── gleiche Ableitung ──► tro_annotate ──► <name>_annotated.dxf (nur Ansicht)
(braucht zusätzlich die DXF-Zeichnung)
```
Alle Ausgaben landen in `results/` (`%SKEL_RESULTS%`).
## Welche Skripte gibt es und wofür?
### Umgebung (`bin/`, je `.bat` für Windows und `.sh` für Linux/macOS)
Diese vier Skripte verwalten nur Umgebung und venv, sie rufen kein
Python-Programm auf.
| Skript | Wofür |
| --- | --- |
| `setenv` | Setzt `SPS_SKEL` und die `SKEL_*`-Variablen, hängt `SKEL_LIB` an `PYTHONPATH`, legt fehlende Ordner an und gibt alles zur Kontrolle aus. `.sh` muss gesourct werden. |
| `install_py` | Einmalig: legt `.venv` an und installiert `requirements.txt`. Bricht ab, wenn `.venv` schon existiert. |
| `activate_venv` | Je Sitzung: aktiviert `.venv` und zeigt Python-Version und Pakete. `.sh` muss gesourct werden. |
| `get_cmd` | Öffnet eine neue Shell mit gesetzten Umgebungsvariablen. |
### CLI-Tools (`lib/<name>.py`, aufgerufen über `bin/<name>.bat`/`.sh`)
Jeder Wrapper ruft `setenv`, aktiviert `.venv` (falls vorhanden) und startet
das Modul mit allen übergebenen Argumenten.
| Tool | Wofür | Wichtige Schalter |
| --- | --- | --- |
| `material_flow` | Liest den CSV-Export und baut den gerichteten **Materialfluss-Graphen** der mechanischen Objekte (Strecke, Gefällestrecke, Kreisel); Separatoren/Scanner hängen an ihrem Objekt. Grundlage für `tro_flow`. | `--file`, `--tosvg`, `--use-cords`, `--doc`, `--connect`, `--track`, `--show-bbs`, `--chain-graph` |
| `tro_flow` | Leitet aus demselben Graphen die **TROs** ab (Typ aus dem Trägerobjekt des Separators, Weichen aus den Abgängen), verknüpft sie und zeichnet ihren Fluss. Mit `--json` entsteht die TRO-JSON — der Hauptweg zur SCL. | `--file`, `--json`, `--tosvg`, `--use-cords`, `--doc`, `--connect` |
| `scl_skeleton` | Liest die TRO-JSON (geprüft gegen `lib/model.py`) und schreibt das **`FB_Main`-Gerüst** als SCL. Mit `--templates` aus den Jinja2-Templates, sonst generisch. | `--json`, `--out`, `--start`, `--templates [DIR]`, `--strict` |
| `tro_annotate` | Zeichnet dieselben TROs als Marker (Farbe/Form je Typ, Attribute `ID`/`TYPE`/`FB_BLOCK`) in eine **Kopie** der BricsCAD-Zeichnung — zur Sichtkontrolle. Bestimmt dafür den CSV↔DXF-Lagebezug und legt ihn in `cfg/dxf_registration.json` ab. Erzeugt mit `--emit-lisp` die Typliste `cad/tro_types.lsp` für den `TROEDIT`-Dialog. | `--file`, `--dxf`, `--flow`, `--fb`, `--legend`, `--offset`, `--re-register`, `--check`, `--emit-lisp` |
| `tro_extract` | **Geparkt.** Liest TROs aus der annotierten Zeichnung zurück in das alte JSON-Format `sps_skel-tro-extract-v1`, das `scl_skeleton` nicht mehr annimmt. Nicht gelöscht, nicht weiterentwickelt. | `--dxf` |
### Bibliotheken (`lib/`, ohne eigenen Aufruf)
| Modul | Wofür |
| --- | --- |
| `tro_catalog.py` | Katalog der TRO-**Typen**: Kurzname, Siemens-FB, Bauteile, Farbgruppe, CAD-Symbol. Einzige Quelle für FB-Typ und Bauteile. |
| `model.py` | Gemeinsames Modell der TRO-**Instanzen** und der TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`, `TroDocument`). |
| `scl_context.py` | Wandelt die TRO-JSON in den Render-Kontext für die SCL (Reihenfolge, Bauteilblöcke, Staubereiche, Weichenziele, offene Namen). |
| `scl_templates/` | Jinja2-Templates je FB-Typ (`<fbType>.scl.j2`) plus gemeinsame Makros; nur mit `scl_skeleton --templates`. |
| `dxf_registration.py` | Ermittelt, prüft und speichert die Transformation zwischen CSV- und DXF-Koordinaten. |
| `tro_overrides.py` | Liest die Handkorrekturen aus `cfg/tro_overrides.ini`; heute nur von `material_flow --connect` genutzt. |
### Sonstige Skripte
| Skript | Wofür |
| --- | --- |
| `doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | Doku-Werkzeug, nicht Teil der Pipeline: erzeugt die Typ-Schaubilder und Topologie-Graphen für `doc/TRO_Katalog/TRO_Katalog.md` aus `connect.ini`. Aufruf: `py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` |
| `cad/tro_types.lsp` | Generiertes BricsCAD-LISP (Typliste für den `TROEDIT`-Dialog), erzeugt von `tro_annotate --emit-lisp`. |
| `tests/` | pytest-Tests: TRO-Modell, Render-Kontext, Golden-Files der SCL-Ausgabe (`tests/fixtures/scl/`), Templates gegen die Referenz-Aufrufstellen. |
## Projektstruktur
```text
sps_skel/
bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool
setenv.bat/.sh Umgebungsvariablen setzen
install_py.bat/.sh venv erstellen + pip install
activate_venv.bat/.sh venv aktivieren
get_cmd.bat/.sh Shell mit Umgebung oeffnen
material_flow.bat/.sh Wrapper fuer lib/material_flow.py
tro_flow.bat/.sh Wrapper fuer lib/tro_flow.py
tro_annotate.bat/.sh Wrapper fuer lib/tro_annotate.py
tro_extract.bat/.sh Wrapper fuer lib/tro_extract.py
scl_skeleton.bat/.sh Wrapper fuer lib/scl_skeleton.py
cad/ generiertes BricsCAD-LISP (tro_types.lsp) fuer den TROEDIT-Dialog
cfg/ Konfigurationsdateien (INI/JSON); dxf_registration.json
data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git)
doc/ Dokumentation, siehe doc/Python_Scripts.md
examples/ Beispieldateien
lib/ Python-Quellcode (CLI-Tools + Bibliotheken)
log/ Log-Dateien (nicht im Git)
results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.dxf, nicht im Git)
tests/ Unit Tests
bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool
cad/ generiertes BricsCAD-LISP (tro_types.lsp) für den TROEDIT-Dialog
cfg/ Konfiguration: dxf_registration.json, tro_overrides.ini
data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git)
doc/ Dokumentation, siehe doc/Python_Scripts.md
examples/ Beispieldateien (noch leer)
lib/ Python-Quellcode (CLI-Tools, Bibliotheken, scl_templates/)
log/ Log-Dateien (nicht im Git)
results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.json/.scl/.dxf, nicht im Git)
tests/ Unit Tests (pytest)
.gitignore
LICENSE
README.md
@@ -72,94 +121,104 @@ sps_skel/
## Umgebungsvariablen
| Variable | Beschreibung |
|----------------|---------------------------|
| `SPS_SKEL` | Wurzelverzeichnis |
| `SKEL_BIN` | Skriptverzeichnis |
| `SKEL_LIB` | Python-Quellcode |
| `SKEL_CFG` | Konfigurationsdateien |
| `SKEL_DATA` | Eingabedaten |
| `SKEL_DOC` | Dokumentation |
| `SKEL_LOG` | Log-Dateien |
| `SKEL_RESULTS` | Ergebnisse |
| `SKEL_EXAMPLES` | Beispieldateien |
| `SKEL_TESTS` | Unit Tests |
| `PYTHONPATH` | Erweitert um `SKEL_LIB` |
| Variable | Beschreibung |
|-----------------|-------------------------|
| `SPS_SKEL` | Wurzelverzeichnis |
| `SKEL_BIN` | Skriptverzeichnis |
| `SKEL_LIB` | Python-Quellcode |
| `SKEL_CFG` | Konfigurationsdateien |
| `SKEL_DATA` | Eingabedaten |
| `SKEL_DOC` | Dokumentation |
| `SKEL_LOG` | Log-Dateien |
| `SKEL_RESULTS` | Ergebnisse |
| `SKEL_EXAMPLES` | Beispieldateien |
| `SKEL_TESTS` | Unit Tests |
| `PYTHONPATH` | Erweitert um `SKEL_LIB` |
## Installation
### Voraussetzungen
- Python 3.10 oder hoeher
- Graphviz (`dot`) auf `PATH` fuer `--tosvg`, optional aber empfohlen
- fuer `tro_annotate.py`: `ezdxf` (wird per `requirements.txt` installiert)
- Python 3.10 oder höher
- Pakete aus `requirements.txt`: `pydantic`, `jinja2` (SCL-Templates), `ezdxf`
(nur `tro_annotate`), `pytest`
- Graphviz (`dot`, für `--use-cords` bei `tro_flow` auch `neato`) auf `PATH`
für `--tosvg` — optional, aber empfohlen
### Setup (Windows)
### Setup
```bat
bin\install_py.bat
bin\install_py.bat # Windows
```
### Setup (Linux / macOS)
```sh
bash bin/install_py.sh
bash bin/install_py.sh # Linux / macOS
```
## Nutzung
### Umgebung setzen
### Umgebung
```bat
bin\setenv.bat # Windows
source bin/setenv.sh # Linux / macOS
bin\setenv.bat # Windows: nur Variablen setzen
bin\activate_venv.bat # Windows: venv aktivieren
bin\get_cmd.bat # Windows: neue Shell mit Umgebung
```
### Shell mit gesetzten Variablen oeffnen
```bat
bin\get_cmd.bat # Windows
source bin/get_cmd.sh # Linux / macOS
```sh
source bin/setenv.sh
source bin/activate_venv.sh
bash bin/get_cmd.sh
```
### venv aktivieren
### Typischer Durchlauf
`--file`/`--dxf` nehmen einen Dateinamen (aufgelöst gegen `%SKEL_DATA%`) oder
einen vollen Pfad; `--json` sucht in `%SKEL_RESULTS%`.
```bat
bin\activate_venv.bat # Windows
source bin/activate_venv.sh # Linux / macOS
```
### CLI-Tools ausführen
Jedes Tool wird über seinen `bin/<name>.bat`/`.sh`-Wrapper aufgerufen; dieser
setzt die Umgebung, aktiviert `.venv` und ruft dann das Python-Modul auf.
`--file`/`--dxf` akzeptieren entweder einen Dateinamen (wird gegen
`%SKEL_DATA%` aufgelöst) oder einen vollen Pfad; Ausgaben landen in
`%SKEL_RESULTS%`.
```bat
REM 1) Materialfluss der mechanischen Objekte
REM 1) Materialfluss der mechanischen Objekte (optional, zur Kontrolle)
bin\material_flow.bat --file mubea.csv --tosvg --doc
REM 2) TRO-Liste + eigenes Flussdiagramm ableiten
bin\tro_flow.bat --file mubea.csv --tosvg --doc
REM 2) TROs ableiten und als TRO-JSON schreiben
bin\tro_flow.bat --file mubea.csv --tosvg --doc --json
REM 3) TROs als Marker-Symbole in eine Kopie der CAD-Zeichnung einbrennen
REM 3) FB_Main-SCL-Geruest erzeugen (generisch bzw. aus Templates)
bin\scl_skeleton.bat --json mubea_tro.json
bin\scl_skeleton.bat --json mubea_tro.json --templates
REM 4) Optional: TROs zur Sichtkontrolle in eine Kopie der Zeichnung
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
REM (Zeichnung optional in BricsCAD von Hand nachbearbeiten)
REM 4) TROs aus der (ggf. bearbeiteten) Zeichnung zurueck in JSON lesen
bin\tro_extract.bat --dxf export_annotated.dxf
REM 5) FB_Main-SCL-Geruest aus dem JSON erzeugen
bin\scl_skeleton.bat --json export_tro.json --start TRO07
```
Alle Switches, Ausgabedateien und Exit-Codes der drei Tools sind in
[`doc/Python_Scripts.md`](doc/Python_Scripts.md) dokumentiert; das
Domänenmodell (TRO-Typen, geplantes JSON-Layout, I/O-Listen-Analyse) findet
sich in den übrigen Dokumenten unter `doc/`.
`--start TRO07` bei `scl_skeleton` ist nur nötig, solange die Anlage ein
geschlossener Kreis ohne Be-/Entladestation ist. Danach im erzeugten
`*_FB_Main.scl` nach `TODO(E-Planung)` suchen — das sind die Stellen, die aus
der E-Planung gefüllt werden müssen.
### Tests
```bat
python -m pytest tests/
```
mit aktivem `.venv`. Die Golden-Files in `tests/fixtures/scl/` legen die
SCL-Ausgabe byteweise fest; neu schreiben (`SKEL_UPDATE_GOLDEN=1`) nur bei einer
geprüften, gewollten Änderung.
## Roadmap
Noch offen:
- das volle **JSON-Layout-Schema** aus `doc/HundM/Json_Layout-Konzept.md`
(`plc`, `controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `jamAreas[]`,
`routing`, …) als Zwischenformat — die TRO-JSON ist bisher TRO-zentriert;
- die übrigen Bausteine je Steuerung (`FB_CallSensors`, `FC_Direction`,
`FC_Call_Jams` als eigene Dateien) — erzeugt wird bisher nur `FB_Main`;
- Templates für die weiteren FB-Typen (siehe `doc/SCL_Templates.md`);
- typabhängige Zeit-Defaults statt `TODO(E-Planung): pruefen`.
Details: Abschnitt "Roadmap" in `CLAUDE.md`.
## Lizenz
+33
View File
@@ -0,0 +1,33 @@
@echo off
REM ================================================================
REM SPS_SKEL - E-Planung den Bauteilen des Layouts zuordnen
REM ================================================================
REM Aufruf:
REM bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json
REM bin\e_planning.bat --file mubea.csv --eplan C:\pfad\Mubea_positionsdraw.json --re-register
REM
REM CSV und *_positionsdraw.json (Kabellaengen-Tool) werden in %SKEL_DATA%
REM gesucht (oder als Pfad angegeben). Regeln: %SKEL_CFG%\e_planning.ini.
REM Ergebnis <csv>_eplan.json und Bericht <csv>_eplan.md in %SKEL_RESULTS%.
REM
REM Weiter: bin\scl_skeleton.bat --json <name>_tro.json --eplan <name>_eplan.json
REM ================================================================
setlocal
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
)
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
"%PY%" "%SKEL_LIB%\e_planning.py" %*
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# ================================================================
# SPS_SKEL - E-Planung den Bauteilen des Layouts zuordnen
# ================================================================
# Aufruf:
# bash bin/e_planning.sh --file mubea.csv --eplan Mubea_positionsdraw.json
# bash bin/e_planning.sh --file mubea.csv --eplan /pfad/Mubea_positionsdraw.json --re-register
#
# CSV und *_positionsdraw.json (Kabellaengen-Tool) werden in $SKEL_DATA
# gesucht (oder als Pfad angegeben). Regeln: $SKEL_CFG/e_planning.ini.
# Ergebnis <csv>_eplan.json und Bericht <csv>_eplan.md in $SKEL_RESULTS.
#
# Weiter: bin/scl_skeleton.sh --json <name>_tro.json --eplan <name>_eplan.json
# ================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
# shellcheck source=/dev/null
. "$SCRIPT_DIR/setenv.sh" >/dev/null
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
# shellcheck source=/dev/null
. "$SPS_SKEL/.venv/bin/activate"
fi
# Python-Interpreter bestimmen
PY="python3"
command -v python3 >/dev/null 2>&1 || PY="python"
exec "$PY" "$SKEL_LIB/e_planning.py" "$@"
+33
View File
@@ -0,0 +1,33 @@
@echo off
REM ================================================================
REM SPS_SKEL - Hardwareplanung einlesen (TIA-Import + Positionsdatei)
REM ================================================================
REM Aufruf:
REM bin\hw_import.bat --tia Mubea\mubea-1_TIA.xlsx --positions Mubea\Mubea_positionsconv.json
REM bin\hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea
REM
REM TIA-Import und *_positionsconv.json bzw. *_positionsdraw.json werden in
REM %SKEL_DATA% gesucht (auch mit Unterordner, oder als Pfad angegeben).
REM Ergebnis <projekt>_hw.json und Bericht <projekt>_hw.md in %SKEL_RESULTS%.
REM
REM Weiter: bin\merge_hw_layout.bat --tro <csv>_tro.json --hw <projekt>_hw.json
REM ================================================================
setlocal
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
)
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
"%PY%" "%SKEL_LIB%\hw_import.py" %*
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# ================================================================
# SPS_SKEL - Hardwareplanung einlesen (TIA-Import + Positionsdatei)
# ================================================================
# Aufruf:
# bash bin/hw_import.sh --tia Mubea/mubea-1_TIA.xlsx --positions Mubea/Mubea_positionsconv.json
# bash bin/hw_import.sh --tia mubea-1_TIA.xlsx --project Mubea
#
# TIA-Import und *_positionsconv.json bzw. *_positionsdraw.json werden in
# $SKEL_DATA gesucht (auch mit Unterordner, oder als Pfad angegeben).
# Ergebnis <projekt>_hw.json und Bericht <projekt>_hw.md in $SKEL_RESULTS.
#
# Weiter: bin/merge_hw_layout.sh --tro <csv>_tro.json --hw <projekt>_hw.json
# ================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
# shellcheck source=/dev/null
. "$SCRIPT_DIR/setenv.sh" >/dev/null
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
# shellcheck source=/dev/null
. "$SPS_SKEL/.venv/bin/activate"
fi
# Python-Interpreter bestimmen
PY="python3"
command -v python3 >/dev/null 2>&1 || PY="python"
exec "$PY" "$SKEL_LIB/hw_import.py" "$@"
+34
View File
@@ -0,0 +1,34 @@
@echo off
REM ================================================================
REM SPS_SKEL - Layout und Hardwareplanung zusammenfuehren
REM ================================================================
REM Aufruf:
REM bin\merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json
REM bin\merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json --re-register
REM
REM TRO-JSON (tro_flow --json) und Hardware-JSON (hw_import) werden in
REM %SKEL_RESULTS% gesucht. Regeln: %SKEL_CFG%\e_planning.ini,
REM Handkorrekturen: %SKEL_CFG%\hw_overrides.ini.
REM Ergebnis <csv>_anlage.json und Bericht <csv>_anlage.md in %SKEL_RESULTS%.
REM
REM Vorher: bin\tro_flow.bat --file <csv> --json und bin\hw_import.bat
REM ================================================================
setlocal
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
call "%~dp0setenv.bat" >nul
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
)
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
set "PY=py"
py --version >nul 2>&1 || set "PY=python"
"%PY%" "%SKEL_LIB%\merge_hw_layout.py" %*
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# ================================================================
# SPS_SKEL - Layout und Hardwareplanung zusammenfuehren
# ================================================================
# Aufruf:
# bash bin/merge_hw_layout.sh --tro mubea_tro.json --hw Mubea_hw.json
# bash bin/merge_hw_layout.sh --tro mubea_tro.json --hw Mubea_hw.json --re-register
#
# TRO-JSON (tro_flow --json) und Hardware-JSON (hw_import) werden in
# $SKEL_RESULTS gesucht. Regeln: $SKEL_CFG/e_planning.ini,
# Handkorrekturen: $SKEL_CFG/hw_overrides.ini.
# Ergebnis <csv>_anlage.json und Bericht <csv>_anlage.md in $SKEL_RESULTS.
#
# Vorher: bin/tro_flow.sh --file <csv> --json und bin/hw_import.sh
# ================================================================
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
# shellcheck source=/dev/null
. "$SCRIPT_DIR/setenv.sh" >/dev/null
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
# shellcheck source=/dev/null
. "$SPS_SKEL/.venv/bin/activate"
fi
# Python-Interpreter bestimmen
PY="python3"
command -v python3 >/dev/null 2>&1 || PY="python"
exec "$PY" "$SKEL_LIB/merge_hw_layout.py" "$@"
+4 -4
View File
@@ -3,14 +3,14 @@ REM ================================================================
REM SPS_SKEL - FB_Main-Geruest als SCL erzeugen
REM ================================================================
REM Aufruf:
REM bin\tro_flow.bat --file mubea.csv
REM bin\tro_flow.bat --file mubea.csv --tosvg
REM bin\tro_flow.bat --file mubea.csv --tosvg --doc
REM bin\scl_skeleton.bat
REM bin\scl_skeleton.bat --json mubea_tro.json
REM bin\scl_skeleton.bat --json mubea_tro.json --out FB_Main.scl
REM
REM Die JSON-Datei wird in %SKEL_RESULTS% gesucht, die SCL-Datei wird
REM ebenfalls dort abgelegt.
REM
REM Schwesterprogramm: bin\material_flow.bat (Materialfluss der Objekte)
REM Vorstufe: bin\tro_flow.bat --file <name>.csv --json (CSV -> TRO-JSON)
REM ================================================================
setlocal
+1 -1
View File
@@ -10,7 +10,7 @@
# Die JSON-Datei wird in $SKEL_RESULTS gesucht, die SCL-Datei wird
# ebenfalls dort abgelegt.
#
# Vorstufe: bin/tro_extract.sh (Zeichnung -> JSON)
# Vorstufe: bin/tro_flow.sh --file <name>.csv --json (CSV -> TRO-JSON)
# ================================================================
set -eu
+1
View File
@@ -6,6 +6,7 @@ REM Aufruf:
REM bin\tro_flow.bat --file mubea.csv
REM bin\tro_flow.bat --file mubea.csv --tosvg
REM bin\tro_flow.bat --file mubea.csv --tosvg --doc
REM bin\tro_flow.bat --file mubea.csv --json (TRO-JSON fuer scl_skeleton)
REM
REM Die CSV-Datei wird in %SKEL_DATA% gesucht, DOT, SVG, Doku und
REM Fehlerdatei werden in %SKEL_RESULTS% abgelegt.
+1
View File
@@ -6,6 +6,7 @@
# bash bin/tro_flow.sh --file mubea.csv
# bash bin/tro_flow.sh --file mubea.csv --tosvg
# bash bin/tro_flow.sh --file mubea.csv --tosvg --doc
# bash bin/tro_flow.sh --file mubea.csv --json (TRO-JSON fuer scl_skeleton)
#
# Die CSV-Datei wird in $SKEL_DATA gesucht, DOT, SVG, Doku und
# Fehlerdatei werden in $SKEL_RESULTS abgelegt.
+1 -1
View File
@@ -1,4 +1,4 @@
;; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp
;; Quelle: TRO_CATALOG in lib/tro_catalog.py - nicht manuell aendern.
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi"))
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi" "3Sep2Swi"))
(princ)
+64 -7
View File
@@ -1,17 +1,17 @@
{
"mubea.csv|500573_60_1.dxf": {
"dx": 50905.678327433096,
"dx": 50833.7283274331,
"dy": 8414.490373798908,
"rotation": 0.0,
"source": "auto",
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:4, Separator:31; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
"matched": 34,
"candidates": 35,
"residual_max": 452.04557170439375,
"residual_mean": 102.6006793267277,
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:8, Separator:38; 22 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
"matched": 36,
"candidates": 46,
"residual_max": 487.77757110283585,
"residual_mean": 93.12692369578822,
"csv_file": "mubea.csv",
"dxf_file": "500573_60_1.dxf",
"created": "2026-07-30 15:32"
"created": "2026-10-02 12:45"
},
"export.csv|Mubea.dxf": {
"dx": 59.950000000000045,
@@ -26,5 +26,62 @@
"csv_file": "export.csv",
"dxf_file": "Mubea.dxf",
"created": "2026-08-12 10:09"
},
"HundM05.csv|HundM05.dxf": {
"dx": 0.0,
"dy": 0.0,
"rotation": 0.0,
"source": "cli",
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
"matched": 14,
"candidates": 26,
"residual_max": 360.0,
"residual_mean": 164.97465794329284,
"csv_file": "HundM05.csv",
"dxf_file": "HundM05.dxf",
"created": "2026-10-02 13:40"
},
"mubea.csv|mubea.dxf": {
"dx": 0.0,
"dy": 0.0,
"rotation": 0.0,
"source": "cli",
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
"matched": 20,
"candidates": 46,
"residual_max": 360.0,
"residual_mean": 256.77327549812964,
"csv_file": "mubea.csv",
"dxf_file": "mubea.dxf",
"created": "2026-10-02 13:40"
},
"dematic.csv|dematic_TST.dxf": {
"dx": 0.0,
"dy": 360.0,
"rotation": 0.0,
"source": "auto",
"anchor": "6 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:6, Separator:52; 6 Stimmen im 100-mm-Raster, 2 Kandidaten geprueft",
"matched": 17,
"candidates": 58,
"residual_max": 419.9499999999998,
"residual_mean": 235.56575392994918,
"csv_file": "dematic.csv",
"dxf_file": "dematic_TST.dxf",
"created": "2026-10-06 14:19"
},
"mubea.csv|Mubea_positionsconv.json": {
"dx": -151.4000000000001,
"dy": -696.5699999999997,
"rotation": 0.0,
"source": "auto",
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Separator:38; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
"matched": 23,
"candidates": 38,
"residual_max": 541.020430390572,
"residual_mean": 52.985147546665765,
"csv_file": "mubea.csv",
"dxf_file": "Mubea_positionsconv.json",
"created": "2026-10-15 13:15",
"fingerprint": "085df1af63e4b71d8a51301ea0c6e7812d415b5efd0c6b409c58cf2cb77ba4e2"
}
}
+82
View File
@@ -0,0 +1,82 @@
; ================================================================
; e_planning.ini - Regeln fuer lib/e_planning.py
; ================================================================
; Ordnet die Sensoren/Ventile der E-Planung (*_positionsdraw.json des
; Kabellaengen-Tools) den Bauteilen des Layouts (CSV) zu. Hintergrund und
; Befunde: doc/E-Planung_Bruecke.md.
;
; [Registrierung]
; anker VERW-Muster der E-Symbole, die auf einem Separator sitzen;
; daraus wird der Lagebezug E-Zeichnung -> CSV ermittelt.
;
; Jeder Abschnitt "<Art>.<Rolle>" ist eine Regel; sie werden in dieser
; Reihenfolge angewendet, ein E-Bauteil wird hoechstens einmal vergeben.
; layout Muster fuer die Layout-Art (Separator, Ausschleus,
; Einschleus, Weiche, Scanner, Beladung, Entladung).
; Standard: der Teil vor dem Punkt im Abschnittsnamen.
; verw Muster fuer das E-Attribut VERW (fnmatch, Komma-Liste,
; Gross-/Kleinschreibung egal)
; praefix BMK-Praefix (BG, MB ...), leer = egal
; max_mm raeumlich: Hoechstabstand nach dem Lagebezug in mm
; soll_mm raeumlich: Sollabstand - das Symbol liegt immer gleich weit
; neben dem Bauteil; gewertet wird die Abweichung vom Soll
; (nicht der kleinste Abstand), zulaessig bis toleranz_mm
; toleranz_mm zulaessige Abweichung vom Sollabstand (Standard: cfg/parameter.ini
; [merge_hw_layout] toleranz_mm)
; nummer_von Nummernregel: BMK-Nummer = Nummer der genannten Rolle
; nummer_versatz ... plus dieser Versatz (Mubea: Ventil = Sensor + 1)
; Mit nummer_von wird zuerst die Nummer versucht, danach (wenn max_mm oder
; soll_mm gesetzt) der Abstand fuer die uebrigen Bauteile.
;
; Lieber eine Luecke als ein falscher Name: was keine Regel sicher trifft,
; bleibt in der SCL TODO(E-Planung) und steht im Bericht <csv>_eplan.md.
;
; Die VERW-Texte und die Nummernregel sind projektabhaengig. Die Werte hier
; stammen aus Mubea (500573); in den Referenz-SCL (UH01..UH05) haben Sensor
; und Stopper dieselbe Nummer (BG3242/MB3242) -> dort nummer_versatz = 0.
; ================================================================
[Registrierung]
anker = IN SEP
; Sensor im Stopper -> stInSeparator<n>.stSenInSep
; sitzt in der Zeichnung auf dem Separator (Mubea Speicher: 0-1 mm).
; An den Kreiseln liegen Sensoren 200-550 mm daneben, teils naeher an
; Aus-/Einschleuselementen - unsicher, deshalb bewusst eng.
[Separator.sensor]
verw = IN SEP
praefix = BG
max_mm = 300
; Stopperventil -> xOutStopper<n>
; liegt ~270 mm neben dem Separator, im 800-mm-Raster der Speicherlinien
; naeher am Nachbarn als ueber die Nummer -> Nummernregel zuerst
[Separator.stopper]
verw = SEP
praefix = MB
nummer_von = Separator.sensor
nummer_versatz = 1
max_mm = 600
soll_mm = 270
; Stausensor hinter dem Ausschleuselement (Mubea: konstant ~1390 mm)
[Ausschleus.stau]
verw = STAU*
praefix = BG
max_mm = 1600
soll_mm = 1390
; Ausschleusventil (Mubea: konstant ~1414 mm, Nummer = Stausensor + 1)
[Ausschleus.ventil]
verw = AE ABZW*
praefix = MB
nummer_von = Ausschleus.stau
nummer_versatz = 1
max_mm = 1600
soll_mm = 1414
; Weichenventil an einer Kreisel-Weiche
[Weiche.ventil]
verw = ABZWEIG-*, WEICHE*
praefix = MB
max_mm = 1000
+67
View File
@@ -0,0 +1,67 @@
; ============================================================
; TRO-IDs zu export.csv - erzeugt von lib/tro_flow.py am 2026-10-14
;
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
; ============================================================
[tro]
TRO01 = 6133.6, 302.6 ; 1Sep | Separator 0017 auf Kreisel4 Bahn L | Sep 0017
TRO02 = 4540.0, -3169.7 ; 1Sep1Swi | Separator 0014 an ILS Weiche 0029 | Sep 0014
TRO03 = 5340.0, -6534.2 ; 1Sep1Swi | Separator 0015 an ILS Weiche 0028 | Sep 0015
TRO04 = 6933.6, -6618.2 ; 1Sep | Separator 0016 auf Kreisel4 Bahn R | Sep 0016
TRO05 = 7149.2, 1886.0 ; 1Sep | Separator 0018 auf Kreisel6 Bahn R | Sep 0018
TRO06 = 6933.6, 2908.5 ; 1Sep | Separator 0019 auf Kreisel6 Bahn R | Sep 0019
TRO07 = 4540.6, -1493.5 ; 1Sep1Swi | Separator 0013 an ILS Weiche 0024 | Sep 0013
TRO08 = 3699.4, 905.8 ; 1Sep | Separator 0012 auf Kreisel8 Bahn L | Sep 0012
TRO09 = 1244.4, 105.8 ; 1Sep | Separator 0011 auf Kreisel8 Bahn R | Sep 0011
TRO10 = -461.8, 1526.1 ; 1Sep | Separator 0010 auf Kreisel9 Bahn L | Sep 0010
; neu aus export.csv am 2026-10-15
TRO11 = 10310.0, -440.1 ; 1Sep | Separator 0042 auf Kreisel3 Bahn R | Sep 0042
TRO12 = 10310.0, 159.9 ; 1Sep | Separator 0043 auf Kreisel3 Bahn R | Sep 0043
TRO13 = 10310.0, -5013.1 ; 1Sep | Separator 0044 auf Kreisel3 Bahn R | Sep 0044
TRO14 = 9510.0, 14217.0 ; 1Sep | Separator 0045 auf Kreisel3 Bahn L | Sep 0045
TRO15 = 958.0, 12300.0 ; PinStore_Auto | Speicher (20 Schwerkraftlinien) | Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
TRO16 = 3299.6, 13509.6 ; Vario | VF_1 | Sep 0054
TRO17 = 8953.1, 13509.6 ; 1Sep | VF_1 | Sep 0055
TRO18 = 400.0, -3978.1 ; 1Sep1Swi | Separator 0005 an Ausschleuselement 0053 (Strecke 0013, Anfang) | Sep 0005
TRO19 = 3299.6, -4743.4 ; Vario | VF_2 | Sep 0058
TRO20 = 8953.1, -4743.4 ; 1Sep | VF_2 | Sep 0059
TRO21 = -400.0, -3554.9 ; 2Sep2Swi | Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125 | Sep 0012, 0036
TRO22 = 5355.1, -5878.0 ; 1Sep | Separator 0040 auf Kreisel1 Bahn R | Sep 0040
TRO23 = -400.0, 13920.0 ; 1Sep | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
TRO24 = -400.0, -562.0 ; 1Sep | Separator 0011 auf Kreisel2 Bahn L | Sep 0011
TRO25 = -898.9, 1134.6 ; Vario | VF_3 | Sep 0062
TRO26 = -703.0, 12307.8 ; 1Sep | VF_3 | Sep 0063
TRO27 = -1026.0, 10723.0 ; 1Sep | VF_3 | Sep 0037
TRO28 = -1026.0, 5250.1 ; 1Sep | VF_3 | Sep 0038
; neu aus export.csv am 2026-10-15
TRO29 = 34716.5, 3406.7 ; Vario | VF_4 | Sep 0071
TRO30 = 20161.7, 3406.7 ; 1Sep | VF_4 | Sep 0072
TRO31 = 42164.0, 4329.5 ; 1Sep2Swi | Separator 0048 an Ausschleuselementen 0070, 0078 (Strecken 0008, 0010) | Sep 0048
TRO32 = 40437.8, 2448.3 ; Vario | VF_6 | Sep 0079
TRO33 = 33048.6, 2448.3 ; 1Sep | VF_6 | Sep 0080
TRO34 = 26860.8, 2448.3 ; Vario | VF_6 | Sep 0081
TRO35 = 20134.0, 2448.3 ; 1Sep | VF_6 | Sep 0082
TRO36 = 20161.9, 6635.8 ; Vario | VF_5 | Sep 0075
TRO37 = 20161.9, 11560.3 ; 1Sep | VF_5 | Sep 0076
TRO38 = 19604.0, 4982.3 ; 1Sep1Swi | Separator 0043 an Ausschleuselement 0074 (Strecke 0009, Anfang) | Sep 0043
TRO39 = 3098.7, 12119.7 ; 1Sep | Separator 0038 auf Kreisel2 Bahn L | Sep 0038
TRO40 = 34990.4, 12919.7 ; 1Sep | Separator 0046 auf Kreisel2 Bahn R | Sep 0046
TRO41 = 5034.1, 12919.7 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0040, 0085, 0088, 0091, 0094, 0097, 0100, 0103, 0106, 0109, 0112
TRO42 = 2703.7, 21969.7 ; Vario | VF_1 | Sep 0059
TRO43 = 2703.7, 13476.7 ; 1Sep | VF_1 | Sep 0060
TRO44 = 3542.3, 24478.8 ; 1Sep1Swi | Separator 0039 an Ausschleuselement 0058 (Strecke 0005, Anfang) | Sep 0039
TRO45 = 14674.7, 21969.7 ; Vario | VF_2 | Sep 0063
TRO46 = 14674.7, 13476.7 ; 1Sep | VF_2 | Sep 0064
TRO47 = 15472.4, 24478.8 ; 1Sep1Swi | Separator 0041 an Ausschleuselement 0062 (Strecke 0006, Anfang) | Sep 0041
TRO48 = 23232.7, 21969.7 ; Vario | VF_3 | Sep 0067
TRO49 = 23232.7, 13476.7 ; 1Sep | VF_3 | Sep 0068
TRO50 = 24062.0, 24478.8 ; 1Sep1Swi | Separator 0044 an Ausschleuselement 0066 (Strecke 0007, Anfang) | Sep 0044
TRO51 = 15934.1, 12919.8 ; PinStore_Auto | Speicher (7 Schwerkraftlinien) | Sep 0042, 0115, 0118, 0121, 0124, 0127, 0130, 0133
TRO52 = 24734.2, 12919.8 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0045, 0136, 0139, 0142, 0145, 0148, 0151, 0154, 0157, 0160, 0163
TRO53 = 35122.8, 25278.8 ; 1Sep | Separator 0047 auf Kreisel3 Bahn R | Sep 0047
+30
View File
@@ -0,0 +1,30 @@
; ================================================================
; hw_overrides.ini - Handkorrekturen fuer lib/merge_hw_layout.py
; ================================================================
; Was die Regeln (cfg/e_planning.ini) nicht oder falsch zuordnen, wird hier
; von Hand gesetzt. Angewendet nach der automatischen Zuordnung; im Bericht
; <csv>_anlage.md mit Regel "hand".
;
; Je Anlage (Name der CSV, aus der die TRO-JSON stammt) zwei Abschnitte:
;
; [<csv>.bindings]
; <TRO-ID>.<Slot>.<Rolle> = <BMK> BMK setzen (muss im TIA-Import stehen)
; <TRO-ID>.<Slot>.<Rolle> = - Zuordnung loeschen, Rolle bleibt offen
; TRO-IDs aus cfg/<csv>_tro_ids.ini (stabil), Slots/Rollen aus der
; TRO-JSON (separator1.sensor, separator1.stopper, line3.sensor,
; drive.motor ...).
;
; [<csv>.conveyors]
; <Foerderer E-Planung> = <Layout-ID> z. B. K0102 = 0010
; <Foerderer E-Planung> = - Zuordnung loeschen
; Achtung: die Layout-ID (TeileId) aendert sich mit jedem CSV-Export.
;
; Beispiel (auskommentiert):
;
; [mubea.csv.bindings]
; TRO03.separator1.sensor = BG0532
; TRO03.separator1.stopper = MB0533
;
; [mubea.csv.conveyors]
; K0105 = 0001
; ================================================================
+30
View File
@@ -0,0 +1,30 @@
; ============================================================
; TRO-IDs zu mubea.csv - erzeugt von lib/tro_flow.py am 2026-10-15
;
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
; ============================================================
[tro]
TRO01 = 10310.0, -440.1 ; 1Sep | Separator 0042 auf Kreisel3 Bahn R | Sep 0042
TRO02 = 10310.0, 159.9 ; 1Sep | Separator 0043 auf Kreisel3 Bahn R | Sep 0043
TRO03 = 10310.0, -5013.1 ; 1Sep | Separator 0044 auf Kreisel3 Bahn R | Sep 0044
TRO04 = 958.0, 12300.0 ; PinStore_Auto | Speicher (20 Schwerkraftlinien) | Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
TRO05 = -400.0, -3554.9 ; 1Sep | Separator 0012 auf Kreisel2 Bahn L | Sep 0012
TRO06 = 5355.1, -5878.0 ; 1Sep | Separator 0040 auf Kreisel1 Bahn R | Sep 0040
TRO07 = -898.9, 1134.6 ; Vario | VF_3 | Sep 0062
TRO08 = -703.0, 12296.5 ; 1Sep | VF_3 | Sep 0063
TRO09 = -400.0, -562.0 ; 1Sep1Swi | Separator 0011 an Ausschleuselement 0061 (Strecke 0015, Anfang) | Sep 0011
TRO10 = -1026.0, 10723.0 ; 1Sep | VF_3 | Sep 0037
TRO11 = -1026.0, 5250.1 ; 1Sep | VF_3 | Sep 0038
TRO12 = 3299.6, 13509.6 ; Vario | VF_1 | Sep 0054
TRO13 = 8953.1, 13509.6 ; 1Sep | VF_1 | Sep 0055
TRO14 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 an Ausschleuselement 0053 (Strecke 0013, Anfang) | Sep 0003
TRO15 = 9510.0, 14217.0 ; 1Sep | VF_1 | Sep 0045
TRO16 = 3299.6, -4743.4 ; Vario | VF_2 | Sep 0058
TRO17 = 8953.1, -4743.4 ; 1Sep | VF_2 | Sep 0059
TRO18 = 400.0, -3978.1 ; 2Sep2Swi | Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125 | Sep 0005, 0036
+192
View File
@@ -0,0 +1,192 @@
; ================================================================
; parameter.ini - Zahlenwerte der Bibliotheken in lib/
; ================================================================
; Gelesen von lib/parameter.py. Ein Abschnitt je Modul, der Schluessel ist
; der Name der Konstanten im Modul in Kleinbuchstaben:
; [kreisel_junctions] contact_tolerance -> CONTACT_TOLERANCE
;
; Diese Datei ist die versionierte Grunddatei und muss vollstaendig sein -
; fehlt ein Schluessel, bricht das Programm ab (kein Ersatzwert im Code).
; Eine weitere parameter.ini in %SKEL_CFG% (falls SKEL_CFG woanders hin
; zeigt) ueberschreibt einzelne Werte fuer eine Anlage.
;
; Laengen in mm, Winkel in Grad, sofern nicht anders angegeben.
; Werte, die das Ergebnis der Ableitung aendern (Toleranzen, Schwellen),
; nur mit Begruendung und Vergleich der Ausgaben aller Anlagen aendern.
; ================================================================
[kreisel_junctions]
; Hoechster Abstand zweier Kreisel-Umlaeufe, der noch als Beruehrung gilt.
contact_tolerance = 50.0
; Hoechster Abstand eines Separators vom Umlauf, damit er auf ihm liegt.
; Auch die Grenze fuer "liegt auf Kreisel" im Mouseover von
; material_flow.py --track.
separator_tolerance = 100.0
; Hoechster Abstand einer exportierten ILS Weiche von ihrem Beruehrpunkt.
weiche_tolerance = 300.0
; Abtastschritt beim Suchen des Beruehrpunkts.
sample_step = 10.0
; Mindestanzahl Abtastpunkte je Umlauf, auch bei sehr kleinen Kreiseln.
min_samples = 8
; Bei tangentialer Beruehrung sind viele Abtastpunkte fast gleich nah: alle
; bis zu diesem Mehrabstand ueber dem naechsten gehen in den Mittelpunkt ein.
contact_tie = 1.0
; Ein Beruehrbereich, der laenger ist, liegt nicht an einem Punkt an (zwei
; parallele Bahnen nebeneinander) - Hinweis statt Weiche.
max_contact_length = 500.0
; Ein Punkt bis zu diesem Abstand vor dem Ende der Geraden zaehlt schon zur
; Umlenkung. Zwei Kreisel, deren Umlenkungen sich beruehren, tun das oft genau
; am Uebergang Gerade/Halbkreis (export.csv: Kreisel 5 an Kreisel 6 und 4).
end_tolerance = 100.0
; Radius, wenn die Boundingbox ihn nicht liefert (schraeg liegender Kreisel).
default_radius = 400.0
; Halbe Schrittweite fuer die Laufrichtung an einer Stelle des Umlaufs
; (Differenz zweier Punkte davor und dahinter).
direction_step = 1.0
; Gleichlauf am Beruehrpunkt: Mindest-Kosinus des Winkels zwischen beiden
; Laufrichtungen (0.5 = hoechstens 60 Grad). Darunter "gegenlaeufig".
gleichlauf_min_cos = 0.5
[tro_flow]
; Ab dieser Anzahl zusammenhaengender Schwerkraftlinien (Gefaellestrecke mit
; je einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen.
min_store_lanes = 2
; Mindest-Hoehenverlust auf dem vorangehenden TrackIds-Segment, damit ein
; Separator auf einer "Strecke" trotzdem als 1Sep statt Vario gilt (siehe
; _incoming_drop). Muss nur "horizontal" gefuehrte, aber tatsaechlich
; Gefaelle-Segmente erwischen (HundM05 Separator 0035: -104mm auf 3,8m), nicht
; das Rauschen eines echten flachen VF-Laufs (HundM05 Separator 0036: +26mm
; auf 0,5m). 30mm liegt bewusst weit unter dem Befund und ueber dem Rauschen.
gefaelle_drop_mm = 30.0
; Zeichnungsmassstab fuer --use-cords in pt/mm: Graphviz rechnet in Punkt, die
; Anlage in Millimeter. Seit die Symbole eine feste Groesse haben, reichen
; 0.1 pt/mm: ~11 x 20 m Anlage ergeben etwa 1100 x 2000 pt. Vorher 0.25
; (riesige Zeichnung, Symbole und Schrift verschwanden; Review 2026-10-01).
coord_scale = 0.1
; Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
; ab dem angehaengten TRO (siehe _station_positionen).
coord_pfeil_laenge = 1300
; Groesste Luecke zwischen zwei benachbarten Knotenkoordinaten je Achse in der
; lagerichtigen Zeichnung; groessere Leerstrecken werden auf diesen Wert
; gekuerzt (Review 2026-10-01). --true-scale schaltet das ab.
coord_max_luecke = 1500
; Abstand der Legende rechts neben der Anlage.
legend_margin = 3000
; Oberkante der Legende so weit unter der Anlagenoberkante.
legend_drop = 1000
[tro_ids]
; Abstand zwischen Eintrag der ID-Datei und Separator, bis zu dem ein TRO als
; wiedergefunden gilt (Standard fuer --id-tolerance). Separatoren derselben
; Bahn liegen deutlich weiter auseinander; ein verschobenes Bauteil faellt so
; als "neu" auf.
default_tolerance = 100.0
[material_flow]
; Max. Abstand, ab dem ein generisches Eingang/Ausgang-Symbol als
; "deckungsgleich" mit einer BTMT-Station gilt und verschluckt wird. Die
; beobachteten Paare liegen bei 256-431 mm, echte getrennte Stationen >1000 mm.
station_merge_mm = 600.0
; Ab dieser Anzahl paralleler Gefaellestrecken zwischen denselben zwei
; Kreisel-Bahnen werden sie im Materialfluss-Diagramm zu einem Block
; zusammengefasst - sonst wird eine grosse Linienbank unlesbar breit.
min_gefaelle_group = 2
; Zeichnungsmassstab fuer --use-cords in pt/mm. Achtung: tro_flow nutzt seit
; Review 2026-10-01 0.1 - beide Graphen haben damit verschiedene Massstaebe.
coord_scale = 0.25
; Die beiden Bahnen (L/R) eines Kreisels teilen sich eine Position; im
; lagerichtigen Layout so weit versetzen, damit sie nicht uebereinander liegen.
lane_nudge_mm = 350.0
; Legende im lagerichtigen Layout so weit unterhalb der Anlage.
legend_offset_mm = 3000.0
; Legende (--show-bbs): Kaestchen- und Schriftgroesse.
bbs_legend_box_mm = 250.0
bbs_legend_font_mm = 180.0
; --show-bbs: Rand um die Anlage herum und minimale Schriftgroesse.
bbs_margin_mm = 500.0
bbs_min_font_mm = 40.0
; Anschlusspunkte K1-K4 (--show-kx): Groesse des Kreuzes und der Beschriftung.
kx_cross_mm = 120.0
kx_font_mm = 90.0
; --chain-graph: Hoechstgroesse der Zeichenflaeche, Rand und Titelhoehe (px).
chain_max_w = 1400.0
chain_max_h = 850.0
chain_margin = 70.0
chain_title_h = 60.0
[dxf_registration]
; Rastergroesse der Abstimmung.
vote_grid = 100.0
; So viele Kandidaten der Abstimmung werden ausgeglichen und bewertet.
vote_candidates = 6
; Nur Paare bis zu diesem Abstand gehen in den Ausgleich ein (exakte Treffer).
fit_tol = 50.0
; Punkte gelten als zugeordnet, wenn sie nach der Verschiebung so nah liegen.
default_match_tol = 600.0
; So viele exakte Treffer muessen es mindestens sein.
min_inliers = 5
; Ab dieser Drehung (Grad) wird gewarnt.
rotation_warn_deg = 0.05
[tro_annotate]
; Zeichnungsgroessen, wenn nicht per --marker-size/--text-height angegeben.
default_marker = 300.0
default_text = 220.0
; Restfehler des Lagebezugs, ab dem er abgelehnt wird.
default_max_residual = 600.0
[merge_hw_layout]
; Zulaessige Abweichung vom Sollabstand (soll_mm), wenn eine Regel in
; e_planning.ini kein toleranz_mm angibt (die Regeln liest merge_hw_layout, auch
; fuer e_planning).
toleranz_mm = 150.0
; Foerderer der E-Planung (K0102) -> Layout-Foerderer (Kreisel3) per Mehrheit:
; ein Feldgeraet, dessen Klartext den Foerderer nennt, stimmt fuer den
; Layout-Foerderer, dessen Huellbox ihm am naechsten liegt - hoechstens so
; weit entfernt (mm, nach dem Lagebezug).
conveyor_max_mm = 1500.0
; Zugeordnet wird erst ab so vielen Stimmen ...
conveyor_min_votes = 2
; ... und diesem Anteil an allen Geraeten, die den Foerderer nennen (0..1).
conveyor_min_share = 0.6
; Warnung, wenn der Lagebezug fuer weniger als diesen Anteil der
; Layout-Separatoren einen Partner findet (0..1) - Hinweis auf verschiedene
; Planungsstaende von Layout und E-Zeichnung.
registration_min_share = 0.8
+191
View File
@@ -0,0 +1,191 @@
# Command Lines — Overview
**As of:** 2026-10-05
Every command this project provides, in one place. This is the quick reference;
for what each tool derives, how, and the full description of every switch, see
[`Python_Scripts.md`](Python_Scripts.md).
All commands live in `bin/` as a `.bat` (Windows) / `.sh` (Linux/macOS) pair.
Examples below use the Windows form; on Linux/macOS replace `bin\x.bat` with
`bash bin/x.sh` (or `source bin/x.sh` where noted).
## 1. Environment commands
These set up or enter the environment. They never run a Python tool.
| Command | Purpose |
|---|---|
| `bin\setenv.bat` / `source bin/setenv.sh` | Sets `SPS_SKEL` and `SKEL_BIN/LIB/CFG/DATA/DOC/LOG/RESULTS/EXAMPLES/TESTS`, prepends `SKEL_LIB` to `PYTHONPATH`, creates missing folders. Windows: `--keep-open` pauses at the end. |
| `bin\install_py.bat` / `bash bin/install_py.sh` | One-time: creates `.venv` and installs `requirements.txt`. Does nothing if `.venv` already exists. |
| `bin\activate_venv.bat` / `source bin/activate_venv.sh` | Each session: sets the environment and activates `.venv` (error if missing — run `install_py` first). |
| `bin\get_cmd.bat` / `bash bin/get_cmd.sh` | Opens a new shell with the environment variables set. |
## 2. Pipeline tools
The five CLI tools, in pipeline order. Each wrapper sets the environment,
activates `.venv` if present and runs `lib/<name>.py` with all arguments passed
through. Every tool accepts `--help`.
```
CSV ─► material_flow ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
│ (model.py) (validates)
└─► tro_annotate (+ DXF) ─► <name>_annotated.dxf viewer only
parked: tro_extract (DXF → JSON), see 2.4
```
Shared conventions:
- Input names (`--file`, `--dxf`) are a bare filename resolved against
`%SKEL_DATA%`, or a full path. `scl_skeleton` (and the parked `tro_extract`)
look up their input in `%SKEL_RESULTS%` instead (it is the output of the
previous step).
- All output goes to `%SKEL_RESULTS%`.
- `--tosvg` needs Graphviz `dot` (`neato` with `--use-cords`) on `PATH`, or
`GRAPHVIZ_DOT` pointing at `dot`.
### 2.1 `material_flow` — material-flow graph of the mechanical objects
Input: CSV export. Details: [`Python_Scripts.md` §3](Python_Scripts.md#3-material_flowpy--material-flow-graph-of-the-mechanical-layout).
| Switch | Argument | Default | Effect |
|---|---|---|---|
| `--file` | `NAME` | `export.csv` | CSV input. |
| `--tosvg` | — | off | Also render `.svg` via Graphviz. |
| `--use-cords` / `--use-coords` | — | off | Place nodes at plant coordinates (`neato -n`). Only with `--tosvg`. |
| `--doc` | — | off | Write Markdown report `<name>_material_flow.md`. |
| `--connect` | — | off | Write topology as `<name>_connect.ini`. |
| `--show-bbs` | `flow` \| `all` | `flow` if given without value | Write top-down view `<name>_bbs.svg` (no Graphviz). `all` also draws VF/GF chain detail rows. |
| `--show-kx` | — | off | Only with `--show-bbs`: draw connection points K1–K4. |
| `--track` | — | off | Write fine-grained track graph `<name>_track.dot` (`.svg` with `--tosvg`). |
| `--chain-graph` | — | off | Write chain/layout plan of the coarse objects (`<name>_chain.svg` / `.dot`). |
| `--graphtype` | `coarsebox` \| `compound` \| `subgraph` | `coarsebox` | Only with `--chain-graph`: rendering method. |
Exit codes: `0` ok · `1` input/CLI error · `2` SVG rendering failed.
```bat
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\material_flow.bat --file HundM05.csv --show-bbs all --show-kx
bin\material_flow.bat --file HundM05.csv --chain-graph --graphtype subgraph --tosvg
```
### 2.2 `tro_flow` — derive and draw the TROs
Input: CSV export. Details: [`Python_Scripts.md` §4](Python_Scripts.md#4-tro_flowpy--derive-and-draw-the-tros).
| Switch | Argument | Default | Effect |
|---|---|---|---|
| `--file` | `NAME` | `export.csv` | CSV input. |
| `--tosvg` | — | off | Also render the TRO flow diagram as `.svg`. |
| `--use-cords` / `--use-coords` | — | off | Place TROs at plant coordinates (`neato -n`). Only with `--tosvg`. |
| `--true-scale` | — | off | Only with `--use-cords`: draw to scale (default shortens empty stretches > 1500 mm). |
| `--doc` | — | off | Write Markdown report `<name>_tro_doc.md`. |
| `--connect` | — | off | Write TRO topology as `<name>_tro_connect.ini`. |
| `--json` | — | off | Write the TRO-JSON `<name>_tro.json` (model `lib/model.py`) — input for `scl_skeleton`. |
Exit codes: `0` ok · `1` input/CLI error · `2` SVG rendering failed ·
`3` TRO validation errors (`<name>_tro_errors.md` written).
```bat
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\tro_flow.bat --file mubea.csv --json
```
### 2.3 `tro_annotate` — burn the TROs into a copy of the drawing
Viewer only: shows the same TROs as `tro_flow` in the drawing for a visual
check; the drawing is not read back as data.
Input: CSV export + original BricsCAD DXF. The original is never modified.
Details: [`Python_Scripts.md` §5](Python_Scripts.md#5-tro_annotatepy--burn-the-tros-into-a-copy-of-the-cad-drawing).
| Switch | Argument | Default | Effect |
|---|---|---|---|
| `--file` | `NAME` | `export.csv` | CSV input. |
| `--dxf` | `NAME` | — | Original drawing (DXF). Required unless `--emit-lisp` is used alone. |
| `--out` | `NAME` | `<csv-stem>_annotated.dxf` | Output filename. |
| `--flow` | — | off | Draw flow arrows between TROs. |
| `--fb` | — | off | Show the Siemens FB block name on the marker. |
| `--legend` | — | off | Draw a color-group legend. |
| `--marker-size` | `MM` | `300.0` | Marker radius in mm. |
| `--text-height` | `MM` | `220.0` | Text height in mm. |
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand (`0,0` for dxfmakros drawings). |
| `--re-register` | — | off | Re-derive the offset even if a cached one exists in `%SKEL_CFG%`. |
| `--check` | — | off | Dry run: analyze and report, write nothing. |
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` (or under `%DXFM_LISP%`) and exit. |
Exit codes: `0` ok · `1` input/CLI error · `4` coordinate registration not
provable — use `--offset`.
```bat
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
bin\tro_annotate.bat --file mubea.csv --dxf Mubea.dxf --offset 0,0 --check
bin\tro_annotate.bat --emit-lisp
```
### 2.4 `tro_extract` — read the TROs back out of the drawing into JSON
**Parked** — kept, but not part of the main path. Its JSON (schema
`sps_skel-tro-extract-v1`) is rejected by `scl_skeleton`; use
`tro_flow --json` instead.
Input: the (possibly hand-edited) annotated DXF only, never the CSV.
Details: [`Python_Scripts.md` §5a](Python_Scripts.md#5a-tro_extractpy--read-the-tros-back-out-of-the-drawing-into-json).
| Switch | Argument | Default | Effect |
|---|---|---|---|
| `--dxf` | `NAME` | `export_annotated.dxf` | Annotated drawing (looked up in `%SKEL_RESULTS%`). |
| `--out` | `NAME` | `<drawing>_tro.json` | Output JSON filename. |
Exit codes: `0` ok · `1` input/CLI error · `2` no TRO markers in the drawing.
```bat
bin\tro_extract.bat --dxf mubea_annotated.dxf
```
### 2.5 `scl_skeleton` — generate the `FB_Main` SCL skeleton
Input: TRO-JSON from `tro_flow --json`, validated against `lib/model.py` — a
wrong or renamed key stops the run (exit `1`) instead of producing SCL. Output
is a skeleton with `TODO(E-Planung)` gaps, not a compilable program.
Details: [`Python_Scripts.md` §5b](Python_Scripts.md#5b-scl_skeletonpy--generate-the-fb_main-skeleton-as-scl).
| Switch | Argument | Default | Effect |
|---|---|---|---|
| `--json` | `NAME` | `export_tro.json` | TRO-JSON input (looked up in `%SKEL_RESULTS%`). |
| `--out` | `NAME` | `<source>_FB_Main.scl` | Output SCL filename. |
| `--start` | `TRO` | — | TRO where the REGION order begins. Only needed while the flow is a closed loop without a loading/unloading station (`SYSn`). |
| `--templates` | `[DIR]` | off | Build REGIONs from the FB templates (`lib/scl_templates` if `DIR` is omitted); FB types without a template stay generic. Without it: the previous output, unchanged. See [`SCL_Templates.md`](SCL_Templates.md). |
| `--strict` | — | off | With `--templates`: exit `1` and write nothing if any FB type of the plant has no template. |
Exit codes: `0` ok (also when TROs without a template were built generically) · `1`
input/CLI error, JSON does not match the model, template error, or a missing template
with `--strict`.
```bat
bin\scl_skeleton.bat --json mubea_tro.json
bin\scl_skeleton.bat --json mubea_tro.json --start TRO01
bin\scl_skeleton.bat --json mubea_tro.json --templates
```
## 3. End-to-end example
```bat
bin\activate_venv.bat
bin\material_flow.bat --file mubea.csv --tosvg --doc
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json
bin\scl_skeleton.bat --json mubea_tro.json
REM optional, for a visual check in BricsCAD:
bin\tro_annotate.bat --file mubea.csv --dxf Mubea.dxf --flow --legend
```
Tests: `python -m pytest tests/` (with `.venv` active).
## 4. Documentation tooling (not part of the pipeline)
| Command | Purpose |
|---|---|
| `py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | Regenerates the per-type SVGs, the edge legend and the UH01–UH05 topology graphs for `TRO_Katalog.md` from `doc/TRO_Katalog/connect.ini`. No switches, no `bin/` wrapper. Needs Graphviz `dot`. |
The libraries `lib/tro_catalog.py`, `lib/model.py`, `lib/dxf_registration.py`
and `lib/tro_overrides.py` have no command line of their own.
+246
View File
@@ -0,0 +1,246 @@
# Datenquellen für TROs und SCL: mechanisches Layout und Hardwareplanung
Stand 2026-10-11. Welche Angaben für die TRO-Liste und die erzeugten `.scl`-Bausteine
aus dem **mechanischen Layout** kommen, welche aus der **Hardwareplanung** (EA-Liste
und TIA-Import), und was als **Defaultvorbelegung** gesetzt wird. Die vollständige
Liste der Rohangaben beider Quellen (Spalten, Merkmale, Attribute) steht in
`doc/Klassenhierarchie.md`, Kapitel "Daten", Abschnitte "Angaben 1" und "Angaben 2";
die Zuordnung jedes SCL-Parameters zu seiner Quelle in den Tabellen danach.
Wie beide Quellen künftig über eine gemeinsame JSON zum SCL werden:
`doc/Konzept_Layout_Hardware_SCL.md`.
## Die drei Quellen
| Quelle | Dateien | Liefert im Kern |
|---|---|---|
| **Mechanisches Layout** (Anlagen-Hardware) | CSV-Export ILS 2.0 (`data/*.csv`), aus der BricsCAD-Layoutzeichnung | **Struktur**: welche Bauteile, wer folgt auf wen, Drehrichtung, Längen und Höhen, Anzahl Separatoren/Scanner |
| **Hardwareplanung** (E-Planung) | `<projekt>-<n>_EA.xlsx`, `<projekt>-<n>_TIA.xlsx` (PLC Tags + Constants), `<projekt>-<n>_WSCAD.xlsx`, `<zeichnung>_positionsdraw.json` | **Namen und Adressen**: jedes Signal mit BMK, Adresse, Index-Konstante, Rolle, Steuerung, Schaltschrank; mit `positionsdraw` auch die Lage |
| **Defaultvorbelegung** | Typ-Katalog (`lib/tro_catalog.py`), Namensregeln (`scl_context.py`), Projektkonstanten | alles, was weder Layout noch E-Planung kennen: Zeiten, Konstanten-Namen nach Regel, Freigaben, WCS-Ziele |
Die Schlüssel zwischen den Quellen:
```text
Layout-Bauteil (TeileId) --[Lage, e_planning.py]--> E-Symbol (BMK, positionsdraw)
E-Symbol (BMK) --[Name]-----------------> TIA PLC Tag (Adresse, Kommentar)
--[cIn<BMK>]--------------> TIA Constant (Index in DB_Inputs)
E/A-Adresse --[Anschluss]-------------> WSCAD Bezug (Feldstation, Karte)
```
EA-Liste und TIA-Import allein haben **keine Lage**, `positionsdraw` hat **keine
Adresse**. Erst über das BMK als gemeinsamen Schlüssel wird aus "Sensor neben
Separator 0008" ein vollständiger SCL-Eintrag mit Adresse und Indexkonstante.
## Was die Hardwareplanung liefert
### Für die TROs
| Angabe | aus | Regel |
|---|---|---|
| Signal gehört zu welchem Layout-Bauteil | `positionsdraw` `pos` | räumlich, `cfg/e_planning.ini` (heute umgesetzt) |
| Rolle des Signals im TRO | `positionsdraw` `VERW`; ersatzweise Kommentar der EA/TIA-Liste | `IN SEP` → Sensor im Separator, `SEP` → Stopperventil, `STAU_REF-LT` → Stausensor, `AE ABZWEIG` → Ausschleusventil, `BT BELADEN` → Beladesensor, `MT in Position`/`ABFR.FINGER`/`conveyor full` → Vario |
| Welche optionalen Sensoren verbaut sind | vorhanden / nicht vorhanden je Rolle | ergibt die `xPlugged*`-Schalter |
| Fördereinheit (M-Nummer) | Kommentar `(ILS-CV M0108)`, `BEZEICHNUNG` `KR-K0102` | Instanzname des Antriebs (`#fbCarousel0102`), Name der Staubereiche (`DB_JamArea0102`) |
| Steuerung, TRO-Nummernkreis | `KENNZEICHNUNG` `=A1+UH01-KF102`, Dateinummer `-<n>`, `mappings` | `UH01` → TRO 1xx |
| Schaltschrank / Bedienfeld | `KENNZEICHNUNG`, WSCAD `Bezug`, `schaltschrank_elemente` | `stInOutMachineState`, Taster `SF…` |
| Motor ↔ Motorschutz | `MA`/`FC` mit gleicher Nummer | `xOutVarioMotor`, `xInMotorProtection` |
| Safety-Signale | WSCAD `Bezug` auf F-Karte (`KF1FDQ…`) | Not-Halt-Kreis, nur Hinweis |
| Plausibilisierung des TRO-Typs | Anzahl "in Separator n", "AE Abzweig", "Pin Abfrage" je M-Nummer | Gegenprüfung des aus dem Layout abgeleiteten Typs |
### Für die SCL-Dateien
| Ziel | aus | Bemerkung |
|---|---|---|
| Namen in `FB_Main` (`=> "MB3306"`, `:= "FC0145"`, `"SF0002"`) | TIA PLC Tags `Name` | 1:1 |
| Sensor-Indizes `"DB_Inputs".Sensors["cInBG3306"]` | TIA Constants `cIn<BMK>` | 1:1; jede Konstante, die der Generator schreibt, muss in diesem Blatt stehen → **Prüfung möglich** |
| `FB_CallSensors`, Größe `DB_Inputs` | alle `cInBG…`/`cInFC…` mit `Value` | noch nicht generiert |
| Variablen- und Konstantentabelle im TIA Portal | der TIA-Import selbst | wird nicht generiert, sondern importiert |
| `stInOutMachineState`, `xInAutomatic`, `xInErrorQuit` | Schrankelemente `SF…`, `KENNZEICHNUNG` | heute `TODO(E-Planung)` |
| `xInSftyOk` | Safety-Programm; WSCAD nur als Hinweis | heute `TODO(E-Planung)` |
| Scanner `BX…`, Cognex-Variablen | **nicht** in EA/TIA-Tagliste (PROFINET) | TIA-Gerätekonfiguration oder `positionsdraw` mit `BX` |
## Was das mechanische Layout liefert
### Für die TROs
| Angabe | aus | Regel / Modul |
|---|---|---|
| Existenz und Grenzen der TROs | Separatoren (`ILS 2.0 Separator`) und ihr Host (`Zuordnung`, `TrackIds`) | `tro_flow.analyse_tros()` |
| TRO-Typ und damit FB | Host Kreisel/Strecke/Gefälle, Weiche, Aus-/Einschleuselement, Scanner | `doc/500573_Mubea/TRO_Identifikation_500573.md`, `tro_catalog` |
| Anzahl Separatoren, Weichen, Scanner je TRO | `Zuordnung`, `Naechster Separator`, Weichen-Merkmale | Parameterblöcke `stInSeparator<n>`, `stInSwitch<n>` |
| Vorgänger/Nachfolger | `Nachbarn` (mit Bahn `-L`/`-R`), `TrackIds`, `Drehrichtung` | `material_flow` → Kanten, Ein-/Ausgangs-Staubereiche |
| Systemgrenzen | `BTMT Beladung`/`Entladung`, `Eingang`/`Ausgang` | `SYSn`, `xIsBorner` |
| Ziele je Weichenabgang | Erreichbarkeit im TRO-Graphen | `nTo1Destinations`, Zielliste |
| Vorfahrt nötig | Einschleuselement / zwei Ströme in einen Staubereich | `xPrioDir`, `xReleaseOk` |
| Tragende Fördertechnik | Host-Kreisel bzw. Gefälle | `xInCarouselRun1/2` |
| angetriebene Strecke | `L_VF_m`, `Antriebfahrtrichtung` `Auf` | Vario |
| Längen | `L_VF_m`, `L_GF_m`, Kreiselabstand | Grundlage für `tConvEmpty`, Staulängen |
### Für die SCL-Dateien
REGION-Reihenfolge, FB-Typ, Instanzname, Zahl und Reihenfolge der Parameterzeilen,
Staubereichs-Verkettung (Exit von A = Entr von B), `FC_Direction`-Aufruf mit dem
Carrier aus dem Eingangs-Staubereich, `nTo1Destinations`, Cognex-Dummy bei TROs ohne
Scanner. Das ist heute der Teil, den `scl_skeleton.py` schon ohne E-Planung füllt.
## Überschneidungen: gegenseitige Prüfung
Beide Quellen beschreiben dieselbe Anlage, aber unabhängig voneinander. Wo sie sich
überschneiden, prüft die eine die andere:
| Angabe | Layout | Hardwareplanung |
|---|---|---|
| Separatoren je Strecke/Kreisel | `Anzahl_Separator`, `Zuordnung` | Anzahl `IN SEP`/"in Separator n" je M-Nummer |
| Weiche/Abzweig vorhanden | `ILS Weiche`, `ILS Ausschleuselement` | `AE ABZWEIG`, `ABZWEIG-K104`, `ES branch` |
| Scanner | `ILS 2.0 Scanner` | `BX…` (nur in `positionsdraw` mancher Projekte) |
| angetriebene Strecke | `L_VF_m`, `Auf` | `MA…`/`FC…` mit Bezeichnung der Strecke |
| Lage | `Position` | `pos` (nach Lagebezug `dxf_registration`) |
Ein Widerspruch (z. B. Layout 6 Separatoren, E-Planung 5 `IN SEP`) heißt meist:
unterschiedliche Planungsstände. Er gehört in den Bericht `<csv>_eplan.md`, nicht
stillschweigend in die SCL.
## Beispielzeilen mit Herkunft
Zwei reale Aufrufstellen aus `doc/TRO_Katalog/scl_templates/` (UH01), Zeile für Zeile
mit der Quelle als Kommentar:
- **Layout** = mechanisches Layout (CSV)
- **HW-Planung** = Hardwareplanung (EA-Liste, TIA-Import, WSCAD, `positionsdraw`)
- **Default** = Defaultvorbelegung (Typ-Default, Namensregel, Projektkonstante)
Bei zwei Quellen steht die führende zuerst: "HW-Planung + Layout" heißt, der Name
kommt aus der Hardwareplanung, die Zuordnung zu diesem TRO bzw. Ausgang aus dem
Layout. Der Status heute (Generator setzt / `TODO(E-Planung)`) steht in den
Tabellen von `doc/Klassenhierarchie.md`.
Die Codeblöcke sind als `pascal` ausgezeichnet: Markdown-Renderer (VS Code,
GitHub, Gitea) kennen kein `scl`, und SCL ist Pascal-basiert. So werden
Schlüsselwörter, Zuweisungen, Zahlen und `//`-Kommentare hervorgehoben.
### `FB_ILS_MTRO_1Sep1Swi` (TRO 107, Separator mit Weiche am Kreisel)
```pascal
REGION TRO 107(1Sep1Swi) // Layout: Typ aus Separator am Kreisel + Ausschleuselement; Nummer 107: Default (Nummernkreis UH01 aus HW-Planung)
#TRO107.stInSeparator1.Settings.nSeparatorNo := "cSep107.1"; // Default: Namensregel cSep<TRO>.<n>; Anzahl Separatoren: Layout
#TRO107.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short"; // Default; Short/Medium je Host (Kreisel/Strecke): Layout
#TRO107.stInSeparator1.Settings.tTrailingTime := t#100ms; // Default: Typ-Default, bei IBN prüfen
#TRO107.stInSeparator1.Settings.tHandlingTime := t#50ms; // Default: Typ-Default, bei IBN prüfen
#TRO107.stInSeparator1.Settings.tJam := "TimeForSepJam"; // Default: Projektkonstante
#TRO107.stInSeparator1.Settings.xPluggedJam := TRUE; // HW-Planung: Stausensor (VERW STAU_REF-LT) vorhanden
#TRO107.stInSeparator1.Settings.xPluggedPart := FALSE; // HW-Planung: kein Beladesensor (VERW BT BELADEN)
#TRO107.stInSeparator1.Settings.xPluggedScanner := FALSE; // Layout: kein ILS 2.0 Scanner mit "Naechster Separator" = dieser
#TRO107.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG3306"]; // HW-Planung + Layout: BG mit VERW IN SEP am Separator; cInBG3306 aus TIA Constants
#TRO107.stInSenJam2 := "DB_Inputs".Sensors["cInBG3305"]; // HW-Planung + Layout: Stausensor; Ausgang 2 = Weiterfahrt auf dem Kreisel
#TRO107.stInSenJam3 := "DB_Inputs".Sensors["cInBG3330"]; // HW-Planung + Layout: Stausensor; Ausgang 3 = Abzweig am Ausschleuselement
#TRO107.stInSwitch1.Settings.nSwitchNo := "cSwi107.1"; // Default: Namensregel cSwi<TRO>.<n>; Anzahl Weichen: Layout
#TRO107.stInSwitch1.Settings.xPluggedExSen3 := FALSE; // HW-Planung: Abgangssensor Exit 3 verbaut?
#TRO107.stInSwitch1.Settings.xPluggedExSen4 := FALSE; // HW-Planung: Abgangssensor Exit 4 verbaut?
#TRO107.stInSwitch1.Settings.nTo1Destinations := 3; // Layout: über Abgang 1 erreichbare Ziele (reachable_via)
#TRO107.stInSettings.xPrioDir2 := TRUE; // Layout: Zusammenführung vor Ausgang 2 -> Vorfahrt nötig
#TRO107(stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.udt_TRO107.TroIn, // Default: Namensregel udt_TRO<n>; ob nötig: Layout
stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.udt_TRO107.TroOut, // Default: Namensregel
nInMainTroNo := "cMainTro107", // Default: Namensregel cMainTro<n>
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q, // HW-Planung: Not-Halt-Kreis OF1 (Safety-Programm, F-Karten im WSCAD)
xInAllRdyToStart := TRUE, // Default
xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed, // Layout: Host-Kreisel; Name 0102: HW-Planung (KR-K0102, MA/FC-Bezeichnung)
xInRelease := TRUE, // Default: Freigabe/Testlogik
stInOutMachineState := #stInOutControlUnitCabinetOF1, // HW-Planung: Schaltschrank OF1 (KENNZEICHNUNG, WSCAD Bezug)
stInOutCognexInterface := #stCognexInterfaceDummy, // Layout: kein Scanner am TRO
arInOutJamEntr1 := "DB_JamArea0102".stJam0102_1.arCarrier, // Layout: Vorgänger-Staubereich; DB-Name 0102: HW-Planung (Kreisel K0102) + Default (Index _1)
stInOutJamEntr1 := "DB_JamArea0102".stJam0102_1.stData, // wie oben
arInOutJamExit2 := "DB_JamArea0103".stJam0103_1.arCarrier, // Layout: Nachfolger an Ausgang 2; DB-Name: HW-Planung + Default
stInOutJamExit2 := "DB_JamArea0103".stJam0103_1.stData, // wie oben
arInOutJamExit3 := "DB_JamArea0102".stJam0102_2.arCarrier, // Layout: Nachfolger an Ausgang 3; DB-Name: HW-Planung + Default
stInOutJamExit3 := "DB_JamArea0102".stJam0102_2.stData, // wie oben
stInOutHMI := "DB_Interface_HMI".stTRO.TRO107, // Default: Namensregel
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro107"], // Default: Namensregel
xOutStopper1 => "MB3306", // HW-Planung: MB mit VERW SEP, Nummernregel ab BG3306 (TIA PLC Tag)
xOutSw1ExTo3 => "MB3304"); // HW-Planung + Layout: MB AE ABZWEIG am Ausschleuselement von Ausgang 3
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi107.1", // Default: Namensregel
nOutDirection => #TRO107.nInDirection, // Default: fest
InOutCarrier := "DB_JamArea0102".stJam0102_1.arCarrier[1]); // Layout: Carrier aus dem Eingangs-Staubereich
END_REGION
END_REGION
```
### `FB_ILS_MTRO_Vario` (TRO 115, angetriebene Strecke mit Separator)
```pascal
REGION TRO 115 (Vario) // Layout: Strecke mit L_VF_m / Antriebfahrtrichtung "Auf"; HW-Planung bestätigt (MA, MT in Position)
#TRO115.stInSeparator1.Settings.nSeparatorNo := "cSep115.1"; // Default: Namensregel; Anzahl Separatoren: Layout
#TRO115.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short"; // Default; Wahl je Host: Layout
#TRO115.stInSeparator1.Settings.tTrailingTime := t#800ms; // Default: Typ-Default Vario, bei IBN prüfen
#TRO115.stInSeparator1.Settings.tHandlingTime := t#300ms; // Default: Typ-Default Vario, bei IBN prüfen
#TRO115.stInSeparator1.Settings.tJam := "TimeForSepJam"; // Default: Projektkonstante
#TRO115.stInSeparator1.Settings.xPluggedJam := FALSE; // HW-Planung: kein Stausensor am Separator
#TRO115.stInSeparator1.Settings.xPluggedPart := FALSE; // HW-Planung: kein Beladesensor
#TRO115.stInSeparator1.Settings.xPluggedScanner := FALSE; // Layout: kein Scanner
#TRO115.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG3025"]; // HW-Planung + Layout: BG IN SEP am Separator; Index aus TIA Constants
#TRO115.stInSeparator1.Settings.tSenFree := t#2000ms; // Default: Typ-Default
#TRO115.stInSeparator1.Settings.tSenWait := T#5000ms; // Default: Typ-Default
#TRO115.stInVario.Settings.xContinousEmptying := FALSE; // Default: Prozessentscheidung
#TRO115.stInVario.Settings.tWaitForJog := T#300ms; // Default: Typ-Default
#TRO115.stInVario.Settings.tConvEmpty := T#45000ms; // Layout: Förderlänge L_VF_m / Geschwindigkeit (Default) -> heute fest
#TRO115.stInVario.Settings.nCountCarrier := 1; // Default: Prozessentscheidung
#TRO115.stInVario.Settings.tWaitForStop := t#3s; // Default: Typ-Default
#TRO115.stInVario.Settings.tCarrierInPosDelay := t#1200ms; // Default: Typ-Default
#TRO115.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms; // Default: Typ-Default
#TRO115.stInVario.Settings.tRasterHigh := t#5s; // Default: Typ-Default
#TRO115.stInVario.Settings.tRasterLow := t#5s; // Default: Typ-Default
#TRO115.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG3026"]; // HW-Planung: VERW "MT in Position" an dieser Strecke
#TRO115.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG3027"]; // HW-Planung: VERW "Finger in position" / ABFR.FINGER
#TRO115.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG3000"]; // HW-Planung: VERW "conveyor full"
#TRO115.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG3001"]; // HW-Planung: VERW "Jam detector" / STAU_REF-LT
#TRO115.stInPriorityManager.xReleaseOk := TRUE; // Layout: keine Zusammenführung -> keine Vorfahrt
#TRO115.stInVario.Settings.xJamSenDelayActive := TRUE; // Default (nur sinnvoll, wenn HW-Planung einen Stausensor hat)
#TRO115.stInVario.Settings.tJamSenDelay := T#2s; // Default: Typ-Default
#TRO115.stInSettings.xIsBorner := TRUE; // Layout: erster TRO nach Systemgrenze (Beladung/Eingang)
#TRO115(nInMainTroNo := "cMainTro115", // Default: Namensregel
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q, // HW-Planung: Not-Halt-Kreis
xInMotorProtection := "FC0145", // HW-Planung: FC mit Nummer des Motors (schaltschrank_elemente, TIA "Meldekontakt Motorschutzschalter")
xInAllRdyToStart := TRUE, // Default
xInReset := "cTodoFalse", // Default
xInErrorQuit := "SF0002", // HW-Planung: Quittiertaster (SF, Schrank)
xInAutomatic := #t_Automatik_OF1 AND "SF0120", // HW-Planung: Schlüsselschalter SF0120; Verknüpfung: Default
xInRelease := "DB_TRO_TEST".TRO115, // Default: Test-/IBN-Freigabe
arInOutJamEntr := "DB_JamArea4104".stJam4104_1.arCarrier, // Layout: Vorgänger-Staubereich; Nummer: HW-Planung (M-Nummer) + Default
stInOutJamEntr := "DB_JamArea4104".stJam4104_1.stData, // wie oben
arInOutJamFinger := "DB_JamArea4103".stJam4103_1.arCarrier, // Layout: Staubereich auf der Vario-Strecke selbst; Nummer: HW-Planung + Default
stInOutJamFinger := "DB_JamArea4103".stJam4103_1.stData, // wie oben
stInOutMachineState := #stInOutControlUnitCabinetOF1, // HW-Planung: Schaltschrank OF1
stInOutCognexInterface := #stCognexInterfaceDummy, // Layout: kein Scanner
stInOutHMI := "DB_Interface_HMI".stTRO.TRO115, // Default: Namensregel
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro115"], // Default: Namensregel
xOutStopper => "MB3025", // HW-Planung: MB VERW SEP, Nummernregel ab BG3025
xOutVarioMotor => "MA0145"); // HW-Planung + Layout: MA an dieser angetriebenen Strecke (Lage bzw. Bezeichnung)
END_REGION
```
### Auszählung der beiden Beispiele
| Führende Quelle | 1Sep1Swi (39 Zeilen) | Vario (47 Zeilen) |
|---|---|---|
| Layout | 13 | 10 |
| HW-Planung | 11 | 14 |
| Default | 15 | 23 |
Gezählt sind alle kommentierten Zeilen (Zuweisungen und REGION-Kopf) nach der zuerst
genannten Quelle; "wie oben" zählt wie die Zeile davor. Die Struktur (welche Zeilen es gibt, wie oft, in welcher Reihenfolge) kommt
immer aus dem Layout; die Tabelle zählt, woher der **Wert** einer Zeile kommt. Der
Großteil der Defaults sind Namensregeln und Zeiten, die nach dem Typ-Katalog
vorbelegt und bei der Inbetriebnahme geprüft werden. Fast alle HW-Planung-Zeilen
lassen sich aus TIA-Import und `positionsdraw` füllen; offen bleiben `xInSftyOk`
(Safety-Programm) und die Scanner (PROFINET, nicht im Tag-Import).
+290
View File
@@ -0,0 +1,290 @@
# Brücke Layout ↔ E-Planung über `*_positionsdraw.json`
Stand 2026-10-15. Erst Machbarkeitsprüfung, inzwischen umgesetzt als
`lib/e_planning.py` (Regeln `cfg/e_planning.ini`, Aufruf `bin/e_planning.bat`,
`scl_skeleton --eplan`); Bedienung in `doc/Python_Scripts.md` §5c, Stand der
Umsetzung unten. Am 2026-10-15 mit dem neuen Mubea-Datensatz in `data/Mubea/`
(TIA-Import + Positionsdatei) nachgeprüft; Ergebnisse unter "Nachprüfung
2026-10-15".
## Problem
Das Kapitel "Daten" in `doc/Klassenhierarchie.md` zeigt: das Hardwarelayout (CSV)
liefert die **Struktur** (welche TROs, welche Separatoren/Weichen, wer mit wem), die
E-Planung die **Namen und Adressen** (`BG…`, `MB…`, `MA…`, `FC…`). Die EA-Liste allein
kennt aber keine Topologie. Es fehlt die Zuordnung **Layout-Bauteil (CSV-`TeileId`) →
Betriebsmittelkennzeichen (BMK)**.
## Idee
Die E-Planung wird als Layer über der Hardwareplanung gezeichnet: Sensor- und
Aktorsymbole liegen in der Zeichnung neben dem Bauteil, zu dem sie gehören. Das
Kabellängen-Tool (`C:\10-Develop\gitrepos\kabellaengen`, `lib/getpositions.py`)
schreibt diese Symbole mit Position und Attributen nach `<zeichnung>_positionsdraw.json`.
Damit ist die Brücke eine **räumliche Zuordnung**: E-Symbol → nächstes Layout-Bauteil
passender Art.
## Datenquelle `*_positionsdraw.json`
| Schlüssel | Inhalt | Nutzen für sps_skel |
|---|---|---|
| `sensors` | je BMK (`BG0642@UNKNOWN`): `pos` [x, y], `VERW` (Verwendung), `BEZEICHNUNG` (Klartext), `BESCHR`, `ARTINR`, `KENNZEICHNUNG` (`=A1+UH01-KF107`) | Hauptquelle: Sensoren/Ventile/Motoren im Feld |
| `schaltschrank_elemente` | Elemente im Schrank (Motorschutz `FC…`, Taster, Leuchten, `QA…`) mit `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG`, ohne Feldposition | Motorschutz ↔ Motor, Bedienelemente |
| `mappings` | Steuerung → Liste ihrer BMK (`UH01: [...]`) | Steuerungsaufteilung, Nummernkreis |
| `racks`, `distributors`, `tunnels` | Kabeltrassen, Verteiler | für sps_skel ohne Belang |
| `double_ids`, `warnings` | doppelte BMK, fehlende Attribute | Qualitätsprüfung der E-Daten |
Die Erdungs-Beispiele (`PR500501_…_Erdung_…`, `ST500652_…`) enthalten nur
Potentialausgleich (`POT…`) und taugen nicht für die Brücke. Brauchbar sind
Dateien mit `BG`/`MB`/`MA`: `Mubea_positionsdraw.json` (158 Feld-BMK, davon 157
in `mappings` unter UH01 — `BG0514` hat eine leere `KENNZEICHNUNG`),
`ST500592_ILS_positionsdraw.json` (~3100), `ST500592_10_5-10_Omniflo_…` (~900),
`ST-Fortna_positions_.json` (mit 38 Scannern `BX`).
`*_positionsconv.json` und `*_positionsdraw.json` sind bei Mubea byte-gleich;
eine von beiden genügt.
## Datenquelle TIA-Import `*_TIA.xlsx`
Die zweite nötige Quelle. Blatt **PLC Tags**: je Signal Name (`BG0542`),
Absolutadresse (`%E…`/`%A…`), Kommentar. Blatt **Constants**: je Signal eine
Index-Konstante `cIn<BMK>`/`cOut<BMK>` (laufender Index, **nicht** aus der
Adresse berechenbar).
| Nutzen | Warum |
|---|---|
| SCL übersetzbar | Sensoren stehen im SCL als `"DB_Inputs".Sensors["cInBG…"]`, Ventile als `"MB…"`; beides muss im TIA-Projekt existieren — der TIA-Import liefert genau diese Tags und Konstanten |
| Prüfliste | jeder Name im erzeugten SCL muss im TIA-Import stehen |
| `FB_CallSensors`, Größe `DB_Inputs` | alle `cIn…`-Konstanten mit Wert (noch nicht generiert) |
| Schrank- und Antriebssignale | `SF…` (Taster, Not-Halt), `PF…` (Leuchten), `MA…`/`FC…` (Motor/Motorschutz), `GF…` (Geber), `BP…`, `DI…` |
Mubea: 237 Tags (105 BG, 64 MB, 26 SF, 17 PF, 11 MA, 11 FC, je 1 BP/DI/GF) und
237 Konstanten (144 `cIn`, 93 `cOut`). Alle 158 Feld-BMK und 80 von 81
Schrankelementen (fehlt: `QA7000`) der Positionsdatei stehen im TIA-Import, die
Kommentare sind 1:1 gleich der `BEZEICHNUNG`.
**Nicht nötig** (Mubea): `*_EA.xlsx` (dieselben 237 Signale, nur Adresse und
Kommentar, keine Namen), `*_WSCAD.xlsx` (zusätzlich nur die E/A-Baugruppe je
Kanal, die `KENNZEICHNUNG` der Positionsdatei schon trägt, und 259 freie Kanäle),
`*_BOM.xlsx`, `*_cables.*`, `*_todraw.json`, `*_errors.json` (Kabel, Stückliste,
Trassen; die Qualitätswarnungen stehen auch in der Positionsdatei). Eingaben für
die Brücke also: **CSV + TIA-Import + eine Positionsdatei**.
## Test an Mubea
Eingaben: `data/mubea.csv` (Layout, 38 Separatoren) und
`kabellaengen/work/Mubea_positionsdraw.json` (E-Planung, alter Stand). Die Zahlen
in diesem Abschnitt stammen aus der ersten Untersuchung (Umkreis 700 mm, ohne
Regeln); das Ergebnis der Umsetzung und die Nachprüfung mit dem neuen Stand
`data/Mubea/Mubea_positionsdraw.json` stehen unten.
### 1. Lagebezug
Die Ermittlung aus `lib/dxf_registration.py` (`derive_transform`) funktioniert
unverändert, wenn man die E-Symbole mit `VERW` `SEP`/`IN SEP` als Rolle "Separator"
einsetzt:
```text
mubea.csv <- Mubea_positionsdraw.json: dx +50613.6 mm, dy +8555.8 mm, Drehung 0
26/38 Separatoren im Toleranzband
```
Das entspricht dem schon gespeicherten Bezug `mubea.csv|500573_60_1.dxf`
(dx +50833.7, dy +8414.5) bis auf ~250 mm: diese (alte) E-Zeichnung lag im selben
Koordinatensystem wie die Layoutzeichnung. Zum Prüfen genügt also der
bestehende Mechanismus; als Schlüssel in `cfg/dxf_registration.json` dient
`<csv>|<positionsdraw.json>`.
Das gilt **nicht allgemein**: der neue Stand in `data/Mubea/` liegt in einem
anderen Koordinatensystem (dx −151.4, dy −696.6, siehe "Nachprüfung
2026-10-15"). Der Lagebezug ist je Datei neu zu ermitteln, nicht von der
Layoutzeichnung zu übernehmen.
### 2. Was sich zuordnen lässt
| E-Symbol (`VERW`) | Layout-Partner | Abstand | Ergebnis |
|---|---|---|---|
| `IN SEP` (BG, Sensor im Separator) | Separator | **0–1 mm** im Pin-Speicher (20/20), sonst 200–500 mm | Anker: das Symbol sitzt auf dem Separator |
| `SEP` (MB, Stopperventil) | Separator | konstant **~270 mm** daneben | eindeutig über Nummernregel (unten), nicht über Abstand |
| `AE ABZWEIG` (MB, Ausschleusventil) | Ausschleuselement | konstant **1414 mm** (20/20, Versatz (−1405, −159)) | eindeutig, wenn der Versatz je Symbolart bekannt ist |
| `STAU_REF-LT` (BG, Stausensor) | Ausschleuselement | konstant **1391 mm** (20/20) | wie oben |
| `ABZWEIG-K104` (MB, Weiche) | Weiche | 783 mm | eindeutig (nur eine Weiche) |
| `BANDBRUCH`, `KETTENBRUCH`, `ÜBERGABE`/`ÜBERNAHME`, `SYNC P1..4`, `ZAPPLER` | Kreisel/Strecke/Station | uneinheitlich, teils > 50 m (Symbol an Konvejoren außerhalb des CSV-Ausschnitts) | nicht über Abstand, nur über `BEZEICHNUNG` |
Nebeneinander liegende Separatoren (Pin-Speicher, Raster 800 mm) machen den
Abstand allein mehrdeutig: das MB von Strecke n liegt 270 mm von Separator n und
530 mm von Separator n+1. Eindeutig wird es erst mit der Nummernregel.
### 3. Nummernregel der E-Planung
Ventil = Sensor + 1, gleiche Funktionsgruppe:
| Paar | Treffer (von 58 MB) |
|---|---|
| `MB(n)` `SEP` ↔ `BG(n−1)` `IN SEP` (z. B. BG0642 / MB0643) | 31 |
| `MB(n)` `AE ABZWEIG` ↔ `BG(n−1)` `STAU_REF-LT` (z. B. BG0542 / MB0543) | 21 |
| sonstige Paare (`VEREINZ(CP)`, `STOPPER (CP)`, `AE ABZW.`) | 3 |
| ohne BG-Partner | 3 |
Damit reicht es, den **Sensor** räumlich zuzuordnen (der `IN SEP` liegt auf dem
Separator); das Ventil folgt aus der Nummer. Die SCL-Referenzen zeigen dasselbe
Muster (`stSenInSep := …["cInBG3242"]`, `xOutStopper1 => "MB3242"`) — dort sogar
mit gleicher Nummer; die Regel ist also projektabhängig und gehört in eine
Konfiguration, nicht in den Code.
Gleiches gilt für Motor ↔ Motorschutz: `MA0200` "PIN CONVEYOR#100" ↔
`FC0200` "MOTORSCHUTZ CONVEYER #100" (gleiche Nummer, `schaltschrank_elemente`),
wie in der SCL `xOutVarioMotor => "MA0145"`, `xInMotorProtection := "FC0145"`.
### 4. Was nicht passt
12 von 38 Layout-Separatoren (vor allem an den Kreiseln K0102/K0104/K0105) haben im
Umkreis von 700 mm kein `IN SEP`/`SEP`; umgekehrt liegen mehrere `IN SEP` des
Kreisels K0102 neben Aus-/Einschleuselementen statt neben einem Separator. Vermutlich
ist die CSV (aus `500573_60_1.dxf`) ein anderer Planungsstand als die E-Zeichnung
`Mubea.dxf` des Kabellängen-Tools, oder Kreisel-Separatoren sind im CSV anders
modelliert. Das muss mit zueinander passenden Ständen geprüft werden, bevor man
eine Quote angibt.
`Mubea_positionsdraw.json` enthält **keine Scanner** (`BX`). Für Scanner-Zuordnung
ist ein anderes Projekt nötig (`ST-Fortna_positions_.json`, `ILS-Baugruppe`).
## Was die Brücke für das Kapitel "Daten" liefert
| SCL-Datum | aus `positionsdraw.json` | Regel |
|---|---|---|
| `stInSeparator<n>.stSenInSep` | BG mit `VERW` `IN SEP` | räumlich am Separator (Anker) |
| `xOutStopper<n>` | MB mit `VERW` `SEP` | Nummernregel ab dem BG |
| `stInSenJam<n>`, `Settings.xPluggedJam` | BG `STAU_REF-LT` | räumlich, Versatz je Symbolart; vorhanden ⇒ `xPluggedJam := TRUE` |
| `xOutSw<n>ExTo3/4` | MB `AE ABZWEIG` / Weichen-MB | Nummernregel ab dem Stausensor bzw. räumlich an Weiche/Ausschleuselement |
| `Settings.xPluggedPart`, `stSenPart` | BG `BT BELADEN` | räumlich; vorhanden ⇒ `TRUE` |
| `xInMotorProtection`, `xOutVarioMotor` | MA im Feld + FC in `schaltschrank_elemente` | gleiche Nummer |
| `xInCarouselRun` (welcher Antrieb) | FC/MA-`BEZEICHNUNG` "KREISEL-K0102", "CONVEYOR #100" | **räumlich**, nicht über Text: das CSV nennt die Kreisel `Kreisel1/2/3`, die E-Planung `K0102/K0104/K0105`. Alle Symbole, deren `BEZEICHNUNG` einen Kreisel nennt, per Mehrheit dem nächsten CSV-Kreisel zuordnen (siehe Nachprüfung) |
| Steuerung, TRO-Nummernkreis, `stInOutMachineState` | `KENNZEICHNUNG` `=A1+UH01-KF107`, `mappings` | `UH01` = Steuerung, `KF107` = E/A-Station |
| Plausibilisierung | `BEZEICHNUNG` "SEPARATOR STR. 14", "KR-K0102 VOR BT-BELADUNG" | Strecken-/Kreiselnummer gegen Layout prüfen |
Nicht aus dieser Quelle: Scanner-Hardwarekennungen, Weichenbauart (`xNewSwitch`).
Auch nicht aus den E/A-Daten, aber **je Projekt nur ein Wert**: `xInSftyOk`,
`stInOutMachineState`, `xInAutomatic`, `xInErrorQuit`. Der TIA-Import enthält
zwar die Not-Halt-Kanäle (`SF5000…SF6006`) und `SF0001` "ANLAGE EIN" / `SF0002`
"START NACH STÖRUNG", die Referenz-SCL verdrahtet diese Parameter aber nicht mit
E/A-Signalen, sondern mit dem Sicherheitsprogramm
(`"DB_InterfaceSafety".stFromSafety.EStopMain….Q`) bzw. der Schrank-Struktur
(`#stInOutControlUnitCabinet…`). Sie gehören als Projektkonstanten in eine
Konfiguration (bei Mubea ~39 der offenen Stellen), nicht in die Brücke.
## Umsetzung (Stand 2026-10-10)
1. **`lib/e_planning.py`**: liest `*_positionsdraw.json` in `EDevice` (BMK, VERW,
Bezeichnung, Kennzeichnung → Steuerung/E/A-Station, Position).
2. **Lagebezug** über `dxf_registration.derive_transform` mit den `IN SEP`-Sensoren
als Rolle "Separator"; abgelegt in `cfg/dxf_registration.json` unter
`<csv>|<positionsdraw.json>`.
3. **Regeln in `cfg/e_planning.ini`** (projektabhängig): je Rolle Layout-Art, VERW,
Höchstabstand, **Sollabstand** (für die konstanten Versätze von Stausensor und
Ausschleusventil — sonst gewinnt bei 0080/0084 ein näherer, fremder Stausensor
des Kreisels K0102) und die Nummernregel (`MB = BG + 1`).
4. **Ausgabe** `<csv>_eplan.json` (`EplanDocument`) und Bericht `<csv>_eplan.md`.
`scl_skeleton --eplan` setzt Separator-Sensor und Stopperventil ein
(`SclName.todo = False`); alles andere bleibt `TODO(E-Planung)`.
Ergebnis Mubea (alter Stand) mit den ausgelieferten Regeln: Separator-Sensor 21/38,
Stopper 22/38, Stausensor 20/23, Ausschleusventil 20/23, Weichenventil 1/1. Den
Separator-Sensor nimmt die Regel nur bis 300 mm; die Kreisel-Fälle mit 470–540 mm
(z. B. BG0532 an 0044) bleiben bewusst offen.
## Nachprüfung 2026-10-15 (neuer Stand `data/Mubea/`)
Lauf: `e_planning --file mubea.csv --eplan <voller Pfad>\data\Mubea\Mubea_positionsdraw.json`,
dann `tro_flow --json` und `scl_skeleton --eplan --templates`.
| | alter Stand | neuer Stand |
|---|---|---|
| Lagebezug | dx +50613.6, dy +8555.8 | **dx −151.4, dy −696.6**, Drehung 0, 23/38 Punkte, Restfehler max 541 mm |
| Separator-Sensor | 21/38 | 21/38 |
| Stopper | 22/38 | 22/38 |
| Stausensor | 20/23 | **21/23** |
| Ausschleusventil | 20/23 | **19/23** |
| Weichenventil | 1/1 | 1/1 |
| Antriebe | — | 11, alle mit Motorschutz |
Im erzeugten `FB_Main` (18 TROs): 43 Namen aus der E-Planung, 440 Stellen
`TODO(E-Planung)`. Alle 21 Konstanten und 22 Tags im SCL stehen im TIA-Import.
Befunde:
- **Pfad:** `--eplan Mubea/Mubea_positionsdraw.json` (Unterordner relativ zu
`%SKEL_DATA%`) wird nicht gefunden; nur ein Dateiname direkt in `%SKEL_DATA%`
oder ein voller Pfad geht.
- **Gespeicherter Lagebezug:** der Schlüssel `<csv>|<positionsdraw.json>` in
`cfg/dxf_registration.json` enthält nur den Dateinamen. Ein neuer Stand mit
gleichem Namen übernimmt ohne `--re-register` still den alten Bezug — beim
Mubea-Wechsel wären das ~51 m Versatz gewesen.
- **Kreisel-Zuordnung räumlich möglich:** die Symbole, deren `BEZEICHNUNG` einen
Kreisel nennt (nach dem Lagebezug, Abstand zur Boundingbox), liegen eindeutig
auf einem CSV-Kreisel — **K0102 = Kreisel3 (0010)**, **K0104 = Kreisel2 (0002)**,
**K0105 = Kreisel1 (0001)**; ein Ausreißer (`BG0725` "BANDBRUCHÜBERWACHUNG
KR-K0105" an Kreisel2). Damit erhält jeder Kreisel seinen Antrieb
(`MA0202`/`FC0202`, `MA0205`/`FC0205`, `MA0206`/`FC0206`) und K0102 seinen Geber
`GF20000`.
Was sich mit TIA-Import + Positionsdatei noch schließen lässt:
| Offene Stellen (Mubea) | Anzahl | Weg |
|---|---|---|
| `stInSenJam<n>` | ~17 | schon zugeordnet; fehlt nur TRO-Ausgang → Ausschleuselement (siehe "Offen") |
| `xInCarouselRun` | ~13 | Kreisel-Zuordnung oben + Motorpaar |
| Vario-Sensoren (`stSenLastPos`, `stSenJam`, `stSenFinger`, `stSenCarrInPos`), `xOutVarioMotor`, `xInMotorProtection` | ~18 | wahrscheinlich, neue Regeln über `VERW` (`SYNC P1..4`, `ZAPPLER`, `ABFR.FINGER`, `BT BELADEN`) und die Motorpaare; zu prüfen |
| `xInSftyOk`, `stInOutMachineState`, `xInAutomatic`, `xInErrorQuit` | ~39 | Projektkonstante (oben), nicht aus diesen Dateien |
| `nSeparatorNo`/`nSwitchNo`, Zeiten, Staubereichs-Verdrahtung | Rest | weder TIA noch Positionsdatei |
Die 17 nicht zugeordneten Separatoren an den Kreiseln sind ein Standunterschied
Layout ↔ E-Zeichnung, keine fehlenden Daten.
Offen:
- Stausensoren und Ausschleus-/Weichenventile stehen in JSON und Bericht, kommen
aber noch nicht in die SCL: die TRO-JSON sagt nicht, welches Ausschleuselement
bzw. welche Weiche zu welchem TRO-Ausgang gehört.
- Die Lagerlinien eines `PinStore`-TRO (`arInSep[n]`) sind zugeordnet, der
Generator erzeugt die Linienparameter aber noch nicht.
- Scanner (`BX`) fehlen in den Mubea-Daten; Regel und Test mit
`ST-Fortna_positions_.json` stehen aus.
- Ein Abgleich mit passenden Ständen von CSV und E-Zeichnung für die Kreisel.
- ~~TIA-Import einlesen~~ — **erledigt** 2026-10-15: `lib/hw_import.py`
(`<projekt>_hw.json`); `FB_CallSensors` daraus steht noch aus.
- ~~Kreisel ↔ E-Kreisel (`K01xx`) räumlich zuordnen~~ — **erledigt** in
`lib/merge_hw_layout.py` (Mehrheit, nur Förderer der genannten Art); in die SCL
(`xInCarouselRun`) kommt es mit der Umstellung von `scl_skeleton` auf die
Anlagen-JSON.
- ~~Gespeicherten Lagebezug absichern~~ — **erledigt**: SHA-256 der
Positionsdatei im Eintrag von `cfg/dxf_registration.json`.
- ~~Unterordner in `--eplan`~~ — **erledigt** (`material_flow.resolve_input`).
## Abgleich Layoutzeichnung ↔ CSV ↔ E-Planung (2026-10-15)
Zeichnung `data/mubea_layout.dxf` (aus `mubea_layout.dwg`), gelesen über alle
Blockebenen mit vollständiger Blocktransformation (die `Staustrecke…`-Blöcke
der Gefällestrecken sind mit gekippter Extrusionsrichtung eingefügt; ohne
OCS liegen ihre Separatoren falsch).
| Vergleich | Ergebnis |
|---|---|
| DXF ↔ `mubea.csv` | **gleiches Koordinatensystem, kein Versatz.** 38/38 Separatoren je 60–70 mm (Einfügepunkt `Separator_SP_3D` gegen CSV-Punkt, in Blockrichtung), 8/8 Scanner je 360 mm, Aus-/Einschleuselemente 22/23 bzw. 22/23 auf 0 mm (`0061`, `0064` und die Weiche `0125` sind keine eigenen Blöcke) |
| E-Planung ↔ DXF | (−211, −697) mm; Speicherlinien 20/20 auf **1 mm** |
| E-Planung ↔ CSV | (−151, −697) mm; 23/38 im Toleranzband |
Die 20 Separatoren der Gefällestrecken passen mit dem Versatz exakt (ohne ihn
liegt jeder Sensor eine Speicherlinie daneben, 235 mm). Die 12 Separatoren an
Kreiseln/Strecken und die 6 Vario-Separatoren passen in **keiner** Lage
(155–3438 mm, mit und ohne Versatz). Es gibt also keine starre Transformation
für die ganze Anlage: der Speicher ist in der E-Zeichnung als Ganzes
verschoben, der Bereich um die Kreisel ist ein anderer Planungsstand.
Folge: die Kreisel-Separatoren bleiben offen, bis E-Zeichnung und Layout zum
selben Stand gehören; `merge_hw_layout` warnt deshalb (23/38 = 61 %).
Nebenbefund für `tro_annotate` (Kontrollansicht): es liest nur die
Modellbereichs-Blöcke (12 der 38 Separatoren, die übrigen stecken in
`GF_n`/`VF_n`), und der Lagebezug übernimmt den 60-mm-Einfügeversatz als
scheinbare Verschiebung (dx +59.8).
+741
View File
@@ -0,0 +1,741 @@
# Klassenhierarchie
Bestandsaufnahme aller Python-Klassen in `lib/` (Stand 2026-10-10). Kurzes
Ergebnis vorweg: es gibt praktisch **keine Vererbung** in diesem Code (eine
Ausnahme: `DerivedTro(TroBase)`, plus die Standard-Exception
`RegistrationError(RuntimeError)`). Alles andere sind flache, eigenständige
Klassen — entweder `@dataclass` (veränderlicher Laufzeit-/Analysezustand) oder
Pydantic `BaseModel` (validierte Daten, die auch als JSON ein-/ausgehen). Eine
klassische "ist-ein"-Hierarchie zu suchen wäre hier der falsche Blickwinkel;
interessanter ist, **wer wen enthält/referenziert** und in welchem Modul-Layer
eine Klasse sitzt (siehe Layer-Kommentar oben in `model.py`).
## Modul-Layer (wer darf wen importieren)
```
tro_catalog.py (Typ-Ebene: was ein TRO-Typ ist)
|
v
model.py (Instanz-Ebene: ein konkreter TRO, die TRO-JSON)
|
v
tro_flow.py (Ableitung: CSV-Graph -> Analysis -> TroDocument)
|
+----------------------+
v v
scl_context.py tro_annotate.py (Ausgabe: annotierte Zeichnung)
(TroDocument -> SclFile, Render-Kontext)
|
v
scl_skeleton.py (Ausgabe: SclFile -> FB_Main.scl, generisch oder per Jinja-Template)
material_flow.py dxf_registration.py (eigenständig, ohne Abhängigkeit auf obige)
tro_overrides.py (importiert model.Connection / model.TroCandidate)
e_planning.py (Brücke zur E-Planung; importiert dxf_registration, liest das
CSV über material_flow; scl_context kennt nur EplanDocument)
```
`tro_flow.py` und `tro_annotate.py` definieren keine eigenen Klassen (nur
Funktionen). `scl_skeleton.py` hat genau eine (`SclTemplates`, Jinja-Anbindung).
`material_flow.py` hat Graph-Dataclasses — das ist CSV-Rohmodell, nicht
TRO-Modell.
## Vererbung (ASCII)
```
RuntimeError (stdlib) BaseModel (pydantic, stdlib: pydantic)
| |
+-- RegistrationError +-- _Positioned (model.py, Mixin)
(dxf_registration.py) |
+-- TroBase
| +-- DerivedTro
+-- TroCandidate
```
Das ist die gesamte `class X(Y)`-Liste im Projekt — alle anderen Klassen unten
haben `object`/`BaseModel` direkt als einzige Basis und erscheinen hier nur
noch als Gruppen nach Datei.
## Klassen nach Datei, mit Komposition ("enthält")
```
lib/tro_catalog.py [Typ-Katalog, keine Instanzdaten]
├── TroItem (Konstanten-Klasse: Bauteilarten wie SEPARATOR, SWITCH)
├── TroSymbol (Konstanten-Klasse: Zeichensymbole wie CIRCLE, TRIANGLE)
├── TroStyle(BaseModel) Farbe/Darstellung einer Typgruppe
└── TroDefinition(BaseModel)
└─ style: TroStyle (1 TroDefinition hat genau 1 TroStyle)
lib/model.py [Instanz-Ebene / TRO-JSON]
├── TroBase(BaseModel)
│ └─ definition() --> TroDefinition (gelesen aus tro_catalog, nicht gespeichert)
│ └── DerivedTro(TroBase) [einzige echte Vererbung im Projekt]
│ └─ slots: list[Slot] (Stellen fuer die Hardwareplanung)
├── _Positioned(BaseModel) (Mixin: position {x,y,z}, describe_position)
│ ├── TroBase, TroCandidate, JamArea
├── Slot(BaseModel) (separator1/switch1/scanner1/drive + Rollen)
├── JamArea(_Positioned) (Staubereich J<Sep>: Eingang eines TRO)
├── Connection(BaseModel) (from/to/kind zwischen zwei TRO-IDs)
├── TroCandidate(_Positioned) (gemeldete Stelle, an der ein TRO erwartet wird)
├── Analysis(BaseModel)
│ ├─ tros: list[DerivedTro]
│ ├─ isolated: list[DerivedTro]
│ └─ boundaries: list[DerivedTro]
└── TroDocument(BaseModel) (= <csv>_tro.json)
├─ tros: list[DerivedTro]
├─ boundaries: list[DerivedTro]
├─ connections: list[Connection]
├─ candidates: list[TroCandidate]
└─ jam_areas: list[JamArea] (JSON: jamAreas)
lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass]
├── Element (1 CSV-Zeile)
├── Node
│ └─ element: Element
├── Edge
├── Graph
│ ├─ nodes: dict[str, Node]
│ ├─ edges: list[Edge]
│ └─ unassigned: list[Element]
├── TrackNode
│ └─ element: Element | None
├── TrackEdge
└── TrackGraph
├─ nodes: dict[str, TrackNode]
└─ edges: list[TrackEdge]
lib/tro_overrides.py [manuelle Korrekturen aus cfg/tro_overrides.ini, @dataclass]
├── TroOverride (Typkorrektur an einem abgeleiteten TRO: tro_id ODER separator)
├── SplitSpec (TRO aus einem bestehenden heraustrennen)
├── MergeSpec (mehrere TROs zu einem zusammenfassen)
└── Overrides (Sammelcontainer für eine CSV-Datei)
├─ tros: dict[str, TroOverride]
├─ splits: list[SplitSpec]
├─ merges: list[MergeSpec]
├─ connections: list[model.Connection]
└─ candidates: list[model.TroCandidate]
lib/dxf_registration.py [CSV<->DXF Koordinatentransform]
├── RegistrationError(RuntimeError)
└── Transform(BaseModel)
lib/scl_context.py [Render-Kontext für die SCL, BaseModel]
├── SclName (Wert + Merker todo: noch aus der E-Planung offen?)
├── SclSeparator (ein stInSeparator<n>-Block)
├── SclSwitch (ein stInSwitch<n>-Block + FC_Direction-Aufruf)
├── SclSwitchExit (Weichenabgang + darüber erreichbare TROs)
├── SclScanner (ein stInBarcodeReader-Block)
├── SclJam (Staubereich an Ein-/Ausgang)
├── SclTro (alles für eine REGION)
│ ├─ separators: list[SclSeparator]
│ ├─ switches: list[SclSwitch]
│ ├─ switch_exits: list[SclSwitchExit]
│ ├─ scanners: list[SclScanner]
│ ├─ jam_entries / jam_exits: list[SclJam]
│ └─ main_tro, safety, ... : SclName
└── SclFile (die ganze Datei)
└─ tros: list[SclTro] (in Fliessrichtung)
lib/scl_skeleton.py [SCL-Ausgabe]
└── SclTemplates (Ordner mit <fbType>.scl.j2, hält ein jinja2.Environment)
lib/e_planning.py [Brücke Layout <-> E-Planung]
├── EDevice(BaseModel) (ein BMK aus *_positionsdraw.json: VERW, Bezeichnung, Position)
├── LayoutPoint (@dataclass: punktartiges Layout-Bauteil in CSV-Koordinaten)
├── Rule (@dataclass: eine Regel aus cfg/e_planning.ini)
├── Assignment(BaseModel) (BMK in einer Rolle an einem Layout-Bauteil, mit Abstand)
├── Drive(BaseModel) (Motor MA ↔ Motorschutz FC)
└── EplanDocument(BaseModel) (= <csv>_eplan.json)
├─ transform: Transform (aus dxf_registration)
├─ assignments: list[Assignment]
└─ drives: list[Drive]
```
## Auffälligkeiten für ein mögliches Refactoring
1. ~~`OpenPoint` existiert zweimal~~ — erledigt 2026-10-10: zu
`model.TroCandidate` zusammengefasst (`expect` → `type_name`, `x/y/z` →
`position`); `ConnectionSpec` durch `model.Connection` ersetzt;
`tro_overrides.py` importiert beide aus `model.py`.
2. **Zwei parallele Graph-Hierarchien** in `material_flow.py`
(`Node/Edge/Graph` vs. `TrackNode/TrackEdge/TrackGraph`) teilen sich keine
Basisklasse, obwohl Struktur und Feldnamen sich stark ähneln (`node_id,
kind, ...` / `src, dst, ...`). Eine gemeinsame leichte Basis (z. B. ein
generisches `GraphBase[NodeT, EdgeT]`) wäre möglich, aber angesichts der
wenigen Stellen, die beide Typen anfassen, vermutlich mehr Abstraktion als
Nutzen.
3. **`TroItem` und `TroSymbol` sind reine Konstanten-Namespaces** (keine
Instanzen, keine Methoden außer `TroItem.all()`), kein Pydantic-Modell wie
der Rest von `tro_catalog.py`. Das ist bewusst so (siehe Docstring: "damit
Tippfehler auffallen") und funktioniert wie ein `Enum`-Ersatz — ein `StrEnum`
wäre eine nahe liegende, idiomatischere Alternative, falls ein Refactoring
ansteht.
4. Die einzige echte Vererbung (`DerivedTro(TroBase)`) ist sauber: `TroBase`
ist bereits so geschnitten, dass es für Systemgrenzen (`boundaries`)
wiederverwendet wird, `DerivedTro` ergänzt nur das, was die CSV-Ableitung
zusätzlich weiß. Kein Handlungsbedarf.
**Fazit:** Die Code-Basis nutzt Vererbung bewusst kaum — die Architektur trennt
stattdessen über Module (Typ-Katalog vs. Instanz vs. Ableitung vs. Ausgabe,
siehe `model.py`-Kopfkommentar) und über Komposition. Ein Refactoring in
Richtung "mehr Vererbung" wäre hier eher kontraproduktiv. Die doppelte
`OpenPoint`-Definition (1) ist behoben; offen bleibt nur optional `StrEnum`
statt Konstanten-Klassen (3).
## Die komplexen Klassen im Einzelnen
Die folgenden Abschnitte beschreiben je Klasse, **wozu** sie da ist, **was
darin passiert** und **wer sie erzeugt bzw. liest**. Einfache Datenhalter
ohne eigene Logik — `Connection`, `TroCandidate`, `Edge`, `TrackEdge`,
`TroItem`/`TroSymbol` und die kleinen Blöcke `SclSeparator`, `SclSwitch`,
`SclSwitchExit`, `SclScanner`, `SclJam` — sind im Baum oben ausreichend
beschrieben und bekommen keinen eigenen Abschnitt.
### `TroDefinition` (+ `TroStyle`) — `lib/tro_catalog.py`
**Wozu:** Beschreibt einen TRO-**Typ**, nicht eine Stelle in der Anlage: Kurzname
(`1Sep1Swi`), Siemens-FB (`FB_ILS_MTRO_1Sep1Swi`), Bauteile
(`{"Separator": 1, "Switch": 1}`), Farbgruppe und CAD-Symbol. Der Katalog
aller Definitionen ist die ausführbare Form von `doc/TRO_Typen.md`.
**Was passiert darin:** Pydantic mit `validate_assignment` und `extra="forbid"` —
eine Definition kann nie ungültig werden, auch nicht über die `set_*`-Methoden.
`_clean_items` wirft Bauteile mit Anzahl ≤ 0 heraus. Ein eigener `__init__`
erlaubt die alte Positionsform `TroDefinition("1Sep", "FB_...", {...})`. Dazu
kommen Abfragen (`get_items`, `has_item`, `get_item_fb`, `get_total_items`) und
Darstellungshelfer (`get_dot_attrs`, `mermaid_class/_classdef`,
`describe_items`). `TroStyle` liefert nur die Farben einer Gruppe für Graphviz,
Mermaid und CAD.
**Wer nutzt sie:** Über `get_tro(name)`/`catalog()` jeder, der vom Typnamen auf
FB oder Bauteile kommen muss — vor allem `TroBase.definition()`; außerdem
`tro_flow` (Typprüfung), `tro_annotate` (Farbe/Symbol, `--emit-lisp`) und die
Doku-Grafiken.
### `TroBase` / `DerivedTro` — `lib/model.py`
**Wozu:** Ein konkreter TRO in einer Anlage (Instanz). `TroBase` ist der Kern, den
jeder TRO hat, egal woher er stammt: `tro_id`, `type_name`, `separators`,
`predecessors`/`successors`, `position`. `DerivedTro` ergänzt, was `tro_flow`
beim Ableiten aus dem CSV zusätzlich weiß: `hosts`, `scanners`, `reason`,
`colocated`, `position_from`/`position_points`. Systemgrenzen (`SYSn`) sind
ebenfalls `DerivedTro`.
**Was passiert darin:**
- `fb_block` und `items` sind **berechnete** Felder: bei jedem Zugriff aus dem
Katalog gelesen (`definition()` → `get_tro(type_name)`), nie gespeichert. In der
JSON stehen sie trotzdem (`fbType`, `items`), damit sie lesbar ist.
- `_check_catalog_fields` (Validator vor dem Einlesen): steht in einer JSON ein
`items`/`fbType`, das nicht zum Typ passt, ist das ein Fehler — "den Typ ändern,
nicht die Bauteile". Danach werden die Felder verworfen.
- Aliase: in der JSON `id`/`type`, im Code `tro_id`/`type_name`
(`populate_by_name`).
- `_Positioned` (Mixin) wandelt `position` zwischen Tupel und
`{"x","y","z"}` und liefert `x`/`y`/`z` und `describe_position()`.
- `DerivedTro.connected`: hat der TRO überhaupt Nachbarn?
**Wer nutzt sie:** `tro_flow.derive_tros`/`derive_boundaries` erzeugen sie,
`scl_context` liest sie (als `TroBase`), `tro_annotate` zeichnet sie.
### `Analysis` — `lib/model.py`
**Wozu:** Das Arbeitsergebnis eines Ableitungslaufs, bevor daraus eine Datei wird.
Enthält mehr als die JSON: Kanten als Tupel, Kantenarten, isolierte TROs,
unbekannte Typen, Befunde, die verwendete Methode.
**Was passiert darin:** Fast nur Daten; `by_id()` sucht über TROs und
Systemgrenzen. Befüllt wird sie in `tro_flow.analyse_tros` in fester
Reihenfolge: `derive_tros` → `derive_boundaries` → `locate_tros` (Position,
nötig für die Reihenfolge auf einer Kreiselbahn) → `link_tros` (Kanten und
Kantenarten) → Prüfung gegen den Katalog.
**Wer nutzt sie:** `tro_flow` (Bericht, Diagramm, `--json`) und `tro_annotate`
(zeichnet direkt aus der `Analysis`, ohne Umweg über die JSON).
### `TroDocument` — `lib/model.py`
**Wozu:** Die TRO-JSON `<csv>_tro.json` (Schema `sps_skel-tro-v4`) — die
Schnittstelle zwischen Ableitung (`tro_flow --json`) und SCL-Ausgabe
(`scl_skeleton`). Weil beide Seiten dieselbe Klasse benutzen, kann das Format
nicht unbemerkt auseinanderlaufen.
**Was passiert darin:** `from_analysis()` macht aus einer `Analysis` ein Dokument
(Kanten + Kantenarten → `Connection`-Liste, je TRO die `slots` aus
`derive_slots()`, die Staubereiche aus `derive_jam_areas()`). `to_json()` schreibt mit den
JSON-Aliasen, `from_json()` liest und prüft. `extra="forbid"` sorgt dafür, dass ein
umbenannter oder falscher Schlüssel als Fehler auffällt statt still einen
Standardwert zu bekommen; `scl_skeleton.main` meldet bei falschem `schema`
gezielt "mit tro_flow neu erzeugen".
**Wer nutzt sie:** geschrieben von `tro_flow --json`, gelesen von
`scl_skeleton.load_document` → `scl_context.build_context`.
### `Element`, `Graph`, `TrackGraph` — `lib/material_flow.py`
**Wozu:** Das Rohmodell aus dem CSV-Export (ILS 2.0), noch ohne TRO-Begriff.
- `Element` = eine CSV-Zeile: Art, ID, Name, Nachbarn, `track_ids`,
Merkmale, Position, Bounding-Box, Anschlusspunkte K1..K4.
- `Graph` (`Node`/`Edge`) = grober **Materialfluss-Graph**: Transportobjekte
(Strecke, Gefällestrecke, Kreisel-Bahn) als Knoten, Geräte (Separatoren,
Weichen, Scanner …) an die Knoten gehängt (`Node.devices`), Stationen und
Übergaben als Kanten.
- `TrackGraph` (`TrackNode`/`TrackEdge`) = feiner **Bahnzusammenhang**: jedes
Bahnelement (AS, SEP, ES, Weiche, BTMT …) ist ein Knoten, verkettet in
Bau- bzw. Drehrichtung (siehe `doc/track.md`).
**Was passiert darin:** `Element` hat viele abgeleitete Properties: Klassifizierung
(`is_transport`, `is_circle`, `is_device`, `is_station`, `is_transfer`,
`is_entry`/`is_exit`), Geometrie (`x/y/z`, `bbox_x/y`, `height`,
`height_from/to`, `rise`), Antrieb (`drive_dir`, `rotation`). Die Graphklassen
selbst sind reine Container; die Logik steckt in den Funktionen
`build_graph()` (Knoten bilden, Geräte zuordnen, Kanten sammeln, Fliessrichtung
aus Gefälle/Antrieb, Stationen und Übergaben einhängen) und
`build_track_graph()` (Ketten aus der Spalte `TrackIds`; ein Element in zwei
Containern verbindet diese automatisch). `TrackNode` kann ohne `element`
existieren (Platzhalter für eine nicht auflösbare Bahn, generisches
Ein-/Ausgang-Symbol).
**Wer nutzt sie:** `material_flow` selbst (DOT/SVG/connect.ini/Bericht) und
`tro_flow.analyse_tros(graph, elements)` als Eingang der TRO-Ableitung;
`tro_annotate` für Ankerpunkte des Lagebezugs.
### `Overrides` (+ `TroOverride`, `SplitSpec`, `MergeSpec`) — `lib/tro_overrides.py`
**Wozu:** Von Hand gepflegte, vor Ort bestätigte Korrekturen aus
`cfg/tro_overrides.ini` für das, was die Ableitung nicht erkennen kann.
- `TroOverride`: Typ eines TRO korrigieren. Adressiert über `tro_id` **oder**
— stabiler — über einen `separator`, weil TRO-Nummern sich zwischen Läufen
verschieben, Separatornummern aus dem Export nicht (`matches()`).
- `SplitSpec`: Separatoren/Scanner aus einem TRO herauslösen und einen neuen TRO
bilden (Übergabestelle, deren Zuordnung auf die Strecke statt auf die
Kreiselbahn zeigt).
- `MergeSpec`: mehrere TROs zu einem Steuerobjekt zusammenfassen (z. B. zwei
Sperren beiderseits eines Kreisel-Berührpunkts).
- `Overrides`: Sammelcontainer je CSV-Datei, dazu gesetzte Topologie
(`connections`, ersetzt die abgeleiteten Kanten vollständig), erwartete
TROs (`candidates`), Protokoll (`applied`) und `warnings`. `bool(overrides)`
ist True, sobald irgendeine Korrektur vorhanden ist.
**Was passiert darin:** `load_overrides(csv_name)` liest den INI-Abschnitt der
CSV-Datei und baut daraus die Objekte; Lesefehler landen in `warnings` statt
den Lauf abzubrechen.
**Wer nutzt sie:** Stand heute **nur** `material_flow --connect`
(`render_connect_ini` schreibt sie als `[<Anlage>.manual]`-Abschnitt). `tro_flow`
wendet sie (noch) nicht an — sie fliessen also nicht in die TRO-JSON und nicht
in die SCL.
### `Transform` (+ `RegistrationError`) — `lib/dxf_registration.py`
**Wozu:** Der Lagebezug zwischen CSV-Export und BricsCAD-Zeichnung: eine starre
Transformation `csv = R(rotation) · dxf + (dx, dy)`. Ohne sie landen die
TRO-Marker in `tro_annotate` an der falschen Stelle.
**Was passiert darin:** `to_csv()`/`to_dxf()` rechnen Punkte hin und zurück,
`describe()` fasst Verschiebung, Drehung, Punktzahl und Restfehler zusammen.
Die Ermittlung steckt in Funktionen: `derive_transform()` ordnet punktartige
Objekte gleicher Rolle (Separator, Scanner) aus CSV und DXF zu, stimmt über
Verschiebungen ab, gleicht aus und wählt den besten Kandidaten;
`verify_transform()` rechnet die Restfehler nach und schreibt sie ins Objekt;
`load_transform()`/`save_transform()` lesen/schreiben
`cfg/dxf_registration.json`. `RegistrationError` heisst: der Bezug liess sich
nicht **nachweisbar** bestimmen — dann lieber abbrechen als falsch zeichnen.
**Wer nutzt sie:** `tro_annotate.resolve_transform` (Reihenfolge: `--offset` →
abgelegter Bezug → neu ermitteln, jeweils mit Prüfung), dazu das geparkte
`tro_extract`.
### `SclFile`, `SclTro`, `SclName` — `lib/scl_context.py`
**Wozu:** Der **Render-Kontext** für das FB_Main-Gerüst. Alles, was aus der
TRO-JSON *abgeleitet* wird, steht hier fertig drin; die Ausgabe
(`emit_*`-Funktionen oder Jinja-Templates) rechnet nichts mehr selbst, sie
liest nur noch. So können generischer Aufbau und Templates nicht
unterschiedlich ableiten.
**Was passiert darin:**
- `build_context(document, source, start_id)` baut aus einem `TroDocument` ein
`SclFile`: Kopfdaten plus die TROs in Fliessrichtung.
- `flow_order()` bestimmt die Reihenfolge der REGIONs: Start bei `--start`, sonst
beim TRO ohne Vorgänger, sonst beim TRO, den eine Station (`SYSn`) speist,
sonst wird der Kreislauf willkürlich aufgetrennt — der Grund steht im
SCL-Kopf (`start_reason`). Danach Breitensuche; nicht erreichte TROs werden
hinten angehängt, damit keiner verloren geht.
- `_tro_context()` baut je TRO ein `SclTro`: so viele Separator-, Weichen- und
Scannerblöcke, wie der Katalogtyp Bauteile hat (`items`); je Vorgänger ein
Staubereich am Eingang, je Nachfolger einer am Ausgang; `to1_destinations`
nur bei genau einer Weiche (bei mehreren ist die Aufteilung eine
Planungsentscheidung); die Zielliste je Weichenabgang über
`reachable_via()` (Erreichbarkeit im TRO-Graphen, ohne über die Weiche
zurückzulaufen).
- `SclName` = Wert + Merker `todo`. Alles, was aus der E-Planung kommen muss
(Separator-/Weichennummern, Sensor-/Aktoradressen, MainTro, Staubereichs-DB …),
ist heute ein Platzhalter mit `todo=True`; die Ausgabe hängt daran den
Kommentar `TODO(E-Planung)`. Spätere Namensregeln ersetzen nur den Wert und
löschen den Merker — die Ausgabe ändert sich dafür nicht.
**Wer nutzt sie:** erzeugt in `scl_skeleton.render()`/`main()`, gelesen von
`render_context()`, den `emit_*`-Funktionen und den Templates (als `t`).
### `SclTemplates` — `lib/scl_skeleton.py`
**Wozu:** Die Anbindung der parametrisierten SCL-Aufrufstellen
(`lib/scl_templates/*.scl.j2`) an die Ausgabe. Ein Objekt steht für einen
Template-Ordner.
**Was passiert darin:** Der Konstruktor baut ein `jinja2.Environment` mit
`StrictUndefined` (ein Tippfehler im Template wird zum Fehler statt zu einer
leeren SCL-Stelle), `trim_blocks`/`lstrip_blocks` und der globalen Variable
`TODO`. `has(fb)` prüft, ob `<fb>.scl.j2` existiert; `render(tro)` rendert das
Template mit `t = SclTro` und fasst danach mehrfache Leerzeilen zusammen (statt
das mit `{%- -%}` im Template zu tun, was die Einrückung bricht).
#### Template ↔ TRO: Zuordnung und Einstieg in die Produktion
**Zuordnung — es gibt keine Tabelle, nur eine Namenskonvention:
Dateiname = FB-Typ.** Die Kette:
```text
DerivedTro.type_name "1Sep1Swi" (TRO-JSON, Feld "type")
-> tro_catalog.get_tro() TroDefinition
-> TroDefinition.fb_block "FB_ILS_MTRO_1Sep1Swi"
-> TroBase.fb_block (berechnet, nicht gespeichert)
-> SclTro.fb (scl_context._tro_context)
-> SclTemplates.has/render lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2
```
Ein neues Template für einen weiteren Typ heisst also einfach
`<fbType>.scl.j2` und wird automatisch für alle TROs dieses Typs verwendet.
Heute gibt es nur den Piloten `FB_ILS_MTRO_1Sep1Swi.scl.j2` (plus
`_macros.scl.j2` mit gemeinsamen Makros). Alle anderen Typen laufen über den
generischen Aufbau (`emit_region()`).
**Einstieg — nur per Schalter (opt-in):**
- `scl_skeleton --templates [DIR]` (ohne DIR: `lib/scl_templates`). `main()`
legt dann ein `SclTemplates` an; `render_context()` nimmt pro TRO
`templates.render(tro)`, wenn `templates.has(tro.fb)`, sonst `emit_region()`.
Der SCL-Kopf listet, welche FB-Typen aus Template bzw. generisch kamen.
- `--strict` bricht ab (Exit 1, keine SCL), wenn ein FB-Typ der Anlage kein
Template hat. Ein `TemplateError` (fehlende Variable, Syntaxfehler) bricht
ebenfalls ab — lieber keine SCL als eine mit stillen Lücken.
- Ohne `--templates` werden Templates gar nicht geladen; die Ausgabe ist die
bisherige (Golden-Files `tests/fixtures/scl/*_FB_Main.scl`). Mit Templates
pinnen `*_FB_Main_templates.scl` das Ergebnis.
- `bin/scl_skeleton.bat`/`.sh` reichen die Schalter nur durch; der Standardaufruf
ohne `--templates` nutzt also **keine** Templates.
**Nicht Produktion:** `doc/TRO_Katalog/scl_templates/*.scl` sind die realen
Referenz-Aufrufstellen. Gegen sie prüft `tests/test_scl_templates.py` jedes
Template im Round-Trip; in die erzeugte SCL fliessen sie nicht ein.
## Daten
Bestandsaufnahme aller Daten, die in den Referenz-Aufrufstellen und FB-Schnittstellen
unter `doc/TRO_Katalog/scl_templates/*.scl` vorkommen (Stand 2026-10-10), und woher
der Generator sie bekommen muss. Grundlage: die sieben Aufrufstellen
(`FB_ILS_MTRO_1Sep`, `_1Sep1Swi`, `_1Sep2Swi`, `_1Sep_SSCC`, `_PinStore_Auto`,
`_Vario`, `_Vario_workStation_CallSite`) und die `VAR_INPUT/OUTPUT/IN_OUT`-Abschnitte der
vier vollständig vorliegenden FBs (`FB_ILS_MTRO_2Sep1Swi`, `FB_ILS_MTRO_Vario_workStation`,
`FB_EmptyCarrBuffer`, `FB_LoadingBoom_INBOUND`). Die Beispielwerte stammen aus diesen
Dateien.
**Quellen** (Spalte "Quelle"):
| Kürzel | Quelle | Was sie liefert |
|---|---|---|
| **L** | Anlagen-Hardware = mechanisches Layout (CSV-Export ILS 2.0, `data/*.csv`) | Bauteile (`TeileArt`: Separator, Scanner, Weiche, Kreisel, Strecke, Gefällestrecke, BTMT Be-/Entladung, Ein-/Ausschleuselement), `Nachbarn`/`TrackIds`, `Merkmale` (Drehrichtung, Antriebfahrtrichtung, Höhen, Längen, Anzahl Separatoren/Scanner, Zuordnung). Daraus abgeleitet: TRO-Typ, Topologie, Reihenfolge, Ziele. |
| **E** | Hardwareplanung = Elektroplanung (EA-Liste, TIA-Import, WSCAD-Import, `*_positionsdraw.json`; siehe `doc/HundM/EA-Listen-Analyse.md`) | Betriebsmittelkennzeichen und Variablennamen (`BG…` Sensoren, `MB…` Ventile, `MA…` Motoren, `FC…` Motorschutz, `SF…` Taster, `BX…` Scanner), Hardware-Kennungen, welche optionalen Sensoren verbaut sind, Zuordnung zu Schaltschrank und Not-Halt-Kreis. |
| **K** | Namenskonvention | Name folgt aus TRO-Nummer bzw. Staubereichs-/Weichennummer (`cMainTro<n>`, `cSep<n>.<i>`, `"DB_Interface_HMI".stTRO.TRO<n>` …). Keine externe Quelle nötig, sobald Nummernkreis und Regel feststehen. |
| **P** | Parameter / Projektstandard | Weder Layout noch E-Planung: Zeiten, Schwellwerte, Projektkonstanten, WCS-/Inbetriebnahme-Logik. Kandidaten für Typ-Defaults (`doc/TRO_Typen.md`). |
`L + E` heisst: das Layout sagt, **dass/wie viele** es gibt und **wo**, die
E-Planung liefert den **Namen/die Adresse**. Das ist der häufigste Fall.
Die zwei folgenden Abschnitte listen **alle Angaben**, die die beiden externen
Quellen heute enthalten (Stand 2026-10-11, geprüft an `data/*.csv` und an den
Exporten unter `kabellaengen/work/`: `easy_sps-1_EA/_TIA/_WSCAD.xlsx`,
`Mubea_positionsdraw.json`). Was sich daraus für TROs und SCL gewinnen lässt und
zwei kommentierte SCL-Beispiele: `doc/Datenquellen_Layout_Hardwareplanung.md`.
### Angaben 1: Anlagen-Hardware (mechanisches Layout, CSV)
Eine Zeile je Bauteil, Trennzeichen `;`, `utf-8-sig`. Gelesen von
`material_flow.load_elements()`.
#### Spalten (alle Bauteilarten)
| Spalte | Beispiel | Bedeutung | Nutzen für TRO/SCL |
|---|---|---|---|
| `Elementnummer` | `1` | laufende Zeilennummer des Exports | keiner (nicht stabil) |
| `TeileArt` | `ILS 2.0 Separator` | Bauteilart (Liste unten) | Knotenart, TRO-Typ |
| `TeileId` | `0008` | stabile ID im Layout | Schlüssel für Topologie, Overrides, E-Brücke |
| `Bezeichnung` | `VF_1`, `Kreisel :3` | Klartext aus der Zeichnung | Anzeige, REGION-Kommentar |
| `Planquadrat` | `B/1-C/4` | Raster der Zeichnung | Bericht/Suche |
| `Anzahl` | `1` | Stückzahl | keiner |
| `Position` | `5878.63, 4330.96, 2128.70` | Einfügepunkt x, y, z in mm | Lagebezug zur E-Planung (`dxf_registration`), Abstände |
| `Boundingbox` | `2837.82, 6404.92, 731.58` | Ausdehnung x, y, z in mm | Lage von Weichen/Bahnen |
| `Insertpoint` | `TV0UEkxg…` | kodierter Einfügepunkt (Base64) | intern |
| `K1`–`K4` | `AK41AC7XAFQk` | Anschluss-Koordinatensysteme (Base64, je x/y/z) | Anschlussseite, Lage der AS/ES (`decode_kpoint`) |
| `Nachbarn` | `0006-L, 0014` | berührende Bauteile, bei Kreiseln mit Bahn `-L`/`-R` | Topologie (Kanten) |
| `TrackIds` | `0044, 0027, 0026` | Anbauteile eines Containers in Baureihenfolge bzw. Umlaufrichtung | Reihenfolge Separator/AS/ES auf Strecke und Kreisel |
| `Fehler`, `Warnungen` | `nur ein Partner`, `unverbunden` | Prüfmeldungen des Exports | Qualitätsprüfung |
| `Merkmale` | JSON | artspezifische Eigenschaften (unten) | siehe unten |
#### Merkmale je `TeileArt`
| TeileArt | Merkmale | Nutzen für TRO/SCL |
|---|---|---|
| `ILS 2.0 Kreisel` | `Name`, `Kreiselart` (`STANDARD`), `Drehrichtung` (`UZS`/`GUZ`), `Drehung`, `Höhe in m`, `Abstand (Kreiselachse A - Kreiselachse) in Meter`, `Anzahl der Separatoren`, `Anzahl der Scanner`, `Anzahl der Rampen`, `Geruest fuer Einzelmodul` | Host eines TRO (`xInCarouselRun1/2`), Umlaufrichtung → Reihenfolge, Länge → Staulänge; Anzahlen zur Plausibilisierung |
| `ILS 2.0 Strecke` | `Typ` (`Streckengruppe`), `Seite_AS`, `Seite_ES`, `Winkel`, `VF_Winkel`, `DeltaH_mm`, `DeltaL_mm`, `L_VF_m` (angetriebene Teilstücke), `L_GF_m` (Gefälleteilstücke), `Montagehoehe_m`, `Hoehe_Von_mm`, `Hoehe_Bis_mm`, `Antriebfahrtrichtung` (`horizontal`/`Auf`), `Anzahl_Separator`, `Anzahl_Scanner`, `Separatorlist` | angetrieben (`L_VF_m`, `Auf`) → Vario/Antrieb; Längen → `tConvEmpty`, Staulänge; Anzahlen zur Plausibilisierung |
| `ILS 2.0 Gefaellestrecke` | wie Strecke, `L_VF_m` und `Antriebfahrtrichtung` leer | Host "Gefälle" → `1Sep`, `xInCarouselRun1 := TRUE` |
| `ILS 2.0 Separator` | `Zuordnung` (Host, ggf. mit Bahn `-L`/`-R`), `ARTINR`, `Höhe`, `Drehung`, `Typ` | **TRO-Anker**: jeder Separator ist ein TRO oder Teil davon; Host → TRO-Typ |
| `ILS 2.0 Scanner` | `Zuordnung`, `Naechster Separator`, `ARTINR`, `Höhe`, `Drehung`, `Typ` | Scanner am TRO (`xPluggedScanner`, `stInBarcodeReader`, REGION-Titel) |
| `ILS Weiche` | `Kreisel A`, `Kreisel B`, `Achse von Kreisel`, `Beruehrung` (`Kreis/Kreis`), `Erzeugt aus` | Kreisel-Kreisel-Weiche → `1SepNSwi`, `xCar2NecRight/Left`, zweiter Kreisel `xInCarouselRun2` |
| `ILS Ausschleuselement` | `Kreisel`, `Strecke`/`Förderer`, `Streckenende` (`Anfang`), `Element` (`AS_Element_90_links_3D`), `ARTINR`, `Erzeugt aus` | Abgang Kreisel → Strecke: Weichenausgang `Exit3/4`, Ziel je Abgang |
| `ILS Einschleuselement` | wie Ausschleuselement, `Streckenende` `Ende` | Zusammenführung Strecke → Kreisel: Vorfahrt (`xPrioDir`, PriorityManager) |
| `ILS 2.0 BTMT Beladung` / `Entladung` | `ARTINR`, `Höhe`, `Drehung` | Systemgrenze `SYSn`, `xIsBorner`, Ziele; Ladeschleife |
| `Eingang` / `Ausgang` | `ARTINR`, `Höhe`, `Drehung` | Systemgrenze zu Nachbaranlage, Übergabe-Staubereich |
`ARTINR` (Artikelnummer) unterscheidet Bauarten, z. B. Separator `200000077`,
Scanner `829534306`; daran ließe sich die Weichenbauart (`xNewSwitch`) prüfen.
Zusätzlich gibt es den **Sivas-Stücklisten-Export** (`data/*_sivas.csv`:
`SivasNummer`, `Dispogruppe`, `Laenge[mm]`, `IDs`, `details` mit
Förder-/Gefällegruppen, Kurven, `MotorSeite`). Er ist eine Stückliste, keine
Topologie, und wird nicht gelesen; brauchbar wäre `MotorSeite` und die
Gruppenlängen.
**Nicht im Layout:** Betriebsmittelkennzeichen, E/A-Adressen, welche optionalen
Sensoren verbaut sind, Schaltschrank/Steuerung, Zeiten, Ziel-Nummern des WCS.
### Angaben 2: Hardwareplanung (EA-Liste, TIA-Import, WSCAD-Import, positionsdraw)
Vier Exporte derselben E-Planung. Gemeinsamer Schlüssel ist das
**Betriebsmittelkennzeichen (BMK)** `BG3306`, `MB3306` …; nur `positionsdraw`
kennt eine Lage, nur EA/TIA/WSCAD kennen die E/A-Adresse.
**`<projekt>-<n>_EA.xlsx`** — Blatt "Ein-, Ausgaenge", je SPS `n` eine Datei
| Spalte | Beispiel | Bedeutung |
|---|---|---|
| `Name` | `E324.0` | E/A-Adresse |
| `Kommentar` | `Stausensor 1 (ILS-CV M0108)` | Funktionsrolle, lfd. Nr., Fördereinheit (M-Nummer), Bereich |
| `Zeile` | `2` | Verweis Schaltplan |
**`<projekt>-<n>_TIA.xlsx`** — Importdatei für das TIA Portal
| Blatt / Spalte | Beispiel | Bedeutung | Nutzen für SCL |
|---|---|---|---|
| PLC Tags `Name` | `BG3240`, `MB3306`, `MA0062`, `FC0062` | BMK = Variablenname | direkt die Namen in `FB_Main` (`=> "MB3306"`, `:= "FC0145"`) |
| PLC Tags `Path` | `Import` | Variablentabelle | Ordner der Variablentabelle |
| PLC Tags `Data Type` | `Bool` | Datentyp (nur Bool) | – |
| PLC Tags `Logical Address` | `%E324.0`, `%A006.2` | Absolutadresse | Prüfung, `FB_CallSensors` |
| PLC Tags `Comment` | `Stausensor 1 (ILS-CV M0108)` | wie EA-Kommentar | Rolle, M-Nummer |
| PLC Tags `Hmi Visible/Accessible/Writeable` | `True` | HMI-Flags | – |
| PLC Tags `Typeobject ID`, `Version ID` | leer | – | – |
| Constants `Name` | `cInBG3240`, `cInFC0062`, `cOutMA0062` | Index-Konstante je Signal | genau die Indizes in `"DB_Inputs".Sensors["cInBG3306"]` |
| Constants `Data Type`, `Value` | `Int`, `1` | Array-Index | Dimensionierung `DB_Inputs` |
**`<projekt>-<n>_WSCAD.xlsx`** — Importdatei für das E-CAD
| Spalte | Beispiel | Bedeutung | Nutzen für SCL |
|---|---|---|---|
| `Anschluss` | `E324.0` | E/A-Adresse | Schlüssel zu EA/TIA |
| `Kommentar` | wie EA | | |
| `Bezug` | `=A01+UC0101-KF1DI1` | Anlage, Feldstation (`UC…`), Baugruppe (`KF1DI1`, `KF1FDQ2` = F-Karte) | Steuerungs-/Schaltschrankzuordnung, Safety-Signale |
| `Symboltext`, `Sondertext 1..8` | meist leer | | |
**`<zeichnung>_positionsdraw.json`** — Kabellängen-Tool, E-Symbole aus der Zeichnung
| Schlüssel / Attribut | Beispiel | Bedeutung | Nutzen für TRO/SCL |
|---|---|---|---|
| `sensors{BMK@SPS}` | `BG0542@UNKNOWN` | Feldgeräte (BG, MB, MA …) | Hauptquelle der Brücke |
| `.IO` / `.B` | `BG0542` | BMK | Schlüssel zu TIA |
| `.pos` | `[-42369.8, -11437.2]` | Lage in mm | **Zuordnung zum Layout-Bauteil** (`e_planning.py`) |
| `.VERW` | `IN SEP`, `SEP`, `STAU_REF-LT`, `AE ABZWEIG`, `ABZWEIG-K104`, `BT BELADEN`, `SYNC P1..4`, `ABFR.FINGER`, `ÜBERGABE`, `ÜBERNAHME`, `BANDBRUCH`, `KETTENBRUCH`, `ZAPPLER P1`, `MOT_0,37kW` | Verwendung (kontrolliertes Vokabular) | **Rolle** des Signals im TRO |
| `.BEZEICHNUNG` | `STAUSENSOR STR. 01`, `PRÜFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)` | Klartext | Strecken-/Kreiselnummer, Plausibilisierung |
| `.BESCHR` | `STAUSENSOR` | Gerätebeschreibung | – |
| `.KENNZEICHNUNG` | `=A1+UH01-KF102` | Steuerung `UH01`, E/A-Station `KF102` | Steuerung, TRO-Nummernkreis, Schaltschrank |
| `.ARTINR` | `829434300` | Artikelnummer Gerät | Gerätetyp |
| `.SPS` | `''`, `A/B` | SPS-Nummer (oft leer) | Steuerung |
| `.A`, `.C`, `.ID`, `.MENGE`, `.POSITION`, `.GRUPPE`, `.ETIKETTE`, `.AUFLOESE`, `.TEXT-D/E/ES/F` | | Blockattribute des Symbols | – |
| `schaltschrank_elemente{…}` | `FC0200` `MOTORSCHUTZ CONVEYER #100`, `SF…` Taster, `PF…` Leuchten, `QA7000` Hauptschütze, `GF` Encoder, `BP` Druckschalter | Elemente im Schrank, ohne Feldlage | Motorschutz ↔ Motor, Bedienelemente, Encoder |
| `mappings` | `UH01: [BMK …]` bzw. Verteiler → BMK | Zuordnung zur Steuerung/Verteiler aus der Verkabelung | Steuerungsaufteilung |
| `distributors`, `racks`, `tunnels` | | Verteiler, Trassen | – |
| `warnings`, `not_found`, `double_ids` | | Prüfprotokoll | Qualitätsprüfung |
**BMK-Präfixe:** `BG` Sensor, `SF` Taster/Schalter, `DI` Diagnose, `BP`
Druckschalter, `GF` Encoder, `BX` Scanner (nur PROFINET, nicht in den Digitallisten),
`MB` Ventil (Stopper, Weiche, Ausschleuser), `MA`/`QA` Motor/Schütz, `DQ`
Sonderausgang, `FC` Motorschutz, `PF` Leuchtmelder.
**Nicht in der Hardwareplanung:** Topologie (wer auf wen folgt), TRO-Grenzen,
Ziele/Weichenrichtungen, Zeiten, Analogwerte und PROFINET-Geräte (Cognex-Variablen
stehen in der TIA-Gerätekonfiguration, nicht im Tag-Import).
### TRO-Kopf und Instanzaufruf
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Existenz, Anzahl, Reihenfolge der TROs | `REGION TRO 100 …` | L | `tro_flow` → `DerivedTro`; REGION-Reihenfolge `scl_context.flow_order()` |
| TRO-Typ / FB-Typ | `FB_ILS_MTRO_1Sep1Swi` | L (→ Katalog) | Typ aus Separator-Host (`doc/500573_Mubea/TRO_Identifikation_500573.md`), FB aus `tro_catalog` |
| TRO-Nummer, Instanzname | `#TRO107` | L + E | Anzahl aus L; Nummernkreis je Steuerung (100er = UH01, 200er = UH02 …) ist Projektvorgabe |
| REGION-Titel | `TRO 267 (1Sep2Switch)+Scanner(BX2100)` | L + E | Typ aus L, Scanner-BMK aus E |
| `nInMainTroNo` | `"cMainTro100"` | K | Wert der Konstante in der PLC-Konstantentabelle |
| `nOutStateLast` | `"DB_GLOB_TroState".arTroState["cMainTro100"]` | K | |
| `stInOutHMI` | `"DB_Interface_HMI".stTRO.TRO100` | K | PinStore/Puffer: eigener UDT `UDT_HMI_TRO_PinStore` |
| `xInRelease` | `"DB_TRO_TEST".TRO100`, `TRUE`, WCS-Verbindung aktiv | P | Freigabe-/Testlogik, Inbetriebnahme |
| `xInAllRdyToStart`, `xInCancel`, `xInReset` | `TRUE`, `"cTodoFalse"` | P | meist Konstante |
| `stInSettings.xIsBorner` (`xIsBorner1/2`) | `TRUE` | L | erster TRO nach Einfahrt in die Anlage (Beladestation / Systemgrenze `SYSn`) |
| `stInSettings.nBornerDest(1/2)` | – | P | Standardziel für neu angelegte Carrier (WCS) |
| `stInSettings.bSepType` (`bSep1Type/2Type`) | `.%X2 := TRUE` | E + P | X0 erstes Bein halten, X1 Beladekontrolle, X2 Beladeerfassung — hängt an verbauter Sensorik und Prozess |
| `stInSettings.nTroStopper1/2`, `nTroSwitchExit2/3` | – | L + K | Nummern der Nachbar-TROs |
| `stInSettings.sJamSrc` / `sJamDest` | – | L + K | Namen der benachbarten Staubereiche |
| `stInSettings.xCar2NecRight/Left` | – | L | zweiter Kreisel nötig für Richtung rechts/links (Topologie an einer Kreisel-Weiche) |
| `tPartSenHigh`, `tInSenHigh`, `tDummyTimeNormal`, `tDummyTimeBorner` | FB-Defaults 500 ms / 3 s / 0 ms | P | Typ-Defaults |
### Separator (`stInSeparator<n>`, `xOutStopper<n>`)
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Anzahl Separatoren je TRO | `stInSeparator1`, `stInSeparator2` | L | `ILS 2.0 Separator` + `Zuordnung`, Typ-Katalog `items` |
| `Settings.nSeparatorNo` | `"cSep107.1"` | K | Layout-Separator-ID (`TeileId`) ist **nicht** diese Nummer |
| Sensor im Stopper `stSenInSep` | `"DB_Inputs".Sensors["cInBG3242"]` | E | |
| Sensor nach Stopper `stSenSepLeft`, Beladesensor `stSenPart`, Scan-Start `stSenStartScan` | `["cInBG4465"]` | E | nur wenn verbaut |
| Stopperventil `xOutStopper<n>` / `xOutStopper` | `=> "MB3242"` | E | BG- und MB-Nummer sind in der E-Planung meist gleich (BG3242/MB3242) |
| `Settings.xPluggedJam` | `TRUE` | E | Stausensor verbaut? |
| `Settings.xPluggedPart` | `FALSE` | E | Beladesensor verbaut? |
| `Settings.xPluggedConfirm`, `xPluggedNullBT` | `FALSE` | E | |
| `Settings.xPluggedScanner` | `TRUE` | L | Scanner am Separator (`DerivedTro.scanners`) |
| `Settings.tDelayToNextItem` | `"TimeForSepWaiting_Short"` / `_Medium` | P (+ L) | Wahl Short/Medium hängt am Host (Kreisel/Strecke/Gefälle) |
| `Settings.tTrailingTime`, `tHandlingTime` | `t#600ms`, `t#200ms`, `"cTimeSepTrailing"` | P | Typ-Default, IBN |
| `Settings.tJam` | `"TimeForSepJam"` | P | Projektkonstante |
| `Settings.tSenFree`, `tSenWait` | `T#1500ms`, `T#4000ms` | P | |
### Staubereiche (Jam)
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Anzahl Ein-/Ausgänge und Zuordnung Exit-Nr ↔ Nachbar | `arInOutJamEntr1`, `arInOutJamExit2..4` | L | ein Eingang je eigenem Staubereich (`TroDocument.jamAreas`, Kennung `J<Separator>`; mehrere Vorgänger fahren in denselben ein), je Nachfolger/Weichenabgang ein Ausgang (`SclJam`) |
| Gleicher Staubereich auf beiden Seiten einer Kante | Exit von A = Entr von B | L | Identität aus der Topologie |
| Staubereichs-DB und -Index | `"DB_JamArea0102".stJam0102_1.arCarrier` / `.stData` | K (+ E) | Nummerierung der Staubereiche ist Projektvorgabe |
| Stausensor je Ausgang | `stInSenJam2 := "DB_Inputs".Sensors["cInBG3243"]` | E | |
| Übergabe-Staubereich an Steuerungsgrenze | `#arCarrierHandoverJam5001`, `#stJamDataHandoverJam5001` | L + E | Steuerungsaufteilung aus der E-Planung |
| `xInJamAtExit2/3` | – | P | NIU |
### Weiche (`stInSwitch<n>`, `FC_Direction`)
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Anzahl Weichen je TRO | `stInSwitch1`, `stInSwitch2` | L | `ILS Weiche`, Ausschleuselement, Typ-Katalog |
| `Settings.nSwitchNo` | `"cSwi107.1"` | K | |
| `Settings.nTo1Destinations` | `3` | L | Erreichbarkeit im TRO-Graphen (`reachable_via()`); bei mehreren Weichen Planungsentscheidung |
| Zielliste je Abgang | `cDest…` | L + P | welche Ziele: L; Zielnummern: Projekt/WCS |
| `Settings.xPluggedExSen3/4`, `stSenExit3/4` | `FALSE` | E | Abgangssensoren verbaut? |
| `Settings.xNewSwitch` | `TRUE` | E | Weichenbauart (ggf. über `ARTINR` im CSV prüfbar) |
| Weichenventile `xOutSw<n>ExTo3/4`, `xOutSw1EntrTo1/2` | `=> "MB3304"` | E | |
| `FC_Direction`-Aufruf | `nInSwitchNo := "cSwi107.1"`, `InOutCarrier := …stJam0102_1.arCarrier[1]` | K + L | Carrier aus dem Eingangs-Staubereich |
| Vorfahrt `stInSettings.xPrioDir<n>`, `stInPriorityManagerDir<n>` | `"DB_Prozessdaten".stPriorityManager.udt_TRO107.TroIn` | L + K | Vorfahrt nötig, wo zwei Ströme zusammenlaufen (Kreuzung/Zusammenführung) |
| `stInPriorityManager.xReleaseOk` | `TRUE` ("braucht keine Vorfahrtsregelung") | L | TRUE, wenn keine Zusammenführung |
### Scanner / Barcode / SSCC
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Scanner vorhanden, Anzahl | `stInBarcodeReader`, `stInBarcodeReader1/2` | L | `ILS 2.0 Scanner`, `Anzahl_Scanner` |
| Scanner-BMK und Index | `#arCognexInterface["cScannerBX2100"]` | E | |
| Cognex-Variablen | `"bx2100-TriggerReady"`, `-TriggerAck`, `-Results Available`, `-General Fault`, `-TriggerEnable`, `-Trigger`, `-ResultsAck` | E | aus dem BMK nach Regel bildbar, wenn die TIA-Variablentabelle dem Muster folgt |
| Hardware-Kennung Ergebnisse | `"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1"` | E | |
| Kein Scanner | `#stCognexInterfaceDummy` | L | |
| `Settings.xActivateCheckASCI`, `tTimeout` | `TRUE`, `T#5s` | P | |
| `Settings.nStation`, `nScannerPurpose` | `111`, `0` | P | WCS |
| Zweck SSCC-Scanner, `xInPluggedSSCC` | `#stCognexIntfTro105` | E + P | Layout kennt den zweiten Scanner, nicht seinen Zweck |
| `stSenEndMeasurement` | `["cInBG3281"]` | E | |
| `FC_CARR_GLOB_CORRECTION` | `nInStation := 0`, Carrier aus Eingangs-Staubereich | P + L | |
| WCS-Telegramm, HMI-SSCC-Ausgabe | `FC_Send_GOHDReqM`, `"DB_Interface_HMI".stSSCC.SSCC1107` | P | WCS-Schnittstelle |
### Vario (`stInVario`) und Arbeitsplatz
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Vario vorhanden | `FB_ILS_MTRO_Vario` | L | angetriebene Strecke / Variofoerderer |
| Sensoren `stSenCarrInPos`, `stSenFinger`, `stSenLastPos`, `stSenJam`, `stSenNextSep`, `stSenSepVario` | `["cInBG3026"]` … | E | |
| Motor `xOutVarioMotor`, Motorschutz `xInMotorProtection` | `"MA0145"`, `"FC0145"` | E | |
| Finger-Staubereich `arInOutJamFinger` | `"DB_JamArea4103".stJam4103_1` | L + K | |
| `Settings.tConvEmpty` | `T#45000ms` | L + P | aus Förderlänge (`L_VF_m`) und Geschwindigkeit ableitbar |
| `Settings.nCountCarrier`, `tWaitForJog`, `tWaitForStop`, `tCarrierInPosDelay`, `tCarrierInPosEdgeDelay`, `tRasterHigh/Low`, `xJamSenDelayActive`, `tJamSenDelay`, `xContinousEmptying`, `SettingsVarioFingerMonitoring` | `1`, `T#300ms` … | P | |
| Arbeitsplatz `stInStationSensor`, `xInReleaseStation`, `fbConvVario.xBeladenError` | `["cInBG3314"]`, `#"rTrig_Workstation 1".CLK` | E | Taster/Sensor am Arbeitsplatz |
### Antrieb, Sicherheit, Betriebsart
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Fördertechnik, die den TRO trägt | `xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed`; Gefälle: `TRUE` | L | Host-Kreisel aus `DerivedTro.hosts`; Instanzname nach K |
| `xInCarouselRun2` | zweiter Kreisel | L | TRO an der Berührung zweier Kreisel |
| `xInConveyorRunIn/Out` (PinStore) | `#fbSortConv1101`, `#fbExitConv1103` | L + K | |
| `xInSftyOk` | `"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q` | E | Not-Halt-Kreis |
| `stInOutMachineState` | `#stInOutControlUnitCabinetOF1` | E | Schaltschrank (OF1/OF2) |
| `xInAutomatic`, `xInErrorQuit`, `xInQuitLocal` | `#t_Automatik_OF1 AND "SF0120"`, `"SF0002"`, `"SF0121" OR "SF0117"` | E | Bedienelemente |
| `nInTestTarget` | `"IBN".nTestTarget111` | P | |
### Pin-Lager (`FB_ILS_MTRO_PinStore_Auto`) und Leerträgerpuffer (`FB_EmptyCarrBuffer`)
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Anzahl Lagerlinien | `nNoOfLines := 19`, `arInSep[1..5]` | L | Separatoren auf Kreisel mit Pinband / `Separatorlist` |
| Positionen je Linie, Staulänge | `nNoOfPositions := 70`, `nJamLength := 4` / `"cStorage111ExitJam2"` | L + P | aus Linienlänge und Teilung |
| Liniensensoren `arInSep[n].stSenTrolley`, `arInJamSensor[n]`, `stInSenJamExit` | `["cInBG3270"]` | E | |
| Linienventile `arOutSeparatorStore[n]` | `"MB3270"` | E | |
| Pin-Separator `stInSeparatorPin`, `xOutSeparatorPin` | `["cInBG3000"]`, `"MB3000"` | E | |
| Referenzsensor "Zappler" `stInSensorReference` | `["cInBG3001"]` | E | |
| Encoder `stInEncoderParameter` | `#fbEncoderGF2200.nOutPosActDif` | E | |
| `nDestGeneral`, `nConvStations`, `nStationsMax`, `nStorageThreshold` | `"cDest11100"`, `"cHooksSorterMaxPin111"` | P | WCS/Projekt |
| `nInBufferNr`, `xInBufferReq/Release/ReleaseNext`, `xInPause` | `101`/`102` | P | Pufferlogik |
| Lager-/Stations-DBs | `"DB_Storage1.1.1".arLine`, `"DB_ConveyorStations"`, `"DB_HandshakeStorage"` | K + P | |
| Linien-Zeiten, Simulation | `"cTimeSepHandlingStorageLine"`, `stSimu.*` | P | |
### Ladeschleife (`FB_LoadingBoom_INBOUND`)
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|---|---|---|---|
| Ladeschleife vorhanden, Lage | – | L | Beladestation (`ILS 2.0 BTMT Beladung`), prüfen |
| Bedienelemente `stInControl.*` | Taster, Knebel-/Schlüsselschalter | E | |
| Ausleger/Stützfuß `stInBoom.*`, `stInFoot.*` | Endlagen, Motorschutz, Bumper | E | |
| Analogwerte `stInTilt.wTilt`, `stInDistance.wDistance` | Neigung, Abstand | E | |
| Sensoren `stInSen2mThreshold`, `stInSenJamEntr/Exit1/Exit2`, `stInSenStopper1/2`, `stInSenGrid` | | E | |
| Ausgänge `stOutStopper.*`, `stOutBoom.*`, `stOutFoot.*`, `xOutLedMovement`, `xOutLight`, `xOutLedBumper`, `xOutCarousel` | | E | |
| Schwellwerte `nDiff`, `nZeroOk`, `nStartSlow/Fast`, `nDistanceZero`, `tDiff`, `tZero`, `nTiltComp` | | P | Inbetriebnahme |
| `stInSettings.*` Zeiten | `tBoomInput := T#3500ms` … | P | |
| Exit-Staubereich `arInOutCarrierExit`, `stInOutCarrierExit` | | L + K | |
### Zusammenfassung
- **Aus dem Hardwarelayout (L)** kommt die **Struktur**: welche TROs es gibt, ihr Typ
und damit FB und Anzahl der Parameterblöcke (Separatoren, Weichen, Scanner, Vario,
Lagerlinien), Vorgänger/Nachfolger und damit Ein-/Ausgangs-Staubereiche,
REGION-Reihenfolge, Ziele je Weichenabgang (`nTo1Destinations`), Vorfahrt ja/nein,
Systemgrenzen (`xIsBorner`), die tragende Fördertechnik (`xInCarouselRun`, Gefälle →
`TRUE`), ob ein Scanner am Separator sitzt (`xPluggedScanner`) und Längen als
Grundlage für Zeiten/Staulängen. Das meiste davon ist heute in `DerivedTro` →
`SclTro` abgebildet; Vorfahrt, `xIsBorner` und Längen noch nicht.
- **Aus der Elektroplanung (E)** kommen **alle Namen und Adressen**: jeder Sensor
(`BG`), jedes Ventil (`MB`), Motor (`MA`), Motorschutz (`FC`), Taster (`SF`), Scanner
(`BX`) samt Cognex-Variablen und Hardware-Kennung, die `xPlugged*`-Schalter (was ist
tatsächlich verbaut), Weichenbauart, Not-Halt-Kreis, Schaltschrank und
Steuerungsgrenzen. Im Render-Kontext sind das die `SclName` mit `todo=True`
(`TODO(E-Planung)`). Offen ist die Brücke: die EA-Liste kennt keine Topologie; es
braucht eine Zuordnung Layout-Bauteil (Separator-`TeileId`) ↔ BMK (`BG3242`/`MB3242`).
Diese Brücke baut `lib/e_planning.py` über die Symbolpositionen in
`*_positionsdraw.json` des Kabellängen-Tools (`doc/E-Planung_Bruecke.md`);
heute kommen damit Separator-Sensor und Stopperventil in die SCL
(`scl_skeleton --eplan`).
- **Namenskonvention (K)**: `cMainTro`, `cSep`, `cSwi`, Staubereichs-DBs, HMI- und
State-Pfade — sobald Nummernkreise feststehen, ohne weitere Quelle erzeugbar.
- **Parameter (P)**: Zeiten, Projektkonstanten, WCS-Ziele/Stationen, Test-/IBN-Signale —
weder L noch E; gehören als Typ-Defaults in den Katalog bzw. in eine
Projektkonfiguration.
+416
View File
@@ -0,0 +1,416 @@
# Konzept: Layout und Hardwareplanung → Anlagen-JSON → SCL
Stand 2026-10-11. Zielbild für den Ausbau der Pipeline, damit die beiden
Fachabteilungen unabhängig voneinander und zeitversetzt liefern können. Welche
Angaben aus welcher Quelle kommen, steht in `doc/Datenquellen_Layout_Hardwareplanung.md`;
die Rohangaben beider Quellen in `doc/Klassenhierarchie.md`, Kapitel "Daten".
## Ausgangslage
Die Produktion läuft über zwei Abteilungen:
1. **Anlagenlayout** (aus dem Angebotswesen, detaillierter aus der Projektierung).
Liefert alles für die TROs: Bauteile, Abfolge, Weichen, Staubereiche,
Systemgrenzen.
2. **Hardwareplanung** (Karten, Kabel, Sensoren, Aktoren). Liefert die globalen
Variablen und die tatsächlichen Sensoren und Aktoren mit Namen und Adressen.
Die Hardwareplanung kommt **Tage bis Wochen später** als das Layout. Das System
muss deshalb mit dem Layout allein ein brauchbares SCL-Skelett erzeugen und die
Hardware später nachtragen können, ohne Layout-Arbeit zu wiederholen.
## Zielbild
```text
Abteilung 1: Anlagenlayout Abteilung 2: Hardwareplanung (später)
CSV ──tro_flow──> <csv>_tro.json TIA.xlsx, positionsdraw.json
(TROs, Abfolge, ──hw_import──> <projekt>_hw.json
Staubereiche, Slots) (globale Variablen, Geräte)
│ │
└──────────── merge_hw_layout ─────────────────┘
│
<csv>_anlage.json (Layout + Zuordnungen)
│
scl_skeleton ──> FB_Main.scl, FB_CallSensors.scl, …
```
| Stufe | Skript | Eingabe | Ausgabe | Status |
|---|---|---|---|---|
| 1 Layout | `tro_flow.py --json` | CSV, `cfg/tro_overrides.ini` | `<csv>_tro.json` | vorhanden, wird erweitert |
| 2 Hardware | `hw_import.py` | `*_TIA.xlsx`, `*_positionsdraw.json` (optional `*_EA.xlsx`/`*_WSCAD.xlsx` ohne Positionsdatei) | `<projekt>_hw.json` | vorhanden (2026-10-15), ohne EA/WSCAD |
| 3 Zuordnung | `merge_hw_layout.py` | `<csv>_tro.json`, `<projekt>_hw.json`, Regeln, `cfg/hw_overrides.ini` | `<csv>_anlage.json`, Bericht `.md` | vorhanden (2026-10-15, Logik aus `e_planning.py`) |
| 4 SCL | `scl_skeleton.py` | `<csv>_anlage.json` (oder nur `<csv>_tro.json`) | `.scl` | vorhanden, wird umgestellt |
Grundsätze:
- **Stufe 1 allein reicht für ein Skelett.** Ohne Hardware wird jeder offene Wert
wie heute `TODO(E-Planung)`.
- **Die Stufen verändern sich nicht gegenseitig.** Die Zuordnung legt ihre
Ergebnisse neben das Layout, statt es zu überschreiben. Ein neuer Layoutstand
oder ein neuer Hardwarestand heißt: die betroffene Stufe und alles danach neu
ausführen.
- **Lieber eine Lücke als ein falscher Name** (wie heute in `cfg/e_planning.ini`):
was keine Regel sicher trifft, bleibt offen und steht im Bericht.
- **Drei Quellen, jede sichtbar:** Layout, Hardwareplanung und Defaults
(`cfg/defaults.ini`, Typ-Katalog). Jede SCL-Zeile lässt sich einer davon zuordnen.
## Umbauten
### 1. TRO-JSON zur Layout-JSON ausbauen (Stufe 1)
Heute fehlt der Zuordnung ein Ziel: die TRO-JSON sagt nicht, welche Stellen ein
TRO hat, an die Hardware gehört. Daran scheitern heute Stausensoren und
Weichenventile (`doc/E-Planung_Bruecke.md`, "Offen").
- **Slots je TRO:** je Separator, Weiche, Ausgang und Antrieb ein Eintrag mit der
Layout-ID des Bauteils und den Rollen, die die Hardwareplanung dort füllen muss.
```json
"slots": [
{"slot": "separator1", "layoutId": "0008", "roles": ["sensor", "stopper", "jamSensor?", "partSensor?"]},
{"slot": "switch1", "layoutId": "0037", "exit": 3, "roles": ["valve", "exitSensor?"]},
{"slot": "drive", "layoutId": "0002", "roles": ["carouselRun"]}
]
```
Die Slots ergeben sich aus dem Katalogtyp (`items`) und der Topologie; `?` =
optional, fehlt das Gerät, wird der zugehörige `xPlugged*`-Schalter `FALSE`.
- **Staubereiche als eigene Liste `jamAreas[]`:** ein Staubereich je TRO-Eingang,
mit allen Vorgängern, dem Förderer, auf dem er liegt, und seiner Lage auf dem
Förderer (siehe "Staubereiche" unten). Heute nur abgeleitet beim Rendern
(`scl_context.SclJam`). Dabei korrigieren: `scl_skeleton` erzeugt heute je
Vorgänger einen eigenen Eingang (`arInOutJamEntr1`, `…Entr2`); in den Referenzen
laufen mehrere Vorgänger in **einem** Staubereich zusammen, der TRO hat einen
Eingang (Ausnahme `2Sep1Swi`, PinStore).
- **Weitere Layout-Angaben:** `xIsBorner`, Vorfahrt ja/nein, tragender Förderer
mit Rolle (heute `hosts` ohne Rolle), Längen für `tConvEmpty` und Staulängen.
- **Ableitungen aus `scl_context` nach `tro_flow`/`model` verschieben,** damit die
JSON sie trägt und nicht erst das Rendern sie kennt.
- Schema `sps_skel-tro-v4`.
**Stand (2026-10-11), erster Teil umgesetzt:**
- `model.Slot` und `DerivedTro.slots`: `separator<n>`, `switch<n>`,
`scanner<n>`, `drive` mit Rollen, Anzahl aus dem Katalog (`derive_slots`).
Weichen-Slots haben noch keine `layoutId`: `tro_flow` hält das Weichen- bzw.
Ausschleuselement nicht am TRO fest.
- `model.JamArea` und `TroDocument.jamAreas`: je Separator laut Katalog ein
Eingangs-Staubereich `J<Separator>` mit allen Vorgängern, dem Host und der
Lage (`derive_jam_areas`). Bei 2Sep-Typen ist die Aufteilung der Vorgänger
auf die zwei Eingänge offen (Hinweis im Feld `note`); beim PinStore heißt
der Eingang vorläufig nach dem ersten Linien-Separator.
- `scl_context` liest die Staubereiche aus der JSON: **ein** `JamEntr` je
eigenem Staubereich statt je Vorgänger (Abweichung zu UH01–UH05 behoben),
jeder Ausgang nennt im Kommentar den Staubereich des Nachfolgers. Golden
Files neu erzeugt. Eine v3-Datei ohne `jamAreas` wird weiter gelesen.
- **Noch offen aus Umbau 1:** Layout-ID der Weichen, `xIsBorner`, Vorfahrt,
Host-Rolle, Längen (`tConvEmpty`, Staulängen), `<NNNN>`/`<k>` für den
SCL-Namen der Staubereiche.
### 2. Neues Skript `hw_import.py` (Stufe 2)
- Liest TIA-Import (PLC Tags + Constants) und `positionsdraw.json`. Das genügt:
bei Mubea stehen alle BMK der Positionsdatei im TIA-Import, die Kommentare sind
gleich; EA-Liste und WSCAD-Import bringen nichts dazu (EA ohne Namen, WSCAD nur
die E/A-Baugruppe, die `KENNZEICHNUNG` schon trägt). EA/WSCAD nur als Ersatz,
wenn keine Positionsdatei vorliegt (Weg "nur EA-Liste" in Stufe 3). Befund in
`doc/E-Planung_Bruecke.md`, "Nachprüfung 2026-10-15".
- Schreibt ein neues Modell `HardwareDocument`:
- **Geräte** je BMK: Adresse, Index-Konstante (`cInBG3306`), Kommentar, VERW,
Steuerung, Station/Schrank, Position (falls vorhanden);
- **globale Variablen**: alle Tags und Konstanten, Motor↔Motorschutz-Paare,
Taster, Not-Halt.
- Prüft die Exporte gegeneinander: BMK fehlt in einer Liste, doppelte BMK,
Adresse ohne Tag.
- Übernimmt aus `e_planning.py`: `load_positionsdraw`, `EDevice`, `pair_drives`.
- Erste Quelle: der TIA-Import, weil er Namen und Konstanten 1:1 liefert und die
spätere Prüfung des SCL ermöglicht.
### 3. Neues Skript `merge_hw_layout.py` (Stufe 3)
- Eingabe: `<csv>_tro.json` + `<projekt>_hw.json`; das CSV wird nicht mehr gelesen.
- Zuordnungswege, als Regeln in einer INI (heute `cfg/e_planning.ini`):
- **räumlich** über `positionsdraw`: die bestehende Logik aus `e_planning.py`
(Lagebezug über `dxf_registration`, Höchst-/Sollabstand), jetzt gegen die Slots;
- **Nummernregel**: `MB = BG + 1` bzw. gleiche Nummer, Motor = Motorschutz;
- **nur EA-Liste, ohne Positionen**: über die Kommentare
("in Separator 1 (ILS-CV M0108)") und eine von Hand gepflegte Tabelle
M-Nummer → Layout-Förderer.
- Handkorrekturen in `cfg/hw_overrides.ini` (analog `cfg/tro_overrides.ini`).
- Ausgabe `<csv>_anlage.json`: Layoutteil unverändert, daneben
```json
"bindings": [
{"layoutId": "0008", "role": "sensor", "bmk": "BG3306", "constant": "cInBG3306",
"address": "%E330.6", "source": "positionsdraw", "rule": "raeumlich",
"distance": 12.0, "status": "zugeordnet"}
]
```
`status`: `zugeordnet` | `offen` | `widerspruch`.
- **Stabile Schlüssel:** Zuordnungen hängen an TRO-ID + Slot + Rolle. Die TRO-IDs
sind ab der Festlegung durch die SPS-Programmierer stabil (siehe "TRO-IDs"
unten). Die Layout-`TeileId` taugt dafür **nicht**: sie ändert sich zwischen
zwei CSV-Exporten (siehe dort). `layoutId` in `bindings` dient nur zur
Nachverfolgung im aktuellen Stand.
- **Gegenprüfung** beider Quellen (Anzahl Separatoren vs. `IN SEP`, Weichen vs.
`AE ABZWEIG`, angetriebene Strecken vs. `MA`) in den Bericht.
**Stand Umbau 2/3 (2026-10-15), umgesetzt:**
- `lib/hw_import.py` (`bin/hw_import.bat`): TIA-Import (Blätter an der
Kopfzeile erkannt, xlsx ohne openpyxl gelesen) + Positionsdatei →
`<projekt>_hw.json` (`HardwareDocument`, `sps_skel-hw-v1`): je BMK Adresse,
Richtung, Index-Konstante, VERW, Station, Lage, Förderer; Antriebe mit
Motorschutz und Geber; Gegenprüfung beider Quellen. EA-Liste/WSCAD nicht
gelesen (Weg "nur EA-Liste" bleibt offen).
- `lib/merge_hw_layout.py` (`bin/merge_hw_layout.bat`): `<csv>_tro.json` +
`<projekt>_hw.json` → `<csv>_anlage.json` (`model.AnlageDocument`,
`sps_skel-anlage-v1`): Layout unverändert unter `layout`, daneben
`bindings[]` (Schlüssel TRO-ID + Slot + Rolle, mit Konstante und Adresse aus
dem TIA-Import, Status `zugeordnet`/`offen`/`widerspruch`/`ohne_slot`) und
`conveyors[]`. Handkorrekturen `cfg/hw_overrides.ini`, Zahlenwerte
`cfg/parameter.ini [merge_hw_layout]`.
- Damit `merge_hw_layout` kein CSV liest, trägt die TRO-JSON jetzt `parts` (Lage und
Hüllbox der Layout-Bauteile, von `tro_flow --json`; Schema bleibt v4, eine
Datei ohne `parts` liest sich weiter). Ein `PinStore_Auto` hat je Lagerlinie
einen Slot `line<n>`.
- **Förderernummer:** die E-Planung nennt Förderer im Klartext als `K0102` bzw.
`#106` (beides dieselbe Nummer, `CONVEYOR-#103` = `CONVEYOR-K0103`).
`merge_hw_layout` ordnet sie per Mehrheit der Feldgeräte einem Layout-Förderer zu
(Mubea: K0102 = Kreisel3, K0104 = Kreisel2, K0105 = Kreisel1). Das ist die in
"Staubereiche" gesuchte Quelle für `<NNNN>` und für `xInCarouselRun`.
- Gespeicherter Lagebezug mit SHA-256 der Positionsdatei; ein neuer Stand
gleichen Namens wird neu ermittelt.
- Noch offen: Weichen-/Ausschleus-Slots mit Layout-ID (sonst bleiben
Stausensoren und Ausschleusventile `ohne_slot`), Scanner (`BX`), Weg "nur
EA-Liste", Umstellung von `scl_skeleton` (Umbau 4).
### 4. `scl_skeleton.py` / `scl_context.py` (Stufe 4)
- Liest **eine** Eingabe: `<csv>_anlage.json`; ohne Hardwareteil wie heute
`<csv>_tro.json`.
- `--eplan` und die Zusammenführung in `scl_context._separator` entfallen; der
Kontext liest Namen aus `bindings`, offene Slots werden `TODO(E-Planung)`.
- **Prüfung gegen die globalen Variablen:** jeder Tag und jede `cIn…`-Konstante im
SCL muss im TIA-Import stehen, sonst Abbruch mit Liste.
- **Neue Bausteine** nach Stufe 2: `FB_CallSensors` aus der Sensorliste,
Antriebsaufrufe (`fbCarousel0102`) aus den Motorpaaren, Staubereichs-DBs.
### 5. Defaults als dritte Quelle
Zeiten und Projektkonstanten, die heute fest in `scl_skeleton.py` stehen
(`t#600ms`, `T#1500ms` …), wandern in den Typ-Katalog (`tro_catalog.py`) bzw. eine
Projektdatei `cfg/defaults.ini`.
## Bestehende Programme
| Programm | Rolle im neuen Ablauf |
|---|---|
| `material_flow.py`, `tro_flow.py` | bleiben Stufe 1; Ausbau um Slots und `jamAreas` |
| `tro_catalog.py`, `model.py`, `tro_overrides.py`, `dxf_registration.py` | bleiben Bibliotheken; `model.py` bekommt Slots, `jamAreas`, Anlagen-JSON |
| `e_planning.py` | wird aufgeteilt (unten) |
| `tro_annotate.py` | bleibt als Kontrollansicht, liest künftig die JSON (unten) |
| `tro_extract.py` | entfällt bzw. wird umgebaut (unten) |
| `scl_skeleton.py`, `scl_context.py` | Stufe 4, Umstellung auf `<csv>_anlage.json` |
### `e_planning.py` → `hw_import` + `merge_hw_layout`
| Teil heute | Wohin |
|---|---|
| `load_positionsdraw`, `EDevice`, `pair_drives` | `hw_import.py` |
| `resolve_transform`, `load_rules`, `_assign_spatial`, `_assign_by_number`, Bericht | `merge_hw_layout.py`, Ziele = Slots statt CSV-Bauteile |
| `cfg/e_planning.ini` | Regeldatei von `merge_hw_layout` |
| `EplanDocument` / `<csv>_eplan.json` | entfällt, ersetzt durch `bindings[]` |
Übergang: `e_planning.py` bleibt kurz als Hülle, die `hw_import` und `merge_hw_layout`
nacheinander aufruft, damit `bin/e_planning.bat` weiter funktioniert; danach
löschen.
### `tro_annotate.py` → Kontrollansicht für Layout und Zuordnung
- **Aus der JSON zeichnen** statt `analyse_tros` selbst aufzurufen: die Zeichnung
zeigt dann genau das, woraus das SCL entsteht.
- **Zuordnungen einzeichnen:** neben jedem Slot das BMK (`BG3306`, `MB3306`),
offene Slots und Widersprüche rot. Für die Hardwareplanung die schnellste
Kontrolle der räumlichen Zuordnung.
- `--emit-lisp` (`TROEDIT`) bleibt, solange Marker in BricsCAD bearbeitet werden;
hängt an der Entscheidung zu `tro_extract`.
### `tro_extract.py` → entfällt im neuen Ablauf
Es wäre ein dritter Korrekturweg neben `cfg/tro_overrides.ini` (Layout) und
`cfg/hw_overrides.ini` (Zuordnung) und schreibt noch das alte Schema
`sps_skel-tro-extract-v1`, das niemand mehr liest.
- **Löschen** (Empfehlung), zusammen mit `TROEDIT`/`--emit-lisp`. Korrekturen nur
in den INI-Dateien: nachvollziehbar und versioniert.
- **Umbauen**, falls die Projektierung in BricsCAD korrigieren will: es schreibt
dann Einträge in `tro_overrides.ini`/`hw_overrides.ini`, keine eigene JSON.
Bis zur Entscheidung bleibt es geparkt.
## Festlegungen (2026-10-11)
### TRO-IDs
**Festgelegt:** Die TRO-IDs vergeben die SPS-Programmierer, die zugleich die
Anwender der Skripte sind. Eine ID muss nur **eindeutig** sein. Üblich ist eine
Nummerierung entlang des Materialflusses und eine Benennung nach den Bauteilen
(Kreisel usw.), vorgeschrieben ist das nicht. Sobald größere Teile des SPS-Codes
stehen, ändern sich die IDs nicht mehr; in der frühen Projektphase dürfen sie
mehrmals neu vergeben werden.
**Befund:** Heute vergibt `tro_flow` laufende Nummern je Lauf (`TRO01`, `TRO02` …,
`tro_flow.py` Zeile ~1163); `cfg/tro_overrides.ini` warnt bereits, dass sich die
Nummern bei neuer Datenbasis verschieben. Die Layout-`TeileId` ist ebenfalls nicht
stabil: Vergleich zweier Exporte derselben Anlage
| Vergleich | Bauteile | gleiche Id und Lage | gleiche Lage, andere Id |
|---|---|---|---|
| `mubea.csv` ↔ `mubea_export.csv` | 119 / 125 | 46 | 27 |
| `HundM05.csv` ↔ `HundM05_14.csv` | 45 / 44 | 22 | 16 |
Stabil ist nur die **Lage** des Bauteils.
**Umgesetzt (2026-10-11, `lib/tro_ids.py`, Schalter in `tro_flow.py`;
Einzelheiten in `doc/Python_Scripts.md` §4 "TRO-IDs"):**
- Eine versionierte ID-Datei je Anlage, `cfg/<csv>_tro_ids.ini`, Abschnitt
`[tro]`: je TRO die ID und die Lage seines Anker-Separators (x, y in mm), der
Kommentar dahinter nennt Typ, Bezeichnung und Separatoren. Von `tro_flow`
angelegt, von den SPS-Programmierern umbenannt. Weitere Abschnitte (später
Separator- und Förderernummern) bleiben bei jedem Lauf erhalten. Ein anderer
Export derselben Anlage nutzt dieselbe Datei über `--ids`.
```ini
[tro]
TRO01 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
K0302_1 = 10310.0, -440.1 ; von Hand umbenannt
```
- **Standard (IDs halten):** `tro_flow` liest die ID-Datei und findet jeden TRO
über die Lage seiner Separatoren wieder (Toleranz `--id-tolerance`, Standard
100 mm, nächster Treffer zuerst, je TRO ein Eintrag). Gefundene TROs behalten
ihre ID, auch umbenannte. Neue TROs bekommen die nächste freie Nummer und
werden an `[tro]` angehängt; weggefallene stehen im Bericht (mit dem TRO, der
ihren Separator jetzt enthält), bleiben in der Datei, ihre ID wird nicht
wiederverwendet. `tro_annotate` liest dieselbe Datei, schreibt sie aber nicht.
- **Programmschalter `--renumber` (frühe Phase):** alle IDs neu entlang des
Materialflusses vergeben (Tiefensuche ab den Beladestationen, auf Kreisel und
Strecke in `TrackIds`-Reihenfolge) und `[tro]` neu schreiben. Ohne ID-Datei
geschieht dasselbe beim ersten Lauf.
- **Geprüft** mit `mubea.csv` → `mubea_export.csv` (andere TeileIds): 18 TROs
wiedergefunden, darunter eine umbenannte ID; `TRO14` als weggefallen gemeldet,
weil der Export die Separatoren 0005 und 0036 zu einem `2Sep2Swi` (`TRO13`)
zusammenfasst.
- **Noch offen:** die Abschnitte `[mubea.csv.tro.TRO03]` usw. in
`cfg/tro_overrides.ini` hängen an der alten Nummerierung und passen schon
heute nicht mehr (`TRO03` ist dort Sep 0010, im aktuellen Lauf Sep 0011).
Sie werden auf die IDs der ID-Datei umgestellt, sobald `tro_flow` die
Overrides wieder liest.
- Dadurch werden auch die Abschnitte `[<csv>.tro.<TRO-ID>]` in
`cfg/tro_overrides.ini` und die Zuordnungen von `merge_hw_layout` stabil.
### Staubereiche
**Bisheriges Namensschema** (Referenzen UH01–UH05, `doc/TRO_Katalog/connect.ini`,
286 Staubereiche):
```text
"DB_JamArea<NNNN>".stJam<NNNN>_<k> Kurzform JA<NNNN>_<k>
```
- `<NNNN>` = **Förderer**, auf dem der Staubereich liegt, ein DB je Förderer:
`0102` = Kreisel K0102 (vgl. `#fbCarousel0102`), `0103`/`0104` = Strecken,
`4103`/`4104` = Vario-Förderer.
- `<k>` = laufende Nummer **entlang des Materialflusses** auf diesem Förderer;
Kreisel K0102 hat `_1` … `_6` in Umlaufrichtung.
- 249 der 286 folgen diesem Schema. Sonderfälle: Lagerlinien `Storage1.1.2` (24),
Übergabe an eine andere Steuerung `IO_Device_UHxx_TO_UHyy…` bzw. lokal
`#arCarrierHandoverJam5000` (9), `JamBuffer101` im Leerträgerpuffer (2), F2H (1).
**Prüfung der Annahme "ein Staubereich = Ausgang des Vorgängers und Eingang des
Nachfolgers":**
- **Kein** Staubereich hat mehr als einen Nachfolger-TRO.
- **65** Staubereiche haben mehrere Vorgänger: dort laufen Ströme zusammen, z. B.
`JA0102_2` mit Vorgängern TRO101, TRO106 und TRO107 (Weichenausgang), alle mit
Nachfolger TRO112.
- Jeder TRO hat genau einen Eingangs-Staubereich; einzige Ausnahme sind die
PinStore-TROs (zusätzlich die Lagerlinie).
Ein Staubereich ist also immer **der Eingang genau eines TRO**, aber nicht immer
der Ausgang genau eines Vorgängers. Ein Name nach dem Schema `ja_tro3tro4` ist bei
Zusammenführungen nicht eindeutig.
**Vorschlag:**
**Ergänzung (2026-10-11):** In einem Mubea-Layout heißen die Staubereiche nach
**ihrem Separator**: der Staubereich vor `Sep1024` heißt `J1024`. Das passt zum
Befund oben: der Staubereich ist die Warteschlange vor dem Stopper, also der
Eingang genau eines TRO bzw. genau eines Separators. Das Layout liegt nicht im
Repo; Quelle der Separatornummer (Zeichnungsbeschriftung, E-Planung, Vergabe durch
die SPS-Programmierer) noch klären.
- **Kennung nach dem Separator:** `J<Separatornummer>`, z. B. `J1024`. Das ist
feiner als eine Kennung je TRO und deckt TROs mit mehreren Separatoren
(`2Sep1Swi` mit `JamEntr1`/`JamEntr2`) ohne Sonderregel ab. Zusammenführungen
sind kein Problem, weil der Name nur am Nachfolger hängt.
- **Separatornummern wie TRO-IDs behandeln:** in derselben ID-Datei, über die Lage
des Separators wiedergefunden, von den SPS-Programmierern vergeben bzw.
übernommen; `--renumber` vergibt sie in der frühen Phase neu.
- **Name im SCL — festgelegt: Standard sind die SCL-Dateien UH01–UH05,** also
`"DB_JamArea<NNNN>".stJam<NNNN>_<k>`. `J<Separatornummer>` ist nur die Kennung
in der JSON (und die Beschriftung im Layout), nicht der SCL-Name. Für den
SCL-Namen braucht es je Staubereich:
- `<NNNN>`: die **Förderernummer** des Förderers, auf dem der Staubereich liegt
(Kreisel K0102 → `0102`); vergeben von den SPS-Programmierern bzw. aus der
Hardwareplanung (`KR-K0102`), gepflegt in der ID-Datei wie TRO-IDs und
Separatornummern;
- `<k>`: laufende Nummer **entlang des Materialflusses** auf diesem Förderer,
aus dem Layout abgeleitet (Kreisel in Umlaufrichtung, beginnend an einer
festzulegenden Stelle, z. B. nach der Einschleusung).
Ohne Förderernummer bleibt der Name `TODO(E-Planung)` wie heute.
- Ausnahmen bleiben Sonderfälle mit eigenem Namen: Lagerlinien, Übergabe an eine
andere Steuerung, Leerträgerpuffer.
### Korrekturen in BricsCAD
**Zurückgestellt.** Eine Alternative wäre, die `<csv>_tro.json` zu visualisieren
und von Hand zu bearbeiten. Bis dahin bleibt `tro_extract.py` geparkt, `TROEDIT`
unverändert.
## Reihenfolge
1. **TRO-IDs stabil machen:** ID-Datei, Wiederfinden über die Lage,
`--renumber`. Darauf bauen Overrides, Staubereiche und Zuordnungen auf.
**Erledigt** (2026-10-11).
2. Slots und `jamAreas` in die TRO-JSON (Umbau 1), ein Eingangs-Staubereich je
TRO — ohne Slots hat die Zuordnung kein Ziel. **Erster Teil erledigt**
(2026-10-11, siehe Umbau 1 "Stand").
3. `hw_import` mit dem TIA-Import als erster Quelle. **Erledigt** (2026-10-15,
siehe unten "Stand Umbau 2/3").
4. `merge_hw_layout` mit der räumlichen Logik aus `e_planning.py`; `e_planning.py` als
Übergangshülle. **Erledigt bis auf die Hülle** (2026-10-15): die Logik liegt
in `merge_hw_layout.py`, `e_planning.py` importiert sie und bleibt vollständig,
solange `scl_skeleton --eplan` die `<csv>_eplan.json` liest (Schritt 5).
5. `scl_skeleton` auf `<csv>_anlage.json` umstellen, `--eplan` entfernen;
`tro_annotate` auf JSON umstellen.
6. Defaults (`cfg/defaults.ini`), `FB_CallSensors`, EA-Liste ohne Positionen.
Zu jedem Schritt: `bin/`-Wrapper für neue Skripte, Tests unter `tests/`
(kleine TIA-Datei als Testdaten), Golden Files bei Schemawechsel bewusst neu
erzeugen, `doc/Python_Scripts.md` ergänzen.
## Offene Entscheidungen
- **Staubereiche:** woher die Separatornummer (`Sep1024` → `J1024`) stammt; wo auf
einem Kreisel die Zählung `<k>` beginnt (Referenz K0102 prüfen). Das SCL-Schema
ist festgelegt (UH01–UH05).
- **Toleranz** beim Wiederfinden der TROs über die Lage: vorläufig 100 mm
(`--id-tolerance`). Ein weiter verschobener Separator wird als "neu" und
"weggefallen" gemeldet, der Programmierer übernimmt die ID von Hand in der
ID-Datei. Mit echten Layoutänderungen prüfen, ob der Wert passt.
- **Korrekturen in BricsCAD oder in der JSON:** zurückgestellt.
+513 -90
View File
@@ -1,13 +1,20 @@
# Python Scripts — CLI Tooling Overview
**As of:** 2026-09-17
**As of:** 2026-10-15
`lib/` currently holds nine Python modules (five of them runnable CLI tools,
four 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, an annotated copy of the BricsCAD drawing, a TRO-JSON read back
out of that drawing, and an `FB_Main` SCL skeleton generated from that JSON.
`lib/` currently holds sixteen Python modules (eight of them runnable CLI
tools, eight 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.
`hw_import.py` and `merge_hw_layout.py` are the new path for that bridge
(`doc/Konzept_Layout_Hardware_SCL.md`, stages 2 and 3): TIA import +
positions file → `<project>_hw.json`, merged with the TRO-JSON into
`<csv>_anlage.json` (§5d, §5e).
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
@@ -26,18 +33,36 @@ material_flow.py ──► <name>_material_flow.dot / .svg / .md
▼
tro_flow.py ──► <name>_tro_flow.dot / .svg, _tro_doc.md, _tro_errors.md
│ (derives TROs, using the type catalog in tro_catalog.py)
▼
tro_annotate.py ──► <name>_annotated.dxf, <name>_registration.json
│ (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)
▼ (drawing may be hand-edited in BricsCAD before the next step)
tro_extract.py ──► <drawing>_tro.json
│ (reads only the drawing; resolves plant coords via the registration)
▼
scl_skeleton.py ──► <source>_FB_Main.scl
(one REGION per TRO; E-planning values left as TODO(E-Planung) gaps)
│
├── --json ──► <name>_tro.json (model.TroDocument — MAIN PATH)
│ │
│ ▼
│ scl_skeleton.py ──► <name>_FB_Main.scl
│ (validates the JSON against model.py; one REGION per TRO;
│ E-planning values left as TODO(E-Planung) gaps)
│ ▲
│ │ --eplan <name>_eplan.json (optional)
│ e_planning.py ◄── <drawing>_positionsdraw.json (kabellaengen)
│ (assigns BG/MB of the E-planning drawing to the layout
│ parts by position + numbering rule; see §5c)
│
│ NEW PATH (stages 2/3, scl_skeleton does not read it yet):
│ <name>_tro.json ──┐
│ ├─► merge_hw_layout.py ──► <name>_anlage.json + .md (§5e)
│ hw_import.py ─────┘ (TRO slots <- BMK, conveyors K0102 -> Kreisel3)
│ ▲ *_TIA.xlsx + *_positionsconv.json (§5d)
│
└── same derivation ──► tro_annotate.py ──► <name>_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**
@@ -51,16 +76,20 @@ 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%\<name>_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` (type corrections, open points, TRO merges)
that survive re-runs of `tro_flow.py`. See §6a.
- **`lib/tro_geometry.py`** — resolves a separator's host and a switch's exit
count for `tro_flow.py` **directly from the part world-coordinates** (shapely
`STRtree`) instead of from the CSV `Zuordnung`/`Nachbarn` columns, for exports
where those columns have gaps. Only used with `tro_flow.py --geometry`. See §6b.
`%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):
@@ -81,7 +110,10 @@ Each CLI tool is invoked through its `bin/<name>.bat` (Windows) / `bin/<name>.sh
```bat
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\tro_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
```
@@ -93,14 +125,14 @@ 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` additionally persists the verified CSV↔DXF coordinate
offset to `%SKEL_CFG%\dxf_registration.json`.
- `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; `shapely>=2.1` for
`tro_flow.py --geometry` only) — run `bin\install_py.bat` once.
`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`
@@ -241,16 +273,18 @@ See `doc/track.md` for the fuller derivation notes.
| `--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 (`<name>_material_flow.md`) with object list, connections and plausibility findings. |
| `--connect` | — | off | Also write the topology as `<name>_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` | — | off | Also write `<name>_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. |
| `--show-bbs [flow\|all]` | `flow` | off | Also write `<name>_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 `<title>` 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 **one** to-scale "chain-/Lageplan" SVG of the **grobe Objekte** (big items) as `<name>_chain.svg`: every Kreisel split into its Bahn L/R halves (`_kreisel_lane_rects()`, same to-scale split as `--show-bbs`) and every Strecke/Gefällestrecke ("VF"/"GF") drawn as rectangles at their real `Position`/`Boundingbox` coordinates, connected by the already-resolved top-level `graph.edges` (material flow, Kreisel-Umlauf, unresolved). Step 1 of the new chain-graph build-up — no Separator/Scanner/Weiche/Ausschleus-/Einschleuselement and no BTMT Ein-/Ausgang yet; an edge touching a node that isn't drawn (e.g. a station) is simply skipped. Hand-built SVG, no Graphviz. |
| `--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`. Every node has a **mouseover text** (`tooltip`, shown on hover in the SVG): for a separator its `Zuordnung`, the container whose `TrackIds` list it (Kreisel/VF/GF) and the Kreisel lane it lies on by geometry (`lib/kreisel_junctions.py`); for an ILS Weiche the two Kreisel that created it (`Kreisel A`/`B`) and the touch point by geometry; for Beladung/Entladung the Kreisel they sit on. Contradictions between these sources get a line starting with `!` (e.g. `export.csv` 0015: `Zuordnung` Kreisel 0003, lies on Kreisel 0002). |
| `--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`).
`<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).
@@ -269,8 +303,27 @@ draws *their* flow. Derivation rules (see
| Gefällestrecke, single line | `1Sep` |
| Strecke (driven) | `Vario` (one per segment) |
| Kreisel lane | `1Sep` |
| Separator before an Ausschleuselement (one / two in a row) | `1Sep1Swi` / `1Sep2Swi` |
| Junction between Kreisel: *n* separators directly upstream of *m* touch points on one pulley | `1Sep1Swi`, `2Sep1Swi`, `2Sep2Swi`, `3Sep2Swi` |
| — additionally: 2 or 3 outgoing paths | upgraded to `1Sep1Swi` / `1Sep2Swi` |
**Junctions between Kreisel** (`lib/kreisel_junctions.py`, step 3c in
`derive_tros`). Every Kreisel is modelled as a stadium (two pulleys plus two
straight lanes, from position, `Drehung` and bounding box) with its direction of
travel (`Drehrichtung`). Wherever two Kreisel touch and run the same way, there
is a switch. An exported `ILS Weiche` is matched to it by position; a touch point
without one gets the ID `W<KreiselA>_<KreiselB>` and a finding (in
`export.csv` three are missing: 2/3, 3/4, 8/9). Touch points on the same pulley
form one junction (Kreisel 5 touches 4 and 6 with its west pulley → 2 switches).
A separator controls a junction if, walking **against** the direction of travel
from the junction on one of its Kreisel, it is the first thing reached — before
any other switch. The type follows from the count: separators × switches. The
host of each separator is the lane it physically lies on, not its `Zuordnung`.
If the two Kreisel run against each other at the touch point (both `UZS` at a
pulley-to-pulley touch, as in Mubea and Dematic — probably an export error), no
junction is formed, a finding is written, and the ILS Weiche is handled by the
older neighbour rule (`_find_weiche_separators`, step 3c').
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
@@ -282,21 +335,59 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here.
| `--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. |
| `--geometry` | — | off | Determine each separator's host and each switch's exit count (fanout) **from the part world-coordinates** via a shapely `STRtree` (`lib/tro_geometry.py`, see §6b) instead of from the CSV `Zuordnung`/`Nachbarn` columns. Use this for exports where those columns are incomplete (the columns are then bypassed entirely for these two inputs). Requires `shapely>=2.1`. |
| `--geo-tol-mm` | `MM` (float) | catalog default | Only with `--geometry`: the tolerance in mm within which two connection points count as the same transfer point. |
| `--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_errors.md` (only
if TROs are unconnected or of an unknown type).
`--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 the same way as `tro_flow.py`, and writes a **copy** of the drawing
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,
@@ -335,22 +426,31 @@ overridden.
| `--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. |
| `--layer-prefix` | `P` | `TRO_` | Prefix for the layers this tool creates. |
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. |
| `--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%`. |
| `--max-residual` | `MM` (float) | `600.0` | Largest residual error of the coordinate registration that is still accepted; above it the run fails (exit 4). |
| `--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 exceeds `--max-residual`) — resolve with `--offset` or
by raising `--max-residual`.
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`:
```
@@ -392,12 +492,18 @@ Last step of the chain:
```
material_flow.py objects -> material flow
tro_flow.py flow -> TROs
tro_annotate.py TROs -> markers in the drawing
tro_extract.py drawing -> JSON
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`.
@@ -412,7 +518,8 @@ is missed.
|---|---|
| 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 areas (`DB_JamArea…`) |
| `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) | |
@@ -424,23 +531,286 @@ 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`), `--out NAME` (default
`<source>_FB_Main.scl`), `--start TRO`. The last one matters while the plant has
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. Templates exist for the seven types
with a real reference call site (`1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`,
`Vario`, `Vario_workStation`, `PinStore_Auto`); each follows its call site in
`doc/TRO_Katalog/scl_templates/` line for line, and every value that is still
open carries `TODO(E-Planung)`. The line decisions are proposals to be confirmed
by a PLC engineer (`doc/SCL_Templates.md` §6). `2Sep1Swi`, `EmptyCarrBuffer`,
`LoadingBoom` and `2Sep2Swi` have no call site yet and stay generic. `--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`).
> **Since 2026-10-15** the E model (`EDevice`, `load_positionsdraw`,
> `pair_drives`) lives in `hw_import.py` and the assignment logic (rules,
> spatial, numbering rule, registration) in `merge_hw_layout.py`; `e_planning.py`
> imports and re-exports them and keeps only `EplanDocument`, the CSV path,
> report and CLI. Results are unchanged (Mubea: 43 names in the SCL). It stays
> until `scl_skeleton` reads `<csv>_anlage.json`. The stored registration now
> carries a SHA-256 of the positions file: a new file with the same name is
> registered anew instead of silently reusing the old offset. `--eplan` (and
> every `--file`-style input) also resolves a relative sub-folder against
> `%SKEL_DATA%` (`Mubea/Mubea_positionsdraw.json`).
## 5d. `hw_import.py` — read the hardware planning (TIA import + positions file)
Stage 2 of [`Konzept_Layout_Hardware_SCL.md`](Konzept_Layout_Hardware_SCL.md).
Two sources are enough (finding in [`E-Planung_Bruecke.md`](E-Planung_Bruecke.md),
"Nachprüfung 2026-10-15"); the EA list and the WSCAD import add nothing once a
positions file exists.
```
*_TIA.xlsx (PLC Tags + Constants) ─┐
├─► hw_import.py ──► <project>_hw.json (HardwareDocument, sps_skel-hw-v1)
*_positionsconv.json (optional) ────┘ ──► <project>_hw.md (report: devices, drives, checks)
```
- **TIA import**: sheets are recognised by their header row, not their name
(English and German TIA exports): a sheet with `Logical Address` /
`Logische Adresse` holds tags, one with `Value` / `Wert` constants. Read with
`zipfile` + XML — no openpyxl dependency.
- **Per BMK one `HwDevice`** (an `EDevice` plus): `direction` (`in`/`out` from
`%E`/`%I` vs. `%A`/`%Q`), `address`, `dataType`, `comment`, the index constant
`constant` (`cInBG0532` / `cOutMB0533` — what the SCL references) with its
`constantValue`, `conveyor` + `conveyorKind`, and `sources` (`tia`,
`positions`).
- **Conveyor from the plain text**: `KREISEL-K0102`, `KR-K0104` → `K0102`,
`K0104` (kind `Kreisel`); `CONVEYOR-#103` and `CONVEYOR-K0103` are the same
conveyor and both become `K0103` (kind `Strecke`) — the four-digit conveyor
number of the SCL naming scheme (`DB_JamArea0103`, `fbCarousel0102`).
- **Drives**: motor `MA` ↔ motor protection `FC` (same number), conveyor and
kind from their texts, encoder `GF…` of the same conveyor.
- **Cross-check** (`checks`, also printed as `?` lines): BMK only in the
positions file / field device (`BG`/`MB`/`MA`/`BX`) only in TIA, TIA comment ≠
`BEZEICHNUNG`, signal without index constant, index constant without signal,
duplicate addresses, duplicate constant values, field device without
controller in `KENNZEICHNUNG`. The positions file's own `warnings` are passed
through.
Switches: `--tia NAME` (required), `--positions NAME` (optional; without it no
positions), `--project NAME` (default from the positions file name:
`Mubea_positionsconv.json` → `Mubea`). Names resolve against `%SKEL_DATA%`,
also with a sub-folder. Exit `0` (findings are in the report), `1` on input or
read errors.
Result on Mubea (`mubea-1_TIA.xlsx` + `Mubea_positionsconv.json`): 238 devices,
237 in both sources, 237 index constants, 158 field devices with position,
11 drives all with motor protection and conveyor (encoder `GF20000` at K0102);
checks: `QA7000` only in the positions file, `BG0514` without controller.
Tests: `tests/test_hw_import.py` (xlsx reader with shared/inline strings,
header-based sheet detection, conveyor texts, merge per BMK, drives, every
cross-check, TIA-only run, JSON round trip, CLI with a sub-folder in
`%SKEL_DATA%`). The test builds its own small TIA xlsx.
## 5e. `merge_hw_layout.py` — merge layout and hardware planning into the plant JSON
Stage 3 of the concept. Reads **no CSV**: the TRO-JSON carries the TRO slots
and, since 2026-10-15, the positions and bounding boxes of the layout parts
(`parts`, written by `tro_flow --json`).
```
<name>_tro.json + <project>_hw.json
└──────┬──────┘
merge_hw_layout.py rules %SKEL_CFG%\e_planning.ini, corrections %SKEL_CFG%\hw_overrides.ini
│
├─► <name>_anlage.json (AnlageDocument, sps_skel-anlage-v1:
│ layout unchanged + bindings[] + conveyors[])
└─► <name>_anlage.md (report)
```
Steps:
1. **Registration** E drawing → layout as in §5c (`IN SEP` sensors against the
layout separators), stored with the SHA-256 of the positions file. Warning
when fewer than `registration_min_share` of the separators find a partner —
a sign that layout and E drawing are different planning states.
2. **Rules** from `e_planning.ini`, unchanged logic (spatial with maximum /
target distance, numbering rule `MB = BG + 1`).
3. **Slots**: each assignment goes to the slot role of the TRO that holds the
part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per
storage line (`line1` … `lineN`, same split as `scl_context._store`);
`separator1` and `line1` can be the same part and both get the binding.
Parts without a slot (outfeed elements, switches — `tro_flow` does not keep
them on the TRO yet) stay visible as `ohne_slot`.
4. **Conveyors**: the E-planning names (`K0102`) and the layout names
(`Kreisel3`) do not match, so every field device whose text names a conveyor
votes for the nearest layout conveyor (distance to its bounding box, at most
`conveyor_max_mm`). Only conveyors of the named kind vote — the bounding
boxes overlap (storage lines reach the Kreisel). Assigned at
`conveyor_min_votes` votes and `conveyor_min_share` share; one E conveyor per
layout conveyor. This gives motor, motor protection and encoder per layout
conveyor (`AnlageDocument.conveyor_for("0010-R")` → the TROs on that
Kreisel, i.e. `xInCarouselRun`) and fills `drive.motor` /
`drive.motorProtection` of Vario TROs.
5. **Corrections** from `hw_overrides.ini` (last):
`[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and
`[<csv>.conveyors] K0102 = 0010`.
6. **Status** per binding: `zugeordnet`; `offen` (required role without BMK →
later `TODO(E-Planung)`; optional `?` roles are not listed); `widerspruch`
(BMK not in the TIA import, unknown, or given to two parts); `ohne_slot`.
Every binding carries `constant` and `address` from the TIA import.
Switches: `--tro NAME`, `--hw NAME` (both required, resolved against
`%SKEL_RESULTS%`), `--rules FILE`, `--overrides FILE`, `--re-register`. Exit
`0` also when not everything was assigned; `1` on input errors, an old
TRO-JSON without `parts`, or an unprovable registration. Parameters:
`cfg/parameter.ini [merge_hw_layout]`.
Result on Mubea (`mubea.csv` → `mubea_tro.json`, `Mubea_hw.json`): registration
dx −151.4 / dy −696.6 mm (23/38, warning); 45 slot roles assigned (all 20
storage-line sensors, 19 stoppers, separator sensor/stopper at TRO03/04/05/18),
58 open, 0 contradictions; 41 assignments at outfeed elements/switch without a
slot; conveyors **K0102 = Kreisel3 (21/23 votes), K0104 = Kreisel2 (8/8),
K0105 = Kreisel1 (2/3)**, each with motor and motor protection. The other
conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or
have a single vote and stay open.
**Not yet:** `scl_skeleton` still reads `<csv>_eplan.json` (`--eplan`);
switching it to `<csv>_anlage.json` is stage 4 of the concept, and only then
`e_planning.py` becomes the thin wrapper and is removed.
Tests: `tests/test_merge_hw_layout.py` (sensor spatial / stopper by number,
contradiction, open roles, part without slot, conveyor majority with
overlapping boxes, Vario drive, too few votes, corrections incl. moving a BMK
and clearing a conveyor, storage-line slots, missing `parts`, JSON round trip,
fingerprint of the stored registration, CLI end to end with a real offset).
## 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
CAD marker shape. **Twelve** 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
> **`2Sep2Swi` and `3Sep2Swi` are the exceptions — they are 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
@@ -448,14 +818,71 @@ verified against `data/*.scl` where the source is vendored locally, and against
> 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.
> `3Sep2Swi` (3 separators, 3 scanners, 2 switches) was added on 2026-10-14
> for the test plant `data/export.csv`, where one Kreisel pulley touches two
> others; **`FB_ILS_MTRO_3Sep2Swi` does not exist either.**
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 `tro_flow.py` and `tro_annotate.py`; not run directly.
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`, `line<n>` for the storage lines of a store — 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`, `parts`, `findings`. `from_analysis()` (derives slots and jam areas, takes `parts`), `to_json()`, `from_json()`, `jam_areas_or_derived()` (a v3 file without `jamAreas` still renders), `part()` (layout part by TeileId, lane suffix ignored). |
| `LayoutPart` | A layout part with position (`layoutId`, `kind`, `name`, `position`, `bbox`) — the target of the spatial assignment in `merge_hw_layout.py`, so that it never reads the CSV. `distance_to_box()`. Optional in v4; a file without `parts` still reads. |
| `Binding` | One assignment hardware → slot role: `tro`, `slot`, `role`, `layoutId`, `bmk`, `constant`, `address`, `source`, `rule` (`raeumlich`/`nummer`/`mehrheit`/`hand`), `distance`, `runnerUp`, `status` (`zugeordnet`/`offen`/`widerspruch`/`ohne_slot`), `note`. |
| `ConveyorBinding` | A conveyor of the E-planning (`K0102`) at a layout conveyor: `eplanName`, `layoutId`, `layoutName`, `votes`/`total`, `motor`, `protection`, `encoder`, `rule`, `status`, `note`. |
| `AnlageDocument` | The plant JSON `<csv>_anlage.json` (`sps_skel-anlage-v1`): `layout` (the TRO-JSON unchanged), `registration`, `bindings`, `conveyors`, `unassigned`, `warnings`. `binding(tro, slot, role)`, `conveyor_for(layout_id)`. Written by `merge_hw_layout.py`; the future single input of `scl_skeleton`. |
`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
@@ -525,47 +952,18 @@ Three kinds of entry, deliberately different in weight:
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; the point is only marked, in the doc
(section 6) and in the annotated drawing on layer `TRO_OPENPOINT` as a red
crossed circle with plain-text label. This keeps "derived" and "asserted"
distinguishable until the spot has been checked in BricsCAD.
TRO. The structure stays untouched. Read as `model.TroCandidate` (`expect` →
`type_name`, `x`/`y`/`z` → `position`, missing `z` = 0).
Consumers: `tro_flow.py` (type corrections + doc section 6), `tro_annotate.py`
(corrected marker types + open-point markers), `material_flow.py` (writes the
`[<plant>.manual]` section into `connect.ini`). Unknown TRO IDs and types are
not applied silently — they are reported as warnings in the console output and
in doc section 6.3, which catches the common trap: **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.
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`.
## 6b. `tro_geometry.py` — geometric separator-host and switch-fanout (library, no CLI)
`tro_flow.py` normally derives two of its inputs from the CSV columns
(`material_flow.py::build_graph`):
```
Zuordnung (host of the separator) -> which conveyor/Kreisel a separator sits on
Nachbarn / graph edges (fanout) -> how many exits a switch has (1Sep / 1Sep1Swi / 1Sep2Swi)
```
For some exports those columns are incomplete — e.g. `hundmM05_1.csv` reports
"nur ein Partner" for several `Strecke` rows, and the older `hundm_export.csv`
failed outright (5 TROs, all `Vario`, 0 connections, all isolated) because
`Zuordnung`/`Nachbarn` could not be resolved. This module derives the same two
values **directly from the part world-coordinates** instead, so the TRO
derivation no longer depends on the quality of those two columns:
- a shapely `STRtree` over the AS/ES chain ends (K1/K2) of every
Strecke/Gefällestrecke plus the two Kreisel lane-ends;
- `resolve_host_geometric` — `.nearest()` to find the closest host to a
separator;
- `count_geometric_exits` — `.query(..., predicate="dwithin")` to find every
other chain that actually meets at a given point (within a tolerance, mm).
It is activated by `tro_flow.py --geometry` (via `make_geometric_resolvers`);
`--geo-tol-mm` sets the meeting tolerance. This module knows nothing about
`tro_flow.py` — only `material_flow.py::Element` lists. Requires `shapely>=2.1`
(pinned in `requirements.txt`, needed only for this path).
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)
@@ -598,6 +996,31 @@ 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.
## 7a. `parameter.py` — numeric settings from `cfg/parameter.ini` (library, no CLI)
Tolerances, thresholds, scales and drawing sizes of the libraries are not
written in the code but read from **`cfg/parameter.ini`**: one section per
module, the key is the constant's name in lower case
(`[kreisel_junctions] contact_tolerance` → `kreisel_junctions.CONTACT_TOLERANCE`).
Meaning and unit of every value are documented as comments in the INI file.
- The base file in the repository is read first and must be complete — a
missing key or a non-number stops the import with `ParameterError`; there are
deliberately no fallback values in the code.
- A second `parameter.ini` in `%SKEL_CFG%` (when it points somewhere else)
overrides single values, e.g. a different contact tolerance for one plant.
- Read by `kreisel_junctions`, `tro_flow`, `tro_ids`, `material_flow`,
`dxf_registration`, `tro_annotate` and `e_planning` (fallback for
`toleranz_mm` in `e_planning.ini`). `tests/test_parameter.py` checks that
every key is read by its module.
- Not in the INI, on purpose: format constants (`KX_TRANS_FAKTOR`, the coding
of the CSV columns K1–K4), mathematical epsilons, and style tables
(colours, fonts, `COORD_SYMBOL_SIZE`, symbol proportions in the drawing
code).
Changing a value that drives the derivation (a tolerance or threshold) can
change TRO types and IDs — compare the outputs of all plants afterwards.
## 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
+528
View File
@@ -0,0 +1,528 @@
# SCL Generation from Parameterised Templates — Concept
**Status:** steps 1–3 and 5 of §11 done — templates for all 7 types with a reference call
site (`1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`, `Vario`, `Vario_workStation`,
`PinStore_Auto`) usable with `--templates`, default output unchanged; the line decisions
(§6) are proposals still to be confirmed by a PLC engineer · **As of:** 2026-10-12
This is the plan for moving `scl_skeleton.py` from hard-coded SCL (Python f-strings in
the `emit_*` functions) to **parameterised templates**: the SCL structure for each FB
lives in a template file, and Python only supplies the data.
It is written so that it fits the existing main path unchanged:
```
export.csv ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
(dxfmakros (derive) (TroDocument, (render) (skeleton with
EXPORTCSV) sps_skel-tro-v4) TODO(E-Planung))
```
Nothing upstream of `scl_skeleton` changes. The TRO-JSON stays the only input, and the
CLI, output file name, `TODO(E-Planung)` marker and exit codes stay compatible. A
workflow that calls the chain today keeps working, and gets the new behaviour only by
passing the new optional switches.
---
## 1. Why
Today the knowledge about each FB call site is copied into Python by hand. That has
two problems:
1. **It drifts.** Compared with the real call site of `FB_ILS_MTRO_1Sep1Swi` (TRO107),
the generated code already differs: the priority-manager call parameters
(`stInPriorityManagerDir2`/`stOutPriorityManagerDir2`, `stInSettings.xPrioDir2`),
`xInCarouselRun1` and the jam sensors `stInSenJam2/3` are missing; the separator times
differ (600/200 ms instead of 100/50 ms); the parameter order differs.
2. **It is in the wrong place for the people who own it.** A PLC engineer can correct
an SCL file; they will not edit Python f-strings.
With templates, the SCL structure is SCL again, the reference call sites become testable,
and the project-specific names can be plugged in through a config (§7).
---
## 2. Reference material that exists today
`doc/TRO_Katalog/scl_templates/` holds two **different kinds** of files. Both stay
**read-only** (see `CLAUDE.md`); they are the reference, not the templates we render.
| Kind | What it is | Use in this concept |
|---|---|---|
| **Call site** (43–138 lines) | A real `REGION` from an `FB_Main`: parameter lines plus the instance call, with real values (`TRO107`, `cSep107.1`, `MB3306`) | Source for the parameterised template **and** the expected output of its round-trip test (§8.1) |
| **FB source** (800–1550 lines) | The full `FUNCTION_BLOCK` with `VAR_INPUT`/`VAR_OUTPUT`/`VAR_IN_OUT` | The FB **interface**; every parameter a template writes must exist there (§8.2) |
Coverage per catalog type (`lib/tro_catalog.py`):
| Catalog type | FB | Call site | FB source | Template possible |
|---|---|---|---|---|
| `1Sep` | `FB_ILS_MTRO_1Sep` | yes (TRO 100) | — | ✅ done |
| `1Sep1Swi` | `FB_ILS_MTRO_1Sep1Swi` | yes (TRO 107) | — | ✅ done (first pilot) |
| `1Sep2Swi` | `FB_ILS_MTRO_1Sep2Swi` | yes (TRO 267) | — | ✅ done |
| `1Sep_SSCC` | `FB_ILS_MTRO_1Sep_SSCC` | yes (TRO 105) | — | ✅ done |
| `Vario` | `FB_ILS_MTRO_Vario` | yes (TRO 115) | — | ✅ done |
| `PinStore_Auto` | `FB_ILS_MTRO_PinStore_Auto` | yes (TRO 208) | — | ✅ done (outlier, largest) |
| `Vario_workStation` | `FB_ILS_MTRO_Vario_workStation` | yes (TRO 421, `_CallSite.scl`) | yes | ✅ done (interface check §8.2 still open) |
| `2Sep1Swi` | `FB_ILS_MTRO_2Sep1Swi` | **no** | yes | from interface only — needs a real call site |
| `EmptyCarrBuffer` | `FB_EmptyCarrBuffer` | **no** | yes | from interface only — needs a real call site |
| `LoadingBoom` | `FB_LoadingBoom_INBOUND` | **no** | yes | from interface only — needs a real call site |
| `2Sep2Swi` | `FB_ILS_MTRO_2Sep2Swi` | **no** | **no** | no — FB does not exist yet (Mubea needs it) |
So 7 types can be done with a test against real code. 3 can only be checked against the
interface until someone supplies a real call site. `2Sep2Swi` stays on the fallback (§9).
---
## 3. Design principles
1. **Templates hold structure, Python holds data.** No SCL text in Python except the
file header and the fallback.
2. **Keep the item-based composition.** `doc/HundM/suggestion.md` shows that all
`FB_ILS_MTRO_*` share one skeleton and differ only in how many separators, switches,
directions and jam areas they repeat. The templates mirror that: shared macros for the
repeated blocks, one thin template per FB that arranges them in that FB's order.
3. **Never invent a value.** Anything without a source stays `TODO(E-Planung)`, as it
does today. A plausible-looking wrong address is worse than an obvious gap.
4. **Every value has exactly one origin**, visible in the template (§6): layout, naming
rule, reference default, E-Planung, or condition.
5. **The TRO-JSON is the contract.** The render context is built only from
`model.TroDocument`; templates never read the CSV, the DXF or the catalog directly.
6. **Fail loudly in templates.** An undefined variable is an error (Jinja2
`StrictUndefined`), not an empty string in the SCL.
7. **Additive migration.** The existing `emit_*` code stays as the fallback until every
type has a template; with no new switch the output is byte-identical to today.
---
## 4. Files and folders
```
sps_skel/
├── doc/TRO_Katalog/scl_templates/*.scl reference, read-only (unchanged)
├── lib/
│ ├── scl_skeleton.py CLI + order + context + render (changed)
│ ├── scl_context.py NEW: TroDocument -> render context (pydantic)
│ ├── scl_naming.py NEW: reads the naming config, resolves names
│ └── scl_templates/ NEW: the parameterised templates
│ ├── _macros.scl.j2 shared blocks (separator, switch, jam, call, direction)
│ ├── FB_ILS_MTRO_1Sep.scl.j2 ✅
│ ├── FB_ILS_MTRO_1Sep1Swi.scl.j2 pilot ✅
│ ├── FB_ILS_MTRO_1Sep2Swi.scl.j2 ✅
│ ├── FB_ILS_MTRO_1Sep_SSCC.scl.j2 ✅
│ ├── FB_ILS_MTRO_Vario.scl.j2 ✅
│ ├── FB_ILS_MTRO_Vario_workStation.scl.j2 ✅
│ └── FB_ILS_MTRO_PinStore_Auto.scl.j2 ✅ (one per FB, named exactly like fbType)
├── cfg/
│ ├── scl_naming.ini NEW: default naming rules (from the reference plant)
│ └── <project>_scl_naming.ini NEW, optional: per-project override
└── tests/
├── test_scl_templates.py round trip against the reference call sites ✅
├── test_scl_skeleton_golden.py whole-file regression on mubea/HundM05 ✅
├── test_scl_context.py the derivations one by one ✅
└── fixtures/scl/
├── <name>_tro.json fixed inputs (mubea, HundM05)
├── <name>_FB_Main.scl expected output, generic
└── <name>_FB_Main_templates.scl expected output, with --templates
```
The file frame (header, `VAR` list, REGION loop) stays in Python (`render_context`) for
now; only the REGIONs come from templates. A `_file.scl.j2` is only worth it once every
REGION does. The reference values of a round trip are set in the test itself
(`tests/test_scl_templates.py`) instead of a JSON fixture, so new context fields need no
fixture update.
**Lookup:** the template file name is the TRO's `fbType` from the catalog
(`FB_ILS_MTRO_1Sep1Swi` → `FB_ILS_MTRO_1Sep1Swi.scl.j2`). No second mapping table, so the
catalog stays the only place that links a type to its FB.
**Dependency:** `jinja2 >= 3.1` is added to `requirements.txt`.
---
## 5. Template structure
### 5.1 Jinja2 settings
| Setting | Value | Reason |
|---|---|---|
| `undefined` | `StrictUndefined` | A misspelled variable stops the run instead of writing empty SCL |
| `trim_blocks`, `lstrip_blocks` | `True` | `{% for %}` lines leave no blank lines/indent in the SCL |
| `keep_trailing_newline` | `True` | Stable files, clean diffs |
| `autoescape` | `False` | SCL, not HTML |
SCL syntax does not clash with Jinja2: SCL uses `#name`, `{ S7_... }` and `//`, never
`{{`, `{%` or `{#`. The one thing to avoid is writing `{#` directly (e.g. `{#TRO…`).
### 5.2 Shared macros (`_macros.scl.j2`)
One macro per repeated block. Each takes the TRO context and the block's own data:
```jinja
{% macro separator(t, s) %}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no }}";{{ s.no_note }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := {{ d.sep_trailing }}; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := {{ d.sep_handling }}; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ 'TRUE' if t.has_scanner else 'FALSE' }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor }}"];{{ s.sensor_note }}
{% endmacro %}
{% macro jam(kind, j) %}
arInOutJam{{ kind }}{{ j.n }} := {{ j.db }}.arCarrier, // {{ j.note }}
stInOutJam{{ kind }}{{ j.n }} := {{ j.db }}.stData,
{% endmacro %}
```
`s.no` is either a resolved name (`cSep107.1`) or the placeholder `cSep_____`, and
`s.no_note` is then ` // TODO(E-Planung)` or empty. Python decides both (§7.3); the
template only places them.
### 5.3 One template per FB
Each FB template is the reference call site with values replaced by variables and
repeated parts replaced by loops. Sketch for `FB_ILS_MTRO_1Sep1Swi.scl.j2`:
```jinja
{% import "_macros.scl.j2" as m %}
REGION {{ t.region_title }}
{{ m.comments(t) }}
{% for s in t.separators %}{{ m.separator(t, s) }}{% endfor %}
{% for w in t.switches %}{{ m.switch(t, w) }}{% endfor %}
#{{ t.id }}.stInSettings.xPrioDir2 := {{ t.prio_dir2 }};{{ t.prio_dir2_note }}
#{{ t.id }}(stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.udt_{{ t.id }}.TroIn,
stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.udt_{{ t.id }}.TroOut,
nInMainTroNo := "{{ t.main_tro }}",{{ t.main_tro_note }}
xInSftyOk := {{ t.safety }},{{ t.safety_note }}
xInAllRdyToStart := TRUE,
{% if t.carousel_run %}
xInCarouselRun1 := {{ t.carousel_run }},
{% endif %}
xInRelease := {{ t.release }},
...
{% for j in t.jam_entries %}{{ m.jam("Entr", j) }}{% endfor %}
{% for j in t.jam_exits %}{{ m.jam("Exit", j) }}{% endfor %}
stInOutHMI := "DB_Interface_HMI".stTRO.{{ t.id }},
nOutStateLast => "DB_GLOB_TroState".arTroState["{{ t.main_tro }}"],
{% for s in t.separators %}
xOutStopper{{ s.n }} => "{{ s.stopper }}"{{ "," if not loop.last or t.switches else ");" }}{{ s.stopper_note }}
{% endfor %}
...);
{{ m.direction(t) }}
END_REGION
```
The order of the lines in the template **is** the parameter order of the reference call
site. That replaces the order that is hard-coded in `emit_call` today.
### 5.4 File frame and whitespace
The file header (source, timestamp, start reason, open-TODO note), `FUNCTION_BLOCK
"FB_Main"`, the `VAR` list of all instances and the REGION loop are written by
`render_context` in Python. Each REGION is rendered from the TRO's own template and
inserted there; TROs without a template get the generic text (§9). With `--templates`
the header names which FB types came from a template and which did not.
Templates do **not** use Jinja whitespace control (`{%- … -%}`) except inside the
one-line macros `todo`/`note`. Instead the renderer drops trailing blanks and collapses
runs of blank lines (`SclTemplates.render`). That keeps the templates readable as SCL and
the indentation intact.
Rule for every template: **each line that contains a placeholder carries the
`TODO(E-Planung)` marker** — also the second line of a pair such as
`stInOutJamExit<n> … .stData`. `tests/test_scl_templates.py` checks this.
---
## 6. Where each value comes from
Every line in a template belongs to exactly one of these classes. The class decides who
supplies the value and what happens when it is missing.
| Class | Source | Example | If missing |
|---|---|---|---|
| **L — layout** | TRO-JSON (`TroDocument`) | `#TRO107`, number of separators/switches, jam entries/exits per neighbour, destination list per switch exit, `nTo1Destinations` (single switch) | cannot be missing — the JSON is validated |
| **N — naming rule** | naming config (§7) | `cSep107.1`, `cSwi107.1`, `cMainTro107`, `udt_TRO107` | `TODO(E-Planung)` placeholder |
| **R — reference default** | the reference call site, written in the template or in `[defaults]` | `tTrailingTime t#100ms`, `tJam "TimeForSepJam"`, `xPluggedJam TRUE` | always present; marked `pruefen` where plant-specific |
| **E — E-Planung** | I/O list (not available yet) | `cInBG3306`, `MB3306`, `MA…`, safety zone, `DB_JamArea0102.stJam0102_1` | `TODO(E-Planung)` placeholder |
| **C — condition** | topology flag in the context | `xInCarouselRun1` only next to a carousel; `stInSenJam2/3` only with extra jam sensors | line left out, or written as a TODO |
Class **C** is the real design work, not the conversion. Each reference call site is one
concrete location and contains lines that only exist because of that location. They have
to be decided once per template, together with a PLC engineer, and recorded in the
template as a comment. For the pilot `1Sep1Swi` (TRO107):
| Line in reference | Implemented in the pilot | Context field | Status |
|---|---|---|---|
| `stInSenJam2`, `stInSenJam3` | one per jam exit, sensor `TODO(E-Planung)` | `jam_exits[].sensor` | to confirm |
| `stInSettings.xPrioDir2 := TRUE` | written as `TRUE`, marked `TODO(E-Planung)` (priority is a planning decision) | `prio_dir2` | to confirm |
| `stIn/OutPriorityManagerDir2` | always; UDT `udt_____` + TODO until the naming rule `udt_{id}` (step 4) | `priority_udt` | to confirm |
| `xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed` | **always written, marked TODO "nur am Kreisel, sonst Zeile loeschen"**. The proposed condition (a connection of kind `umlauf`) does not work: `tro_flow` emits `bahn`/`normal`/`weiche`/`transfer` in practice, `umlauf` never occurs in mubea or HundM05, so the TRO-JSON cannot tell whether a TRO sits at a carousel. Needs either a carousel flag from `tro_flow` or a manual decision. | `carousel_run` | to confirm |
| `xInRelease := TRUE` (generic: `"DB_TRO_TEST".<id>`) | reference value `TRUE` | — (literal) | to confirm |
| `stInOutMachineState := #stInOutControlUnitCabinetOF1` | placeholder `#stInOutControlUnitCabinet` + TODO (cabinet is plant-specific) | `machine_state` | to confirm |
| separator times `t#100ms` / `t#50ms` | reference values, marked `pruefen`; `tSenFree`/`tSenWait` of the generic path are **not** written (not in the reference) | — (literal) | to confirm |
| `nTo1Destinations := 3` (TRO107 has **2** exits) | number of exits, marked `pruefen` — the reference suggests it counts destinations, not exits (§13) | `switches[].to1_destinations` | open |
| `stInPriorityManager.xReleaseOk` (generic only) | not written — not in the reference | — | to confirm |
The same decisions were made for the other six templates (step 5). Each template's
header comment lists them in full; this is the summary of what deviates from "copy the
reference line":
| Template (reference) | Decisions to confirm |
|---|---|
| `1Sep` (TRO100) | times 600/200 ms and `tSenFree`/`tSenWait` 1500/4000 ms as in the reference (`pruefen`); `xInCarouselRun` always written as TODO; `xInRelease := "DB_TRO_TEST".<id>` as in the reference |
| `Vario` (TRO115) | times 800/300 ms, `xPluggedJam FALSE`, `tSenFree`/`tSenWait` 2000/5000 ms, `tRasterHigh/Low` 5 s (`pruefen`); `stInSettings.xIsBorner := TRUE` marked TODO (meaning unclear); `JamEntr` = the TRO's own entry jam area (TRO-JSON v4: several predecessors merge into it), `JamFinger` = the entry jam area of the first successor; `xInErrorQuit`/`xInAutomatic` open |
| `Vario_workStation` (TRO421) | `xPluggedScanner FALSE` although the catalog lists a scanner (it sits at the work station, not the separator — `pruefen`); work-station sensor, release and `xBeladenError` sensor open |
| `1Sep2Swi` (TRO267) | separator without times, `xPluggedPart TRUE` + `stSenPart` as in the reference; **switch 2 has no `nSwitchNo` in the reference** — written as a commented-out proposal; `nTo1Destinations` per switch open (split of the exits); `bSepType.%X2 := TRUE` marked `pruefen`; one `xInCarouselRun<n>` per switch; **no `FC_Direction`** at this place in the reference — only the destination list as a comment; no `nOutStateLast` (not in the reference) |
| `1Sep_SSCC` (TRO105) | second (SSCC) camera with its own interface structure, end-measurement sensor and HMI key open; `xInRelease` written as `"DB_TRO_TEST".<id>` with a TODO (the reference releases via the WCS connection); `FC_CARR_GLOB_CORRECTION` with `nInStation` open; the WCS telegram region of the reference is only noted as TODO |
| `PinStore_Auto` (TRO208) | **number of lines from the layout** (one per storage lane, `nNoOfLines`), not the 19 of the reference; all plant constants (encoder, destinations, stations, thresholds, storage DB, removal handshake) open; pin separator times 0/50 ms (`pruefen`); simulation settings `stSimu` of the reference not written; the FB has one `JamEntr`/`JamExit` — since v4 several feeds merge into the one entry jam area (mubea: 2 feeds → 1 entry); a store with more than one entry or exit jam area (mubea: 2 exits) gets a TODO in the REGION header |
New context fields for these templates (`lib/scl_context.py`): Cognex wiring per scanner
(`interface`, `tag`, `hw_results`), `part_sensor` per separator, `carriers`/`data` per jam
area (handover jams use their own variables), `jam_in`/`jam_out` (same objects as the
first entry/exit), `error_quit`, `automatic`, `is_border`, `correction_station`, and the
blocks `sscc`, `work_station`, `store` (with one `SclStoreLine` per lane). The generic
path does not use them — its golden files are unchanged.
Effect on the open-value count with `--templates`: with the pilot only, mubea 282 → 302
and HundM05 323 → 377; with all seven templates mubea → 489 and HundM05 → 475
`TODO(E-Planung)` (HundM05 now comes completely from templates, mubea except the
`2Sep2Swi`). Since TRO-JSON v4 (one entry jam area per TRO instead of one per
predecessor) it is: mubea 277 generic → 482 with templates, HundM05 316 → 461. More reference lines are written and every open value is marked; the count
drops again with the naming rules (step 4).
---
## 7. Naming config
### 7.1 Purpose
Some names follow a rule in a given plant (`cSep107.1` = separator 1 of TRO 107). Others
do not (`cInBG3306` is whatever E-Planung assigned). The naming config holds the rules,
so that:
- rule-based names are generated instead of left as TODOs,
- a project with a different naming scheme changes a file, not code,
- anything without a rule stays `TODO(E-Planung)`, exactly as today.
### 7.2 File and lookup
INI, like `connect.ini` and the other files in `%SKEL_CFG%`. Lookup order (later wins):
1. `%SKEL_CFG%/scl_naming.ini` — defaults taken from the reference plant
2. `%SKEL_CFG%/<name>_scl_naming.ini` — per project, `<name>` = stem of the TRO-JSON
(`mubea_tro.json` → `mubea_scl_naming.ini`)
3. `--naming FILE` — explicit file
A key that is **absent or empty** in all of them means "no rule" → placeholder +
`TODO(E-Planung)`.
### 7.3 Syntax
```ini
; scl_naming.ini - Namensregeln fuer das FB_Main-Geruest
; Platzhalter: {id} TRO-ID (TRO107), {nr} Nummernteil (107), {n} laufende Nummer
; des Bauteils im TRO (1, 2 ...), {sep} Separatornummer aus dem Layout
[names]
separator_no = cSep{nr}.{n}
switch_no = cSwi{nr}.{n}
main_tro = cMainTro{nr}
priority_udt = udt_{id}
; ohne Regel -> TODO(E-Planung)
sensor_sep =
stopper =
switch_valve =
safety =
jam_area =
[defaults]
sep_trailing = t#100ms
sep_handling = t#50ms
release = TRUE
```
- `{nr}` is the numeric part of the ID **as written** in the JSON (`TRO01` → `01`,
`TRO107` → `107`); padding is not changed.
- `{sep}` is the separator's layout number from `separators[]` (`0040`) — useful if a
project names separators after the layout instead of the TRO.
- An unknown placeholder in a rule is an error (exit 1), not a silent literal.
### 7.4 Later: E-Planung data
When an I/O list exists in machine-readable form, a further section can point to it
(e.g. `[eplanung] file = …csv`) and fill class **E** values per separator/switch. That is
a separate step; the config format leaves room for it but this concept does not build it.
---
## 8. Tests
All under `tests/`, run with `python -m pytest tests/` like the existing ones.
### 8.1 Round trip per template (the main safety net)
For each template that has a reference call site (`tests/test_scl_templates.py`):
1. The test builds the context for a TRO with the reference topology and sets the
**real** values of the reference (ID `TRO107`, `cSep107.1`, `cInBG3306`, `MB3306`, the
real jam DBs, `carousel_run` …).
2. The test renders the template with that context.
3. It compares the result with the `REGION … END_REGION` part of the reference file,
**normalised**: whitespace collapsed, comments removed, case kept.
If they match, the parameterisation lost nothing: same lines, same order, same values.
When someone later replaces a reference file with a newer call site, this test shows
exactly where the template must follow.
### 8.2 Interface check (for FBs with source)
For `Vario_workStation`, `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom`: parse
`VAR_INPUT`/`VAR_OUTPUT`/`VAR_IN_OUT` of the FB source and check that every parameter
name the template writes exists there, with the right direction (`:=` for in/in-out,
`=>` for out). This covers the types that have no call site.
### 8.3 Golden files for the whole output
`tests/test_scl_skeleton_golden.py` renders `mubea_tro.json` and `HundM05_tro.json`
(copied into `tests/fixtures/`) and compares with checked-in expected SCL, with the
timestamp line masked.
- **During migration:** the golden files are today's output, so switching the
generator internals without enabling templates must give byte-identical files.
- **When a template is switched on:** the diff in the golden file is reviewed once
(it is the intended change) and then committed as the new golden file.
### 8.4 Naming and context
Unit tests for `scl_naming.py` (placeholders, lookup order, empty = TODO, unknown
placeholder = error) and `scl_context.py` (jam numbering, destination lists, flags such
as `carousel_run` from connection kinds).
---
## 9. Fallback and errors
| Situation | Behaviour | Exit |
|---|---|---|
| Template exists for the TRO's `fbType` | render template | 0 |
| No template (e.g. `2Sep2Swi`) | render with the current `emit_*` code; header lines `// Aus Template : …` / `// Generisch : … (kein Template - von Hand pruefen)` and an entry in the console summary | 0 |
| No template **and** `--strict` | stop, write nothing | 1 |
| Template uses an undefined variable / syntax error | stop, write nothing, show template file and line | 1 |
| Naming rule with unknown placeholder | stop, write nothing | 1 |
| Invalid TRO-JSON | unchanged from today | 1 |
With `--templates` the console summary gets a `Templates` line (implemented); the
`Namensregeln` line follows with step 4. Real output for mubea today:
```
Templates = 4 von 5 Typen (generisch: FB_ILS_MTRO_2Sep2Swi)
Offene Stellen = 482 (Marker 'TODO(E-Planung)')
```
---
## 10. CLI — additive only
Existing switches (`--json`, `--out`, `--start`) and defaults are unchanged.
| New switch | Argument | Default | Effect | Status |
|---|---|---|---|---|
| `--templates` | `[DIR]` (optional) | off; without `DIR`: `lib/scl_templates` | Render REGIONs from the FB templates; types without template stay generic | ✅ |
| `--strict` | — | off | With `--templates`: exit 1, write nothing, if any FB type of the plant has no template | ✅ |
| `--naming` | `FILE` | lookup per §7.2 | Explicit naming config | step 4 |
Templates are **opt-in** (`--templates`) until step 6. Without the switch the output is
byte-identical to before (golden files). When templates become the default in step 6, a
`--no-templates` switch keeps the old path for one release.
---
## 11. Migration plan
Each step ends with all tests green and is usable on its own.
| Step | Work | Done when |
|---|---|---|
| 1 ✅ | Golden-file tests on today's output (mubea, HundM05) — `tests/test_scl_skeleton_golden.py` | today's SCL is pinned |
| 2 ✅ | `scl_context.py`: build the render context from `TroDocument`; `emit_*` read from it — `tests/test_scl_context.py` | golden files byte-identical (also checked on dematic and showroom, with and without `--start`) |
| 3 ✅ | Pilot `FB_ILS_MTRO_1Sep1Swi.scl.j2` + `_macros`; switches `--templates`, `--strict`; class-C table (§6) filled with proposals — **still to be confirmed by a PLC engineer**; round-trip test | round trip against TRO107 passes; only the 1Sep1Swi REGIONs (+ header) differ from the generic golden files |
| 4 | `scl_naming.py` + `cfg/scl_naming.ini`; switch `--naming` | naming tests pass; Mubea renders with names instead of TODOs where a rule exists |
| 5 ✅ | Remaining call-site templates (`1Sep`, `Vario`, `1Sep2Swi`, `1Sep_SSCC`, `Vario_workStation`, `PinStore_Auto`), each with round trip against its reference (`PinStore_Auto`: the parts the reference abbreviates are left out of the comparison); decisions in §6 — **still to be confirmed by a PLC engineer**. Interface check (§8.2) for `Vario_workStation` still open | every round trip passes; HundM05 fully from templates, mubea except `2Sep2Swi` |
| 6 | Templates on by default; new `--no-templates` keeps the old path for one release | golden files updated and reviewed; one import into TIA Portal (as for Mubea) |
| 7 | Remove `emit_*` once every type in use has a template (`2Sep2Swi` needs a call site first) | fallback only for types without FB |
Docs touched along the way: `doc/Python_Scripts.md` §5b, `doc/Commandlines.md` §2.5,
`CLAUDE.md` (roadmap), this file.
---
## 12. What your workflow can rely on
For a workflow that runs **export CSV → SCL**:
**Calls (unchanged):**
```bat
bin\tro_flow.bat --file <name>.csv --json
bin\scl_skeleton.bat --json <name>_tro.json [--templates [DIR]] [--strict] [--naming <file>]
```
`--templates` and `--strict` exist today; `--naming` follows with step 4.
**Inputs:**
| File | Where | Who writes it |
|---|---|---|
| `<name>.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` |
| `<name>_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v4`) |
| `scl_naming.ini`, `<name>_scl_naming.ini` | `%SKEL_CFG%` | you, once per project (optional) |
| `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers |
**Outputs:** `%SKEL_RESULTS%/<name>_FB_Main.scl` (same name as today).
**Exit codes:** `0` ok (also when some TROs used the fallback) · `1` input, template or
naming error, or a missing template with `--strict`. `tro_flow` keeps its own codes (`3`
= TRO validation errors → stop the workflow before `scl_skeleton`).
**Quality gates a workflow can check:**
- exit code of each step;
- `Offene Stellen` in the summary (count of `TODO(E-Planung)`), e.g. track it per run;
- `Templates = x von y` — any TRO on the fallback needs a manual look;
- `grep "TODO(E-Planung)"` in the SCL lists every open value with its line.
**What stays manual:** class **E** values (sensor/actuator addresses, safety signals,
jam-area DBs) until an E-Planung export exists, and the `pruefen` timings.
---
## 13. Open questions (to clarify with the PLC engineers)
1. The class-C decisions per template (§6) — starting with `1Sep1Swi`.
2. Is the reference naming (`cSep{nr}.{n}`, `cMainTro{nr}`, `udt_{id}`) the standard for
new projects, or plant-specific?
3. Separator timings: reference values (100/50 ms) or the current skeleton values
(600/200 ms) as the default?
4. `xInRelease`: `TRUE` as in the reference, or the `"DB_TRO_TEST"` test release used today?
5. Real call sites for `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom` — available from
another plant?
6. `FB_ILS_MTRO_2Sep2Swi` (Mubea): will it be created in the ILSLib, or should the two
transfer points be modelled with existing types?
7. `nTo1Destinations`: TRO107 has 2 exits but the value 3 ("Info noetig fuer HMI"). Does
it count the destinations behind the switch rather than its exits? If so, which
destinations — the per-exit target lists in the skeleton could supply them.
8. `xInCarouselRun1`: how do we know a TRO sits at a carousel? `tro_flow` would need to
mark it (e.g. a carousel neighbour per TRO); today the line is always written as TODO.
9. `1Sep2Swi` (TRO267): switch 2 has no `nSwitchNo` and the REGION has no
`FC_Direction` call — omission in the reference, or intended (direction elsewhere)?
10. Meaning of `stInSettings.xIsBorner` (Vario) and `stInSettings.bSepType.%X2`
(`1Sep2Swi`) — always `TRUE`, or depending on the location?
11. `PinStore_Auto` with more than one feed/exit (mubea: 2/2): the FB has one
`JamEntr`/`JamExit` plus `JamBypass` — how are the others connected?
12. `Vario_workStation`: is `xPluggedScanner FALSE` right in general (scanner at the work
station, not at the separator)?
+230 -1
View File
@@ -1,6 +1,6 @@
# TRO Catalog — Building Blocks of the Skeleton Program (UH01–UH05)
**As of: 2026-07-20**
**As of: 2026-10-06**
This document is the **central image/concept catalog** for all TRO types of the
Fortna conveyor system. Goal: one place where, per TRO type, everything the
@@ -100,6 +100,55 @@ and line style in both the per-type schematics and the controller graphs:
**Total: 250 TRO instances across 9 actively used types** (`2Sep1Swi` prepared, never instantiated).
### Deciding the type — evidence sources and decision order
Each type section below ends with **Identification criteria** in three evidence layers:
- **Layout** — what the mechanical drawing / CSV export shows (host object of the
separator: Kreisel, Gefällestrecke (GF), driven Strecke (VF); switches, scanners, drives,
station labels). Available first, before any electrical planning.
- **Topology** — the neighbourhood in the material flow (`connect.ini`: in/out edges and
their `kind`, shared JamAreas, typical predecessor/successor types).
- **SCL / I/O** — what the call site in `FB_Main` (or the I/O list) proves beyond doubt:
which parameters, outputs and sub-blocks the instance has.
The layout is often ambiguous; the SCL call site never is. Recommended decision order
(it matches the derivation rules in `lib/tro_flow.py`):
| Step | Question | → Type |
|---:|---|---|
| 1 | Boom with forth/back, foot up/down, tilt + distance sensing, no stopper core? | `LoadingBoom` |
| 2 | Group of ≥ 2 parallel gravity lines, one separator each, between an infeed and an outfeed conveyor, with encoder + scanner + returning Vario? | `PinStore_Auto` (one per block) |
| 3 | Small multi-line buffer for empty carriers behind an infeed separator (`TROnnn_IN`), one distribution output per line, no encoder/scanner? | `EmptyCarrBuffer` |
| 4 | Separator on a **driven** segment (VF, own drive motor)? | `Vario` — or `Vario_workStation` if a manual station (station sensor + release button) sits on it |
| 5 | Separator with **3** outgoing paths (two switches in series)? | `1Sep2Swi` |
| 6 | Separator with **2** outgoing paths (switch / Ausschleuselement)? | `1Sep1Swi` |
| 7 | Separator with barcode **and** SSCC scanner, end measurement and WCS telegram (behind a loading boom)? | `1Sep_SSCC` |
| 8 | Everything else with one stopper, one entry, one exit (Kreisel track, single GF line, last separator before re-entry onto the Kreisel) — with or without scanner | `1Sep` |
| — | Two separate entry separators + one switch in one node | `2Sep1Swi` — concept only; in production modelled as 2× `1Sep` + `1Sep1Swi` |
**Summary per type** (details in each type's *Identification criteria*):
| Type | Deciding feature |
|---|---|
| `LoadingBoom` | Boom forth/back, foot up/down, tilt and distance sensing (analog), no separator. Always between a `1Sep` with scanner and a `1Sep_SSCC`. |
| `PinStore_Auto` | At least 2 parallel gravity lines with one separator each, between an infeed and an outfeed conveyor, plus a returning Vario (loop TRO208 → `1Sep1Swi` TRO203 → `Vario` TRO209 → `1Sep` TRO210 → TRO208). One TRO per block. |
| `EmptyCarrBuffer` | Small buffer (5 lines) behind an infeed separator `TROnnn_IN`, one output per line. No encoder, no scanner. Its exit merges with another flow into a `1Sep1Swi` (hence the priority manager). |
| `Vario` | Separator on a driven segment with its own motor. The outgoing edge is a `finger` edge. Infeed is often from `EXTERN` (then `isBorner`) or from a switch branch. |
| `Vario_workStation` | Like `Vario`, plus a station sensor, release button and load-error sensor. Pattern: `1Sep1Swi → Vario → WS → 1Sep → next switch`. |
| `1Sep2Swi` | 3 exits, two switches in series, always with a scanner. |
| `1Sep1Swi` | 2 exits (`normal` + `dir2`), a switch or an Ausschleuselement on the Kreisel. Typically part of a chain of switches along the main line. |
| `1Sep_SSCC` | Two scanners (barcode + SSCC), end measurement, WCS telegram, directly after the LoadingBoom. Can't be recognized from the mechanical layout alone. |
| `1Sep` | Everything with one stopper, one entry and one exit, with or without a scanner. This includes the last separator before re-entry onto the Kreisel and a single gravity line. |
| `2Sep1Swi` | Concept only. In production this case is built as two `1Sep` feeding the shared entry of one `1Sep1Swi`. |
The `PinStore_Auto` rule is confirmed by the SCL call site: it has exactly two conveyor
signals, `xInConveyorRunIn := fbSortConv1101` and `xInConveyorRunOut := fbExitConv1103`.
> **Limitations:** `1Sep2Swi`, `1Sep_SSCC`, `EmptyCarrBuffer` and `LoadingBoom` have only
> 2 instances each, so their patterns rest on little evidence. Whether the
> `EmptyCarrBuffer` lines are driven or gravity lines is not documented and left open.
---
## 1. `1Sep` — Standard Separator
@@ -134,6 +183,25 @@ flowchart LR
**All instances (96):**
TRO100, TRO101_IN, TRO102_IN, TRO103, TRO104, TRO106, TRO108, TRO109, TRO111, TRO113, TRO116, TRO117, TRO122, TRO125, TRO129, TRO130, TRO201, TRO207, TRO210, TRO213, TRO216, TRO219, TRO221, TRO227, TRO230, TRO233, TRO236, TRO239, TRO241, TRO247, TRO250, TRO253, TRO256, TRO259, TRO261, TRO263, TRO265, TRO266, TRO269, TRO274, TRO275, TRO301, TRO304, TRO309, TRO310, TRO311, TRO312, TRO315, TRO316, TRO323, TRO325, TRO326, TRO329, TRO332, TRO335, TRO338, TRO345, TRO347, TRO348, TRO351, TRO354, TRO357, TRO360, TRO367, TRO369, TRO370, TRO373, TRO376, TRO379, TRO382, TRO386, TRO402, TRO404, TRO406, TRO409, TRO412, TRO415, TRO416, TRO417, TRO424, TRO501, TRO503, TRO504, TRO507, TRO509, TRO511, TRO513, TRO515, TRO517, TRO519, TRO521, TRO523, TRO525, TRO526, TRO528, TRO529.
**Identification criteria:**
- **Layout:** exactly one separator (`S-SP`) with **no** switch and **no** own drive behind it.
Typical hosts: a separator on the (driven) Kreisel track; a single, isolated gravity line
(GF) with one separator; the **last** separator of a VF/GF chain right before it re-enters
the Kreisel (the Kreisel drives from there — alternating rule in `lib/tro_flow.py`);
manual stop points without drive (Aufgabestation, Qualitätskontrolle, fire-door stop
TRO402).
- **Topology:** one entry JamArea, one `normal` exit. Several predecessors may share its
entry JamArea (merge, e.g. TRO266 ← TRO263 + TRO274). Very common right **after a Vario**
(incoming `finger` edge): TRO209 → **TRO210** → PinStore TRO208, TRO408 → TRO421 → **TRO409**
→ switch TRO410. Also the feeder before a switch chain (TRO424 → TRO405).
- **SCL / I/O:** one stopper output (`MB…`), `sensorInSep` (`BG…`), optional `sensorJam2`;
`xInCarouselRun` from an `fbCarouselNNNN`; single exit JamArea; **no** `xInSwitchNo`, **no**
Vario motor (`MA…`), no switch output.
- **Not this type if:** 2/3 outgoing paths → `1Sep1Swi`/`1Sep2Swi`; own drive → `Vario`; SSCC
scanner + WCS telegram → `1Sep_SSCC`. A **plain barcode scanner does not change the type**
(TRO104, TRO109, TRO113, TRO117, TRO269, TRO406 are `1Sep` + scanner).
---
## 2. `1Sep1Swi` — Separator + 1 Switch
@@ -169,6 +237,29 @@ flowchart LR
**All instances (49):**
TRO107, TRO112, TRO114, TRO124, TRO202, TRO203, TRO204, TRO205, TRO206, TRO222, TRO223, TRO224, TRO225, TRO226, TRO242, TRO243, TRO244, TRO245, TRO246, TRO313, TRO314, TRO317, TRO318, TRO319, TRO320, TRO321, TRO322, TRO339, TRO340, TRO341, TRO342, TRO343, TRO344, TRO361, TRO362, TRO363, TRO364, TRO365, TRO366, TRO405, TRO407, TRO410, TRO413, TRO505, TRO510, TRO514, TRO518, TRO522, TRO527.
**Identification criteria:**
- **Layout:** separator directly in front of **one** switch (ILS Weiche, serial `W.…`) or an
**Ausschleuselement** where carriers leave the Kreisel track: one entry, two exits. Usually
with a barcode scanner in front (most UH02/UH04 instances carry `+Scanner(BXnnnn)`), since
the routing decision needs the carrier ID.
- **Topology:** exactly **two** outgoing edges — one `normal` (exit2/Dir1) and one `dir2`
(exit3). Typical patterns:
- **Switch chain along a main line**: each switch hands the main flow on via `dir2` to the
next switch and diverts via `normal` into a branch, which merges back into the next
switch's entry (UH04: TRO405 → 407 → 410 → 413; UH05: TRO505 → 510 → 514 → 518 → 522).
The branch is often `1Sep1Swi → Vario → (Vario_workStation) → 1Sep → merge`.
- **In front of / behind a PinStore block**: TRO202 feeds PinStore TRO211 via `dir2`;
TRO203 takes the PinStore exit and sends carriers to the returning Vario TRO209.
- **Merge with priority**: if the `dir2` exit is a JamArea also fed by a second flow
(TRO107 → JA0102_2 ← TRO101/TRO106), a `Dir2Manager` priority manager is used.
- **SCL / I/O:** in addition to `1Sep`: switch output `sw1ExTo3` (`MB…`), `sensorJam3`,
`xInSwitchNo := cSwiNNN.1`, `nTo1Destinations`, two exit JamAreas (`exit2`, `exit3`), and an
`FC_Direction` call with the switch number and the carrier source; optional
`PriorityManagerDir2In/Out`, `xInPrioDir2`.
- **Not this type if:** three exits → `1Sep2Swi`; two separate entry separators →
`2Sep1Swi` (concept only).
---
## 3. `1Sep2Swi` — Separator + 2 Switches
@@ -204,6 +295,22 @@ flowchart LR
**All instances (2):** TRO267, TRO268.
**Identification criteria:**
- **Layout:** one separator followed by **two switches in series** (cascade) → three
outgoing tracks; barcode scanner at the entry is always present. Sits at a distribution
point where carriers are handed over to other areas (handover JamAreas).
- **Topology:** **three** outgoing edges. TRO267 (entry JA1110_4, merge of TRO265 + TRO266):
`exit2`/`exit3` → handover JamAreas (`handoverJam5001`/`5000`), `exit4` → `dir3` → TRO275
(local loop). Both instances sit on the same loop JA1110 one after the other
(TRO267 → TRO275 → TRO268 → `dir3` TRO269).
- **SCL / I/O:** `switch1` **and** `switch2` (each with `nTo1Destinations`, `newSwitch`),
outputs `sw1ExTo3` + `sw2ExTo3`, `sensorJam2`…`sensorJam4`, exits `exit2`…`exit4`,
Cognex scanner block (`cScannerBXnnnn`, `hwResults`, `checkASCII`, timeout),
`xInCarouselRun1/2`, `sepType_x2`, `pluggedScanner = true`.
- **Derivation in `lib/tro_flow.py`:** a host with 3 outgoing flow edges upgrades `1Sep` →
`1Sep2Swi`.
---
## 4. `1Sep_SSCC` — Separator + SSCC Scanner
@@ -238,6 +345,22 @@ flowchart LR
**All instances (2):** TRO105, TRO110.
**Identification criteria:**
- **Layout:** separator with **two** read points — a normal barcode scanner plus an **SSCC**
scanner — and an end-measurement sensor, placed at the **system inbound** directly behind
a loading boom. Mechanically it looks like a `1Sep` + scanner; the type is decided by the
WCS requirement (dock request), so it **cannot be derived from the mechanical layout
alone** (`lib/tro_flow.py` only emits a hint).
- **Topology:** 1 in / 1 out. Fixed pattern
`1Sep(+Scanner) → LoadingBoom → 1Sep_SSCC → 1Sep`:
TRO104 → fbLoadingBoom1 → **TRO105** → TRO106 and TRO109 → fbLoadingBoom2 → **TRO110** → TRO111.
- **SCL / I/O:** second Cognex interface (`CognexInterfaceSSCC`, second `hwResults` BX),
`sensorEndMeasurement`, WCS `DReqM` telegram fields; release coupled to the WCS
connection (`FB_CommunicationWCSBL_DB.fbTeleHandler.xOutConnectionActive`);
`pluggedScanner = true`; `xInCarouselRun := true`.
- **Not this type if:** only one barcode reader and no WCS telegram → `1Sep`.
---
## 5. `Vario` — Chain Conveyor
@@ -272,6 +395,27 @@ flowchart LR
**All instances (70):**
TRO115, TRO120, TRO121, TRO123, TRO126, TRO127, TRO128, TRO131, TRO133, TRO134, TRO200, TRO209, TRO212, TRO215, TRO218, TRO220, TRO229, TRO232, TRO235, TRO238, TRO240, TRO249, TRO252, TRO255, TRO258, TRO260, TRO262, TRO264, TRO270, TRO272, TRO300, TRO302, TRO303, TRO305, TRO306, TRO307, TRO308, TRO324, TRO328, TRO331, TRO334, TRO337, TRO346, TRO350, TRO353, TRO356, TRO359, TRO368, TRO372, TRO375, TRO378, TRO381, TRO400, TRO401, TRO403, TRO408, TRO411, TRO414, TRO418, TRO420, TRO500, TRO502, TRO506, TRO508, TRO512, TRO516, TRO520, TRO524, TRO530, TRO531.
**Identification criteria:**
- **Layout:** separator at the start of a **driven** segment (VF — friction/chain drive with
its own motor `M.…`, layer `TX200FD`), i.e. host = driven "Strecke". One Vario TRO **per
separator**, not per Strecke. In a mixed VF/GF chain the alternating rule applies counted
back from the re-entry onto the Kreisel: last separator = `1Sep`, the one before = `Vario`,
etc.; a separator whose preceding segment actually loses height (> 30 mm, GF) is `1Sep`.
- **Topology:** the **outgoing** edge is of kind `finger` (index transfer to the next TRO,
usually a `1Sep`, another `Vario` or a `Vario_workStation`). Incoming typically:
- from **EXTERN** — system / PLC infeed (TRO115, TRO200, TRO220, TRO400; `isBorner`);
- from a `1Sep1Swi` branch exit (TRO203 → TRO209, TRO407 → TRO408);
- from another Vario via `finger` (TRO400 → TRO401, TRO500 → TRO530).
As the **returning Vario** of a PinStore block it closes the loop back to the store
entry (TRO209 → TRO210 → TRO208).
- **SCL / I/O:** Vario motor output (`MA…`) + motor protection (`FC…`), `automatic` and
`errorQuit` inputs, Vario sensors `carrInPos` / `finger` / `lastPos` / `jam`, Vario timings
(`waitForJog`, `convEmpty`, `rasterHigh/Low`…), optional `xInBorner`; JamAreas `entry` +
**`finger`** instead of `exit`; **no** `xInCarouselRun`.
- **Not this type if:** station sensor + release button → `Vario_workStation`; separator on
an undriven gravity piece → `1Sep`. F2H segments (UH03) are plain `Vario` (comment only).
---
## 6. `PinStore_Auto` — Automatic Pin Storage
@@ -309,6 +453,31 @@ flowchart LR
**All instances (24):**
TRO208, TRO211, TRO214, TRO217, TRO228, TRO231, TRO234, TRO237, TRO248, TRO251, TRO254, TRO257, TRO327, TRO330, TRO333, TRO336, TRO349, TRO352, TRO355, TRO358, TRO371, TRO374, TRO377, TRO380.
**Identification criteria:**
- **Layout:** a **set of parallel gravity lines (GF, `ABH-GEF`) between two conveyors** —
an infeed/sorter conveyor that distributes onto the lines and an outfeed conveyor that
collects from them — plus a **returning Vario conveyor** that leads carriers back to the
store entry. Each line has its own line separator; at the infeed sit a pin separator, a
barcode scanner and an encoder on the infeed conveyor. ≥ 2 lines (`MIN_STORE_LANES` in
`lib/tro_flow.py`), up to 19 per block. **One TRO per block** (group of lines), never one
per line.
- **Topology** (UH02 block 1.1.x, repeated 4× per hall section):
- entry fed by a `1Sep1Swi` with scanner (TRO202 `dir2` → TRO211) **and** by the return
chain `Vario → finger → 1Sep` (TRO209 → TRO210 → TRO208);
- exit → `1Sep1Swi` (TRO208 → TRO203), whose branch leads into the returning Vario
(TRO203 → TRO209) — the loop store → switch → Vario → 1Sep → store;
- `bypass` edge from the neighbouring block (TRO211 → TRO208, TRO214 → TRO217);
- `EXTERN` storage edge (`Storage1.1.1`) = handshake with the storage intelligence.
- **SCL / I/O:** two conveyor signals **`xInConveyorRunIn`** (`fbSortConv…`) and
**`xInConveyorRunOut`** (`fbExitConv…`) — the SCL proof of "between two conveyors";
`arInSep[1..N]` with `nJamLength` per line, `nNoOfLines`, encoder parameters
(`fbEncoderGF…`), `arInOutStorageLines := DB_Storage…`, `stMFRStorageRemovalInterface`,
`ConveyorStations`, jam bypass, `CognexInterfacePin`, line outputs `MB…` per line, unloading
jam sensors per line. No `xInCarouselRun`.
- **Not this type if:** a **single** GF line with one separator → `1Sep`; a small manual
empty-carrier buffer without encoder/scanner/storage handshake → `EmptyCarrBuffer`.
---
## 7. `Vario_workStation` — Manual Work Station
@@ -341,6 +510,21 @@ flowchart LR
**All instances (3):** TRO421, TRO422, TRO423.
**Identification criteria:**
- **Layout:** a **driven** Vario segment at a **manual work station** — operator place,
release button, station sensor, load-error sensor; station label in the drawing
(*Arbeitsplatz*). Without a drive a manual stop is **not** this type (Mubea:
Aufgabestation / Qualitätskontrolle → `1Sep`).
- **Topology:** both edges are `finger`: in from an upstream Vario, out to a `1Sep` that
merges back into the switch chain. Fixed branch pattern (UH04, 3×):
`1Sep1Swi → Vario → finger → Vario_workStation → finger → 1Sep → next 1Sep1Swi`
(TRO407 → TRO408 → **TRO421** → TRO409 → TRO410).
- **SCL / I/O:** all `Vario` parameters **plus** `station.sensor`, `releaseStation`
(edge-triggered button, `rTrig_Workstation…`), `loadErrorSensor`; internally
`FB_ILS_STRO_Vario_WorkStation` + `FB_CarrAccumulate1Sep`.
- **Not this type if:** no station sensor / release button → `Vario`.
---
## 8. `EmptyCarrBuffer` — Empty Carrier Buffer
@@ -375,6 +559,23 @@ flowchart LR
**All instances (2):** TRO101, TRO102.
**Identification criteria:**
- **Layout:** a **small multi-line buffer for empty carriers** (5 lines in UH01) branching
off the main line, with one distribution switch output per line and an **infeed
separator** (`TROnnn_IN`, a borner) in front of it. No encoder, no scanner.
- **Topology:** `1Sep → TROnnn_IN (1Sep) → JamBufferNNN → EmptyCarrBuffer`. The `_IN`
separators of several buffers sit in series on the same line (TRO100 → TRO101_IN →
TRO102_IN). The buffer exit **merges** into the entry of a `1Sep1Swi` together with another
flow (TRO101 → TRO112 at JA0102_2, TRO102 → TRO107 at JA0102_1) — hence the priority
manager.
- **SCL / I/O:** `FB_EmptyCarrBuffer` (not `FB_ILS_MTRO_*`), `numberOfLines`,
`storageDb := DB_JamBufferNNN`, `inTro` block (`isBorner`, `bornerDest`, own stopper),
`separatorLines[]` with `jamLength` + sensor, `switchLineOutputs` (`MB…` per line),
`jamSensors` per line, `exitSensor`, `PriorityManagerIn/Out`.
- **Not this type if:** encoder, scanner, storage-removal handshake, two conveyors and a
returning Vario → `PinStore_Auto`.
---
## 9. `LoadingBoom` — Loading Boom
@@ -406,6 +607,20 @@ flowchart LR
**All instances (2):** fbLoadingBoom1 (→ TRO104), fbLoadingBoom2 (→ TRO109).
**Identification criteria:**
- **Layout:** a telescopic **loading boom** at the system inbound (loading area): boom drive
forth/back, foot up/down, tilt and distance sensing. No separator/switch core — the only
structurally independent type.
- **Topology:** always between a `1Sep` with scanner and a `1Sep_SSCC`; its entry is the
stopper JamArea of the preceding TRO (`relatedTro`): TRO104 → **fbLoadingBoom1** → TRO105,
TRO109 → **fbLoadingBoom2** → TRO110.
- **SCL / I/O:** instance name `fbLoadingBoomN` instead of `TROnnn`; `FB_LoadingBoom_INBOUND`
with `FB_Tilt_TEST`, `FB_DistanceFoot`, `FB_BoomWorkStation`; **analog** inputs
(`wTiltSensor…`, `wDistanceSensor…`), limit switches boom forth/back (`BG…`) and foot
up/down (`SF…`), several motor protections (`FC…`), HMI under `stLoadingBoom` instead of
`stTRO`. No `FB_ILS_STRO_Sep` / `FB_ILS_STRO_Switch`.
---
## 10. `2Sep1Swi` — Unused (Concept Block)
@@ -436,6 +651,20 @@ flowchart LR
![2Sep1Swi – BricsCAD reference layout](img/2Sep1Swi_layout.png)
> _BricsCAD reference capture (concept block, not instantiated in production). File: `img/2Sep1Swi_layout.png`._
**Identification criteria:**
- **Layout:** two separators, each on its own incoming track, directly in front of **one**
switch — merge and divert in a single hand-over point.
- **Topology:** two incoming edges into **separate** entry JamAreas, two outgoing edges
(`normal` + `dir2`). In the reference plants this situation is always modelled
differently: two `1Sep` feeding the **shared** entry JamArea of one `1Sep1Swi`
(TRO117 + TRO102 → TRO107 at JA0102_1). So: **do not choose this type** for production
code as long as the FB is not finished.
- **SCL / I/O:** `separator1`/`separator2` with `stopper1`/`stopper2`,
`xInCarouselReq1/2`, `switch1`, `prioDir1…4`, `car2NecRight/Left`.
- **Related:** `lib/tro_flow.py` derives a `2Sep2Swi` (Ausschleuselement + ILS Weiche
directly in a row on the Kreisel, Mubea) — that FB does not exist in the ILSLib yet either.
---
## Controller topology graphs (UH01–UH05)
@@ -0,0 +1,62 @@
// REFERENZ: Staubereichs-Datenbausteine (DB_JamArea<NNNN>)
// ------------------------------------------------------------------------------------------
// Zwei reale Beispiele, vom SPS-Programmierer zur Verfuegung gestellt (2026-10-13).
// Nur Referenzmaterial fuer die spaetere Erzeugung der Staubereichs-DBs
// (doc/Konzept_Layout_Hardware_SCL.md, Stufe 4 "Staubereichs-DBs") - nicht ausfuehren,
// nicht aendern.
//
// Aufbau:
// - ein DB je Foerderer <NNNN> (4501, 4104)
// - je Staubereich auf diesem Foerderer ein Struct stJam<NNNN>_<k>
// arCarrier : Array[1.."cJam<NNNN>_<k>"] of "stCarrier" (Groesse ueber PLC-Konstante)
// stData : "stJamData"
// - im BEGIN-Teil je Staubereich nCapacityMax / nCapacitySet
//
// Bezug: "DB_JamArea4104".stJam4104_1 ist der Eingangs-Staubereich von TRO 115
// (FB_ILS_MTRO_Vario.scl, arInOutJamEntr).
//
// Offen (mit SPS klaeren): Wert der Konstanten cJam<NNNN>_<k> (= nCapacityMax?),
// wann nCapacityMax <> nCapacitySet, Herkunft der Kapazitaet (Laenge / Traegerabstand?),
// NON_RETAIN zusammen mit VAR RETAIN beabsichtigt, Beispiel fuer einen Kreisel-DB (_1.._6).
// ------------------------------------------------------------------------------------------
DATA_BLOCK "DB_JamArea4501"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
VAR RETAIN
stJam4501_1 : Struct
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4501_1"] of "stCarrier";
stData : "stJamData";
END_STRUCT;
stJam4501_2 : Struct
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4501_2"] of "stCarrier";
stData : "stJamData";
END_STRUCT;
END_VAR
BEGIN
stJam4501_1.stData.Config.nCapacityMax := 5;
stJam4501_1.stData.Config.nCapacitySet := 5;
stJam4501_2.stData.Config.nCapacityMax := 50;
stJam4501_2.stData.Config.nCapacitySet := 50;
END_DATA_BLOCK
DATA_BLOCK "DB_JamArea4104"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
VAR RETAIN
stJam4104_1 : Struct
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4104_1"] of "stCarrier";
stData : "stJamData";
END_STRUCT;
END_VAR
BEGIN
stJam4104_1.stData.Config.nCapacityMax := 8;
stJam4104_1.stData.Config.nCapacitySet := 8;
END_DATA_BLOCK
+1 -1
View File
@@ -477,5 +477,5 @@
"to": "TRO05"
}
],
"openPoints": []
"candidates": []
}
+38 -14
View File
@@ -50,6 +50,7 @@ neue Ermittlung.
from __future__ import annotations
import hashlib
import json
import math
from collections import Counter
@@ -58,6 +59,8 @@ from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field
from parameter import get_float, get_int
__all__ = [
"Transform",
"RegistrationError",
@@ -67,6 +70,7 @@ __all__ = [
"load_transform",
"save_transform",
"registration_path",
"file_fingerprint",
]
# Rollen fuer den Lagebezug: CSV-Objektart -> Blocknamen im DXF, als Muster
@@ -84,18 +88,15 @@ ANCHOR_BLOCKS: dict[str, tuple[str, ...]] = {
"Scanner": ("SCAN", "Scanner_*"),
}
# Rastergroesse der Abstimmung in mm
VOTE_GRID = 100.0
# So viele Kandidaten der Abstimmung werden ausgeglichen und bewertet
VOTE_CANDIDATES = 6
# Nur Paare bis zu diesem Abstand gehen in den Ausgleich ein (exakte Treffer)
FIT_TOL = 50.0
# Punkte gelten als zugeordnet, wenn sie nach der Verschiebung so nah liegen
DEFAULT_MATCH_TOL = 600.0
# So viele exakte Treffer muessen es mindestens sein
MIN_INLIERS = 5
# Ab dieser Drehung wird gewarnt (Grad)
ROTATION_WARN_DEG = 0.05
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [dxf_registration] - Bedeutung
# und Einheit stehen dort (lib/parameter.py).
_P = "dxf_registration"
VOTE_GRID = get_float(_P, "vote_grid")
VOTE_CANDIDATES = get_int(_P, "vote_candidates")
FIT_TOL = get_float(_P, "fit_tol")
DEFAULT_MATCH_TOL = get_float(_P, "default_match_tol")
MIN_INLIERS = get_int(_P, "min_inliers")
ROTATION_WARN_DEG = get_float(_P, "rotation_warn_deg")
class RegistrationError(RuntimeError):
@@ -124,6 +125,10 @@ class Transform(BaseModel):
csv_file: str = Field(default="")
dxf_file: str = Field(default="")
created: str = Field(default="")
# Pruefsumme der Zeichnung/E-Datei beim Ermitteln ("" = unbekannt). Der
# Schluessel enthaelt nur Dateinamen; ein neuer Stand gleichen Namens wird
# daran erkannt (siehe load_transform).
fingerprint: str = Field(default="")
# -- Anwenden ----------------------------------------------------------
@@ -330,8 +335,25 @@ def _key(csv_file: str, dxf_file: str) -> str:
return f"{Path(csv_file).name}|{Path(dxf_file).name}"
def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str) -> Transform | None:
"""Geprueften Lagebezug aus der Konfiguration lesen."""
def file_fingerprint(path: Path) -> str:
"""SHA-256 einer Datei (Hex) - Kennung des Planungsstands."""
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str,
fingerprint: str = "") -> Transform | None:
"""Geprueften Lagebezug aus der Konfiguration lesen.
Mit ``fingerprint``: nur verwenden, wenn der abgelegte Bezug fuer genau
diesen Dateistand ermittelt wurde. Ein Eintrag ohne Pruefsumme (aeltere
Ablage) oder mit anderer gilt als veraltet -> None, der Aufrufer ermittelt
neu. Ohne ``fingerprint`` (z. B. tro_annotate) wie bisher nur ueber den
Dateinamen.
"""
path = registration_path(cfg_dir)
if not path.is_file():
return None
@@ -342,6 +364,8 @@ def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str) -> Transform | N
entry = data.get(_key(csv_file, dxf_file))
if not entry:
return None
if fingerprint and entry.get("fingerprint", "") != fingerprint:
return None
transform = Transform.model_validate(entry)
transform.source = "cfg"
return transform
+361
View File
@@ -0,0 +1,361 @@
# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
e_planning.py - Bruecke Layout <-> E-Planung ueber die Symbolpositionen.
Das Layout (CSV) sagt, welche Separatoren, Weichen, Aus-/Einschleuselemente
es gibt und wo; die E-Planung, wie ihre Sensoren und Ventile heissen
(BG0642, MB0643 ...). Die EA-Liste allein kennt keine Topologie. Die
E-Planung wird aber als Layer ueber der Hardwareplanung gezeichnet: jedes
Sensor-/Aktorsymbol liegt neben dem Bauteil, zu dem es gehoert. Das
Kabellaengen-Tool (kabellaengen/lib/getpositions.py) schreibt diese Symbole
mit Position und Attributen nach <zeichnung>_positionsdraw.json.
Hier wird daraus die Zuordnung Layout-Bauteil -> BMK:
1. E-Bauteile einlesen (`sensors` und `schaltschrank_elemente`).
2. Lagebezug E-Zeichnung -> CSV mit lib/dxf_registration.py; Anker sind die
Separator-Sensoren der E-Planung gegen die Separatoren des CSV. Der
gepruefte Bezug wird in SKEL_CFG/dxf_registration.json abgelegt.
3. Regeln aus SKEL_CFG/e_planning.ini anwenden, je Rolle (z. B.
"Separator.sensor"): welche Layout-Art, welche E-Verwendung (VERW),
welcher Hoechstabstand - oder "Nummer = Nummer des Partners + Versatz"
(Ventil MB0643 zum Sensor BG0642). Die Zuordnung ist eindeutig: jedes
E-Bauteil und jede Rolle eines Layout-Bauteils wird hoechstens einmal
vergeben, die kuerzesten Abstaende zuerst.
4. Ergebnis als <csv>_eplan.json (EplanDocument) und Bericht
<csv>_eplan.md nach SKEL_RESULTS.
scl_skeleton.py --eplan liest die JSON und setzt die gefundenen Namen in die
SCL ein (SclName mit todo=False). Was keine Regel trifft, bleibt dort
TODO(E-Planung) - geraten wird nicht.
Seit 2026-10-15 liegen das E-Modell (EDevice, load_positionsdraw,
pair_drives) in hw_import.py und die Zuordnung (Regeln, raeumlich,
Nummernregel, Lagebezug) in merge_hw_layout.py - der neue Weg ueber die
Anlagen-JSON (doc/Konzept_Layout_Hardware_SCL.md). e_planning bleibt fuer
scl_skeleton --eplan, bis scl_skeleton die Anlagen-JSON liest, und
re-exportiert die verschobenen Namen.
Schichten: e_planning importiert material_flow (CSV), hw_import und
merge_hw_layout; scl_context importiert nur das Modell EplanDocument.
Aufruf ueber bin/e_planning.bat bzw. bin/e_planning.sh.
"""
from __future__ import annotations
import argparse
import sys
from datetime import datetime
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field
from dxf_registration import RegistrationError, Transform, file_fingerprint
from merge_hw_layout import (
LAYOUT_KINDS,
RULES_FILE,
Assignment,
LayoutPoint,
Rule,
_match,
assign,
load_rules,
resolve_transform,
)
from hw_import import Drive, EDevice, load_positionsdraw, pair_drives
__all__ = [
"EPLAN_SCHEMA",
"EDevice",
"LayoutPoint",
"Rule",
"Assignment",
"Drive",
"EplanDocument",
"load_positionsdraw",
"load_rules",
"assign",
"pair_drives",
"layout_points",
"resolve_transform",
"LAYOUT_KINDS",
]
EPLAN_SCHEMA = "sps_skel-eplan-v1"
# ---------------------------------------------------------------------------
# Modell
# ---------------------------------------------------------------------------
class EplanDocument(BaseModel):
"""Die Zuordnungs-JSON <csv>_eplan.json."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
schema_id: str = Field(default=EPLAN_SCHEMA, alias="schema")
created: str = ""
csv_file: str = ""
eplan_file: str = ""
transform: Transform | None = None
assignments: list[Assignment] = Field(default_factory=list)
drives: list[Drive] = Field(default_factory=list)
# E-Bauteile im Feld ohne Zuordnung (BMK)
unassigned: list[str] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
def for_layout(self, layout_id: str) -> dict[str, Assignment]:
"""Rollen eines Layout-Bauteils: {"sensor": Assignment, ...}."""
return {
a.role.split(".", 1)[-1]: a
for a in self.assignments if a.layout_id == layout_id
}
def to_json(self) -> str:
return self.model_dump_json(by_alias=True, indent=2) + "\n"
@classmethod
def from_json(cls, text: str) -> "EplanDocument":
return cls.model_validate_json(text)
# ---------------------------------------------------------------------------
# Layout
# ---------------------------------------------------------------------------
def layout_points(elements) -> list[LayoutPoint]:
"""Punktartige Bauteile aus den CSV-Elementen (material_flow.Element)."""
points = []
for e in elements:
if e.position is None:
continue
kind = next((k for k in LAYOUT_KINDS
if k.lower() in f"{e.kind} {e.teile_art}".lower()), "")
if kind:
points.append(LayoutPoint(e.teile_id, kind, e.x, e.y, e.name))
return points
# ---------------------------------------------------------------------------
# Bericht
# ---------------------------------------------------------------------------
def render_report(doc: EplanDocument, rules: list[Rule], points: list[LayoutPoint],
devices: list[EDevice]) -> str:
out = []
add = out.append
add(f"# E-Planung -> Layout: {doc.csv_file}")
add("")
add(f"- E-Planung: `{doc.eplan_file}`")
add(f"- Erzeugt: {doc.created}")
if doc.transform:
add(f"- Lagebezug: {doc.transform.describe()}")
add("")
add("## Zuordnung je Rolle")
add("")
add("| Rolle | Layout-Bauteile | zugeordnet | davon Nummernregel |")
add("|---|---|---|---|")
for r in rules:
n_pts = sum(1 for p in points if _match(p.kind, r.layout))
hits = [a for a in doc.assignments if a.role == r.role]
n_num = sum(1 for a in hits if a.rule == "nummer")
add(f"| {r.role} | {n_pts} | {len(hits)} | {n_num} |")
add("")
add("## Zuordnungen")
add("")
add("Abstand nach dem Lagebezug; \"2.\" = Abstand des naechstbesten Kandidaten "
"(knapp beieinander = pruefen).")
add("")
add("| Layout | Art | Rolle | BMK | VERW | Bezeichnung | Abstand mm | 2. | Regel | Station |")
add("|---|---|---|---|---|---|---|---|---|---|")
for a in doc.assignments:
second = "" if a.runner_up is None else f"{a.runner_up:.0f}"
add(f"| {a.layout_id} | {a.layout_kind} | {a.role.split('.', 1)[-1]} | {a.bmk} "
f"| {a.verw} | {a.bezeichnung} | {a.distance:.0f} | {second} | {a.rule} "
f"| {a.controller}-{a.station} |")
add("")
roles_of: dict[str, set[str]] = {}
for a in doc.assignments:
roles_of.setdefault(a.layout_id, set()).add(a.role)
add("## Layout-Bauteile ohne Zuordnung")
add("")
any_missing = False
for r in rules:
missing = [p for p in points
if _match(p.kind, r.layout) and r.role not in roles_of.get(p.layout_id, set())]
if missing:
any_missing = True
add(f"- **{r.role}**: {', '.join(p.layout_id for p in missing)}")
if not any_missing:
add("- keine")
add("")
by_bmk = {d.bmk: d for d in devices}
add("## E-Bauteile im Feld ohne Zuordnung")
add("")
if doc.unassigned:
add("| BMK | VERW | Bezeichnung | Station |")
add("|---|---|---|---|")
for bmk in doc.unassigned:
d = by_bmk[bmk]
add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |")
else:
add("- keine")
add("")
add("## Antriebe (Motor <-> Motorschutz)")
add("")
if doc.drives:
add("| Motor | Bezeichnung | Motorschutz | Bezeichnung |")
add("|---|---|---|---|")
for dr in doc.drives:
add(f"| {dr.motor} | {dr.bezeichnung} | {dr.protection or '-'} "
f"| {dr.protection_bezeichnung} |")
else:
add("- keine Motoren in der E-Planung")
add("")
if doc.warnings:
add("## Hinweise")
add("")
for w in doc.warnings:
add(f"- {w}")
add("")
return "\n".join(out)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_document(csv_path: Path, eplan_path: Path, rules_path: Path, cfg_dir: Path,
re_register: bool = False
) -> tuple[EplanDocument, list[Rule], list[LayoutPoint], list[EDevice]]:
"""Kompletter Lauf ohne Ausgabe: CSV + positionsdraw -> EplanDocument."""
from material_flow import read_elements # erst hier: Tests ohne CSV brauchen es nicht
warnings: list[str] = []
elements = read_elements(csv_path, warnings)
points = layout_points(elements)
devices = load_positionsdraw(eplan_path, warnings)
rules, anchors = load_rules(rules_path)
transform = resolve_transform(points, devices, anchors, cfg_dir, csv_path.name,
eplan_path.name, re_register, warnings,
file_fingerprint(eplan_path))
assignments = assign(points, devices, rules, transform)
used = {a.bmk for a in assignments}
unassigned = sorted(d.bmk for d in devices
if not d.cabinet and d.pos is not None and d.bmk not in used)
doc = EplanDocument(
created=datetime.now().strftime("%Y-%m-%d %H:%M"),
csv_file=csv_path.name,
eplan_file=eplan_path.name,
transform=transform,
assignments=assignments,
drives=pair_drives(devices),
unassigned=unassigned,
warnings=warnings,
)
return doc, rules, points, devices
def main(argv: list[str] | None = None) -> int:
from material_flow import env_dir, resolve_input
args = parse_args(argv)
csv_path = resolve_input(args.file)
eplan_path = resolve_input(args.eplan)
cfg_dir = env_dir("SKEL_CFG", "cfg")
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
for label, path in (("CSV", csv_path), ("E-Planung", eplan_path),
("Regeln", rules_path)):
if not path.is_file():
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
return 1
try:
doc, rules, points, devices = build_document(
csv_path, eplan_path, rules_path, cfg_dir, args.re_register)
except RegistrationError as exc:
print(f"FEHLER: Lagebezug E-Planung -> CSV nicht nachweisbar: {exc}",
file=sys.stderr)
print(" Passen CSV und E-Zeichnung zusammen? Anker-VERW in "
f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr)
return 1
except (OSError, ValueError) as exc:
print(f"FEHLER: {exc}", file=sys.stderr)
return 1
results = env_dir("SKEL_RESULTS", "results")
results.mkdir(parents=True, exist_ok=True)
json_file = results / f"{csv_path.stem}_eplan.json"
md_file = results / f"{csv_path.stem}_eplan.md"
json_file.write_text(doc.to_json(), encoding="utf-8")
md_file.write_text(render_report(doc, rules, points, devices), encoding="utf-8")
print("")
print("================================================================")
print("E-PLANUNG -> LAYOUT")
print("================================================================")
print(f"CSV = {csv_path}")
print(f"E-Planung = {eplan_path}")
print(f"Lagebezug = {doc.transform.describe() if doc.transform else '-'}")
for r in rules:
n_pts = sum(1 for p in points if _match(p.kind, r.layout))
hits = sum(1 for a in doc.assignments if a.role == r.role)
print(f" {r.role:<26} {hits:4} von {n_pts}")
print(f"Ohne Zuordnung = {len(doc.unassigned)} E-Bauteile im Feld")
print(f"Antriebe = {len(doc.drives)} "
f"({sum(1 for d in doc.drives if d.protection)} mit Motorschutz)")
for w in doc.warnings:
print(f" ! {w}")
print(f"JSON = {json_file}")
print(f"Bericht = {md_file}")
print("================================================================")
print("")
return 0
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="e_planning",
description="Ordnet die Sensoren/Ventile der E-Planung "
"(*_positionsdraw.json des Kabellaengen-Tools) den Bauteilen "
"des Layouts (CSV) zu. Ergebnis: <csv>_eplan.json fuer "
"scl_skeleton --eplan und ein Bericht <csv>_eplan.md.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Beispiele:\n"
" e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json\n"
" e_planning.bat --file mubea.csv --eplan C:\\pfad\\Mubea_positionsdraw.json "
"--re-register\n"
"\n"
"Exit-Codes:\n"
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
)
parser.add_argument("--file", default="export.csv", metavar="NAME",
help="CSV-Export in %%SKEL_DATA%% (oder ein Pfad). "
"Standard: %(default)s")
parser.add_argument("--eplan", required=True, metavar="NAME",
help="*_positionsdraw.json der E-Planung in %%SKEL_DATA%% "
"(oder ein Pfad)")
parser.add_argument("--rules", metavar="DATEI",
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
parser.add_argument("--re-register", action="store_true",
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
"einer abgelegt ist")
return parser.parse_args(argv)
if __name__ == "__main__":
sys.exit(main())
+784
View File
@@ -0,0 +1,784 @@
# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
hw_import.py - Hardwareplanung einlesen: TIA-Import + Positionsdatei -> <projekt>_hw.json.
Stufe 2 des Ablaufs in doc/Konzept_Layout_Hardware_SCL.md. Zwei Quellen
genuegen (Befund doc/E-Planung_Bruecke.md, "Nachpruefung 2026-10-15"):
* **TIA-Import** ``*_TIA.xlsx`` - Blatt "PLC Tags" (Name, Datentyp,
Absolutadresse, Kommentar) und Blatt "Constants" (Index-Konstante
``cIn<BMK>``/``cOut<BMK>`` je Signal). Das ist genau das, was im SCL steht
("DB_Inputs".Sensors["cInBG0532"], "MB0533") und im TIA-Projekt existieren
muss - deshalb die erste Quelle und die Pruefliste fuer das SCL.
* **Positionsdatei** ``*_positionsconv.json`` / ``*_positionsdraw.json`` des
Kabellaengen-Tools (optional) - je BMK die Lage in der E-Zeichnung, die
Verwendung (VERW, z. B. "IN SEP"), Klartext und E/A-Station.
EA-Liste und WSCAD-Import bringen bei vorhandener Positionsdatei nichts
dazu und werden nicht gelesen.
Ergebnis ``HardwareDocument`` (Schema sps_skel-hw-v1): je BMK ein Geraet mit
beiden Seiten, die Antriebe (Motor MA <-> Motorschutz FC, Geber GF) je
Foerderer der E-Planung (K0102; "#106" wird K0106), und die Gegenpruefung
beider Quellen.
Die Zuordnung zum Layout macht merge_hw_layout.py.
Die xlsx-Datei wird ohne Fremdbibliothek gelesen (zipfile + XML): der
TIA-Import ist eine einfache Tabelle, openpyxl waere eine Abhaengigkeit nur
dafuer.
Aufruf ueber bin/hw_import.bat bzw. bin/hw_import.sh.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import zipfile
import xml.etree.ElementTree as ET
from collections import Counter
from datetime import datetime
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field
from dxf_registration import file_fingerprint
__all__ = [
"HW_SCHEMA",
"EDevice",
"Drive",
"TiaTag",
"TiaConstant",
"HwDevice",
"HardwareDocument",
"read_xlsx",
"load_tia",
"load_positionsdraw",
"pair_drives",
"conveyor_of",
"conveyor_kind_of",
"build_hardware",
]
HW_SCHEMA = "sps_skel-hw-v1"
_CONFIG = ConfigDict(extra="forbid", populate_by_name=True)
# "=A1+UH01-KF107" -> Ort/Steuerung "UH01", Geraet "KF107"
_KENNZEICHNUNG = re.compile(r"\+([A-Za-z0-9]+)-([A-Za-z0-9]+)")
# "MB0643" -> ("MB", "0643")
_BMK = re.compile(r"^([A-Za-z]+)(\d+)$")
# "%E053.2" / "%I53.2" / "%A020.0" / "%Q20.0"
_ADDRESS = re.compile(r"^%?([EIAQM])\s*(\d+)\.(\d)$", re.IGNORECASE)
# Foerderer im Klartext der E-Planung: "KREISEL-K0102", "KR-K0104", "K0105"
_CONVEYOR_K = re.compile(r"(?<![A-Z0-9])K(\d{4})(?!\d)")
# ... bzw. "CONVEYOR #100", "CONVEYOR-#106", "CONVEYOR#110"
_CONVEYOR_HASH = re.compile(r"#\s*(\d{3})(?!\d)")
# Art des Foerderers im Klartext: "KREISEL-K0102", "KR-K0104" -> Kreisel;
# "CONVEYOR #106", "KLINKENFOERDERER" -> Strecke
_KIND_KREISEL = re.compile(r"(?<![A-Z])(KREISEL|KR)(?![A-Z])")
_KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER")
# ---------------------------------------------------------------------------
# Modell
# ---------------------------------------------------------------------------
class EDevice(BaseModel):
"""Ein Sensor/Aktor/Schrankelement der E-Planung (Positionsdatei)."""
model_config = _CONFIG
bmk: str
verw: str = ""
bezeichnung: str = ""
kennzeichnung: str = ""
# Position in der E-Zeichnung (mm); None bei Schrankelementen ohne Lage
pos: tuple[float, float] | None = None
# True: aus "schaltschrank_elemente" (im Schrank, nicht im Feld)
cabinet: bool = False
@property
def controller(self) -> str:
"""Steuerung/Ort aus der Kennzeichnung ("UH01"), "" wenn unbekannt."""
m = _KENNZEICHNUNG.search(self.kennzeichnung)
return m.group(1) if m else ""
@property
def station(self) -> str:
"""E/A-Station aus der Kennzeichnung ("KF107"), "" wenn unbekannt."""
m = _KENNZEICHNUNG.search(self.kennzeichnung)
return m.group(2) if m else ""
@property
def prefix(self) -> str:
m = _BMK.match(self.bmk)
return m.group(1) if m else ""
@property
def number(self) -> int | None:
m = _BMK.match(self.bmk)
return int(m.group(2)) if m else None
class Drive(BaseModel):
"""Motor und sein Motorschutz (gleiche Nummer), dazu Foerderer und Geber."""
model_config = _CONFIG
motor: str
protection: str = ""
bezeichnung: str = ""
protection_bezeichnung: str = ""
# Foerderer der E-Planung aus dem Klartext ("K0102", "#106" -> "K0106")
conveyor: str = ""
# "Kreisel" | "Strecke" | "" aus dem Klartext (conveyor_kind_of)
conveyor_kind: str = Field(default="", alias="conveyorKind")
# Inkrementalgeber (GF...) desselben Foerderers
encoder: str = ""
class TiaTag(BaseModel):
"""Eine Zeile aus dem Blatt "PLC Tags" des TIA-Imports."""
model_config = _CONFIG
name: str
data_type: str = Field(default="", alias="dataType")
address: str = ""
comment: str = ""
class TiaConstant(BaseModel):
"""Eine Zeile aus dem Blatt "Constants" des TIA-Imports."""
model_config = _CONFIG
name: str
data_type: str = Field(default="", alias="dataType")
value: str = ""
comment: str = ""
class HwDevice(EDevice):
"""
Ein Geraet der Hardwareplanung: Positionsdatei und TIA-Import zusammen.
Die Felder aus EDevice kommen aus der Positionsdatei (leer, wenn das BMK
dort fehlt), Adresse/Konstante aus dem TIA-Import (leer, wenn es dort
fehlt). ``sources`` sagt, wo das BMK steht.
"""
# "in" | "out" | "" (Merker, unbekannt)
direction: str = ""
address: str = ""
data_type: str = Field(default="", alias="dataType")
comment: str = ""
# Index-Konstante fuer "DB_Inputs".Sensors[...] bzw. Ausgaenge
constant: str = ""
constant_value: int | None = Field(default=None, alias="constantValue")
# Foerderer der E-Planung aus Bezeichnung/Kommentar ("K0102"; "#106" -> "K0106")
conveyor: str = ""
# "Kreisel" | "Strecke" | "" - nur aus dem eigenen Klartext
conveyor_kind: str = Field(default="", alias="conveyorKind")
# "tia", "positions"
sources: list[str] = Field(default_factory=list)
@property
def in_tia(self) -> bool:
return "tia" in self.sources
class HardwareDocument(BaseModel):
"""Die Hardware-JSON ``<projekt>_hw.json``."""
model_config = ConfigDict(extra="forbid", populate_by_name=True)
schema_id: str = Field(default=HW_SCHEMA, alias="schema")
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
project: str = ""
tia_file: str = Field(default="", alias="tiaFile")
tia_sha256: str = Field(default="", alias="tiaSha256")
positions_file: str = Field(default="", alias="positionsFile")
positions_sha256: str = Field(default="", alias="positionsSha256")
devices: list[HwDevice] = Field(default_factory=list)
drives: list[Drive] = Field(default_factory=list)
# Konstanten ohne passendes Signal (z. B. Projektkonstanten)
constants: list[TiaConstant] = Field(default_factory=list)
# Gegenpruefung TIA <-> Positionsdatei (je Befund eine Zeile)
checks: list[str] = Field(default_factory=list)
# Hinweise beim Einlesen (auch die der Positionsdatei selbst)
warnings: list[str] = Field(default_factory=list)
def by_bmk(self) -> dict[str, HwDevice]:
return {d.bmk: d for d in self.devices}
def field_devices(self) -> list[HwDevice]:
"""Geraete im Feld mit Lage in der E-Zeichnung."""
return [d for d in self.devices if d.pos is not None and not d.cabinet]
def to_json(self) -> str:
return self.model_dump_json(by_alias=True, indent=2) + "\n"
@classmethod
def from_json(cls, text: str) -> HardwareDocument:
return cls.model_validate_json(text)
# ---------------------------------------------------------------------------
# xlsx lesen (ohne openpyxl)
# ---------------------------------------------------------------------------
_NS = {
"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
_T = "{%s}t" % _NS["m"]
def _column(ref: str) -> int:
"""'C12' -> 2 (Spalte, 0-basiert)."""
n = 0
for ch in re.match(r"[A-Z]+", ref).group():
n = n * 26 + ord(ch) - 64
return n - 1
def read_xlsx(path: Path) -> dict[str, list[list[str]]]:
"""Alle Blaetter einer xlsx-Datei als Zeilen von Texten ("" = leer).
Reicht fuer Tabellen-Exporte (TIA, EA-Liste): Werte, keine Formeln,
keine Formate. Leere Zeilen fallen weg.
"""
with zipfile.ZipFile(path) as z:
shared: list[str] = []
if "xl/sharedStrings.xml" in z.namelist():
root = ET.fromstring(z.read("xl/sharedStrings.xml"))
shared = ["".join(t.text or "" for t in si.iter(_T))
for si in root.findall("m:si", _NS)]
workbook = ET.fromstring(z.read("xl/workbook.xml"))
rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels"))
targets = {r.get("Id"): r.get("Target") for r in rels}
sheets: dict[str, list[list[str]]] = {}
for sheet in workbook.find("m:sheets", _NS):
target = targets[sheet.get("{%s}id" % _NS["r"])].lstrip("/")
target = target if target.startswith("xl/") else "xl/" + target
rows = []
for row in ET.fromstring(z.read(target)).iter("{%s}row" % _NS["m"]):
cells: dict[int, str] = {}
for c in row.findall("m:c", _NS):
v = c.find("m:v", _NS)
kind = c.get("t")
if kind == "s" and v is not None:
text = shared[int(v.text)]
elif kind == "inlineStr":
text = "".join(t.text or "" for t in c.iter(_T))
else:
text = v.text if v is not None and v.text is not None else ""
if text != "":
cells[_column(c.get("r"))] = text
if cells:
rows.append([cells.get(i, "") for i in range(max(cells) + 1)])
sheets[sheet.get("name")] = rows
return sheets
# ---------------------------------------------------------------------------
# TIA-Import
# ---------------------------------------------------------------------------
# Spaltenkoepfe im englischen und deutschen TIA-Export
_TAG_HEADERS = {
"name": ("Name",),
"data_type": ("Data Type", "Datentyp"),
"address": ("Logical Address", "Logische Adresse", "Adresse"),
"comment": ("Comment", "Kommentar"),
}
_CONST_HEADERS = {
"name": ("Name",),
"data_type": ("Data Type", "Datentyp"),
"value": ("Value", "Wert"),
"comment": ("Comment", "Kommentar"),
}
def _columns(header: list[str], wanted: dict[str, tuple[str, ...]]) -> dict[str, int] | None:
"""Spaltennummern nach Kopfzeile; None, wenn "name" fehlt."""
lower = [h.strip().lower() for h in header]
found = {}
for key, names in wanted.items():
for n in names:
if n.lower() in lower:
found[key] = lower.index(n.lower())
break
return found if "name" in found else None
def _cell(row: list[str], index: int | None) -> str:
return row[index].strip() if index is not None and index < len(row) else ""
def load_tia(path: Path, warnings: list[str] | None = None
) -> tuple[list[TiaTag], list[TiaConstant]]:
"""Tags und Konstanten aus einem TIA-Import (xlsx).
Die Blaetter werden an der Kopfzeile erkannt, nicht am Namen: ein Blatt
mit Adressspalte sind Tags, eines mit Wertspalte Konstanten.
"""
warnings = warnings if warnings is not None else []
tags: list[TiaTag] = []
constants: list[TiaConstant] = []
for sheet, rows in read_xlsx(path).items():
if not rows:
continue
tag_cols = _columns(rows[0], _TAG_HEADERS)
const_cols = _columns(rows[0], _CONST_HEADERS)
if tag_cols and "address" in tag_cols:
for row in rows[1:]:
name = _cell(row, tag_cols["name"])
if name:
tags.append(TiaTag(
name=name, data_type=_cell(row, tag_cols.get("data_type")),
address=_cell(row, tag_cols.get("address")),
comment=_cell(row, tag_cols.get("comment"))))
elif const_cols and "value" in const_cols:
for row in rows[1:]:
name = _cell(row, const_cols["name"])
if name:
constants.append(TiaConstant(
name=name, data_type=_cell(row, const_cols.get("data_type")),
value=_cell(row, const_cols.get("value")),
comment=_cell(row, const_cols.get("comment"))))
else:
warnings.append(f"TIA-Import: Blatt '{sheet}' weder Tags noch Konstanten "
f"- uebersprungen")
if not tags:
raise ValueError(f"{path.name}: kein Blatt mit Tags (Spalte 'Logical Address' "
f"bzw. 'Logische Adresse') gefunden")
return tags, constants
def _direction(address: str) -> str:
m = _ADDRESS.match(address.strip())
if not m:
return ""
area = m.group(1).upper()
return "in" if area in "EI" else "out" if area in "AQ" else ""
# ---------------------------------------------------------------------------
# Positionsdatei (Kabellaengen-Tool)
# ---------------------------------------------------------------------------
def _device(key: str, raw: dict, cabinet: bool) -> EDevice:
# Schluessel "BG0642@UNKNOWN" / "QA7000@A/B": vor dem @ steht das BMK.
bmk = str(raw.get("IO") or raw.get("B") or key.split("@", 1)[0]).strip()
pos = raw.get("pos")
return EDevice(
bmk=bmk,
verw=str(raw.get("VERW") or "").strip(),
bezeichnung=str(raw.get("BEZEICHNUNG") or "").strip(),
kennzeichnung=str(raw.get("KENNZEICHNUNG") or "").strip(),
pos=(float(pos[0]), float(pos[1])) if pos and len(pos) >= 2 else None,
cabinet=cabinet,
)
def load_positionsdraw(path: Path, warnings: list[str] | None = None) -> list[EDevice]:
"""E-Bauteile aus einer *_positionsdraw.json / *_positionsconv.json lesen.
Ein BMK kommt hoechstens einmal vor. Steht es doppelt da (im Schrank einmal
mit und einmal ohne Attribute), gewinnt der Eintrag mit VERW.
"""
warnings = warnings if warnings is not None else []
data = json.loads(path.read_text(encoding="utf-8"))
devices: dict[str, EDevice] = {}
for section, cabinet in (("sensors", False), ("schaltschrank_elemente", True)):
for key, raw in (data.get(section) or {}).items():
if not isinstance(raw, dict):
continue
dev = _device(key, raw, cabinet)
if not dev.bmk:
continue
old = devices.get(dev.bmk)
if old is None or (not old.verw and dev.verw):
devices[dev.bmk] = dev
for bmk, positions in (data.get("double_ids") or {}).items():
warnings.append(f"E-Planung: {bmk} steht {len(positions)}x in der Zeichnung "
f"- verwendet wird ein Eintrag")
return list(devices.values())
def _positions_warnings(path: Path) -> list[str]:
"""Die Hinweise, die das Kabellaengen-Tool selbst in die Datei schreibt."""
data = json.loads(path.read_text(encoding="utf-8"))
out = []
for kind, entries in (data.get("warnings") or {}).items():
if isinstance(entries, dict):
for key, text in entries.items():
out.append(f"Positionsdatei ({kind}): {key.split('@', 1)[0]}: "
f"{str(text).split(':', 1)[0]}")
return out
# ---------------------------------------------------------------------------
# Zusammenfuehren
# ---------------------------------------------------------------------------
def conveyor_of(*texts: str) -> str:
"""Foerderer der E-Planung aus einem Klartext als "K<NNNN>", sonst "".
Die E-Planung schreibt denselben Foerderer zweifach: "CONVEYOR-K0103"
(Motorschutz FC0203) und "CONVEYOR-#103" (Motor MA0203). Beides ist die
vierstellige Foerderernummer des SCL-Namensschemas (DB_JamArea0103,
fbCarousel0102) und wird zu "K0103".
"""
for text in texts:
upper = (text or "").upper()
m = _CONVEYOR_K.search(upper)
if m:
return f"K{m.group(1)}"
m = _CONVEYOR_HASH.search(upper)
if m:
return f"K{int(m.group(1)):04d}"
return ""
def conveyor_kind_of(*texts: str) -> str:
"""Art des genannten Foerderers aus einem Klartext: "Kreisel", "Strecke" oder ""."""
for text in texts:
upper = (text or "").upper()
if _KIND_KREISEL.search(upper):
return "Kreisel"
if _KIND_STRECKE.search(upper):
return "Strecke"
return ""
def pair_drives(devices: list[EDevice]) -> list[Drive]:
"""Motor MAnnnn <-> Motorschutz FCnnnn (gleiche Nummer), Geber GF je Foerderer."""
fc = {d.number: d for d in devices if d.prefix.upper() == "FC" and d.number is not None}
encoders: dict[str, str] = {}
for d in sorted(devices, key=lambda d: d.bmk):
if d.prefix.upper() == "GF":
conv = getattr(d, "conveyor", "") or conveyor_of(d.bezeichnung)
if conv:
encoders.setdefault(conv, d.bmk)
drives = []
for d in sorted(devices, key=lambda d: d.bmk):
if d.prefix.upper() != "MA" or d.number is None:
continue
schutz = fc.get(d.number)
conv = (getattr(d, "conveyor", "") or conveyor_of(d.bezeichnung)
or (conveyor_of(schutz.bezeichnung) if schutz else ""))
drives.append(Drive(
motor=d.bmk,
protection=schutz.bmk if schutz else "",
bezeichnung=d.bezeichnung,
protection_bezeichnung=schutz.bezeichnung if schutz else "",
conveyor=conv,
conveyor_kind=conveyor_kind_of(d.bezeichnung,
schutz.bezeichnung if schutz else ""),
encoder=encoders.get(conv, "") if conv else "",
))
return drives
def _constant_name(bmk: str, direction: str) -> str:
return f"cIn{bmk}" if direction == "in" else f"cOut{bmk}" if direction == "out" else ""
def build_hardware(tia_path: Path | None, positions_path: Path | None,
project: str = "") -> HardwareDocument:
"""Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen."""
if tia_path is None and positions_path is None:
raise ValueError("weder TIA-Import noch Positionsdatei angegeben")
warnings: list[str] = []
checks: list[str] = []
tags: list[TiaTag] = []
constants: list[TiaConstant] = []
if tia_path is not None:
tags, constants = load_tia(tia_path, warnings)
e_devices: list[EDevice] = []
if positions_path is not None:
e_devices = load_positionsdraw(positions_path, warnings)
warnings.extend(_positions_warnings(positions_path))
tag_of = {}
for t in tags:
if t.name in tag_of:
checks.append(f"TIA: Tag {t.name} doppelt")
tag_of[t.name] = t
const_of = {c.name: c for c in constants}
e_of = {d.bmk: d for d in e_devices}
devices: list[HwDevice] = []
used_constants: set[str] = set()
for bmk in sorted(set(tag_of) | set(e_of)):
tag, e = tag_of.get(bmk), e_of.get(bmk)
direction = _direction(tag.address) if tag else ""
cname = _constant_name(bmk, direction)
const = const_of.get(cname)
if const:
used_constants.add(cname)
value = None
if const is not None:
try:
value = int(const.value)
except ValueError:
checks.append(f"TIA: Konstante {cname} hat keinen ganzzahligen Wert "
f"'{const.value}'")
base = e.model_dump() if e else {"bmk": bmk}
devices.append(HwDevice(
**base,
direction=direction,
address=tag.address if tag else "",
data_type=tag.data_type if tag else "",
comment=tag.comment if tag else "",
constant=cname if const else "",
constant_value=value,
conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else ""),
conveyor_kind=conveyor_kind_of(e.bezeichnung if e else "",
tag.comment if tag else ""),
sources=[s for s, ok in (("tia", tag is not None), ("positions", e is not None))
if ok],
))
_check(devices, constants, used_constants, tags, e_devices, tia_path, positions_path,
checks)
return HardwareDocument(
project=project,
tia_file=tia_path.name if tia_path else "",
tia_sha256=file_fingerprint(tia_path) if tia_path else "",
positions_file=positions_path.name if positions_path else "",
positions_sha256=file_fingerprint(positions_path) if positions_path else "",
devices=devices,
drives=pair_drives(devices),
constants=[c for c in constants if c.name not in used_constants],
checks=checks,
warnings=warnings,
)
def _check(devices: list[HwDevice], constants: list[TiaConstant], used_constants: set[str],
tags: list[TiaTag], e_devices: list[EDevice], tia_path: Path | None,
positions_path: Path | None, checks: list[str]) -> None:
"""Gegenpruefung TIA-Import <-> Positionsdatei (Ergebnis in checks)."""
if tia_path is not None and positions_path is not None:
only_e = [d.bmk for d in devices if d.sources == ["positions"]]
if only_e:
checks.append(f"nur in der Positionsdatei, nicht im TIA-Import "
f"({len(only_e)}): {', '.join(only_e)}")
# Feldgeraete (Sensoren/Ventile/Motoren) gehoeren in die Zeichnung;
# Taster, Leuchten, Not-Halt (SF/PF) stehen dort meist nicht.
only_tia = [d.bmk for d in devices if d.sources == ["tia"]
and d.prefix.upper() in ("BG", "MB", "MA", "BX")]
if only_tia:
checks.append(f"Feldgeraet nur im TIA-Import, nicht in der Positionsdatei "
f"({len(only_tia)}): {', '.join(only_tia)}")
differ = [d.bmk for d in devices if len(d.sources) == 2 and d.bezeichnung
and d.comment and d.bezeichnung.strip() != d.comment.strip()]
if differ:
checks.append(f"TIA-Kommentar weicht von der BEZEICHNUNG ab ({len(differ)}): "
f"{', '.join(differ)}")
if tia_path is not None:
no_const = [d.bmk for d in devices if d.in_tia and d.direction and not d.constant]
if no_const:
checks.append(f"Signal ohne Index-Konstante cIn/cOut ({len(no_const)}): "
f"{', '.join(no_const)}")
orphan = [c.name for c in constants
if c.name not in used_constants and re.match(r"^c(In|Out)[A-Z]+\d+$", c.name)]
if orphan:
checks.append(f"Index-Konstante ohne Signal ({len(orphan)}): {', '.join(orphan)}")
addresses = Counter(t.address for t in tags if t.address)
double = [a for a, n in addresses.items() if n > 1]
if double:
checks.append(f"Adresse mehrfach vergeben: {', '.join(sorted(double))}")
for direction in ("in", "out"):
values = Counter(d.constant_value for d in devices
if d.direction == direction and d.constant_value is not None)
double = sorted(v for v, n in values.items() if n > 1)
if double:
checks.append(f"Index-Konstanten ({direction}) mit gleichem Wert: "
f"{', '.join(map(str, double))}")
no_ctrl = [d.bmk for d in e_devices if not d.cabinet and not d.controller]
if no_ctrl:
checks.append(f"Feldgeraet ohne Steuerung in KENNZEICHNUNG ({len(no_ctrl)}): "
f"{', '.join(no_ctrl)}")
# ---------------------------------------------------------------------------
# Bericht
# ---------------------------------------------------------------------------
def render_report(doc: HardwareDocument) -> str:
out: list[str] = []
add = out.append
add(f"# Hardwareplanung: {doc.project}")
add("")
add(f"- TIA-Import: `{doc.tia_file or '-'}`")
add(f"- Positionsdatei: `{doc.positions_file or '-'}`")
add(f"- Erzeugt: {doc.created}")
add("")
add("## Geraete")
add("")
prefixes = Counter(d.prefix or "?" for d in doc.devices)
add("| Praefix | Anzahl | im Feld mit Lage | mit Index-Konstante |")
add("|---|---|---|---|")
for p, n in sorted(prefixes.items()):
devs = [d for d in doc.devices if (d.prefix or "?") == p]
add(f"| {p} | {n} | {sum(1 for d in devs if d.pos and not d.cabinet)} "
f"| {sum(1 for d in devs if d.constant)} |")
add("")
add("## Antriebe (Motor <-> Motorschutz, Geber)")
add("")
if doc.drives:
add("| Foerderer | Motor | Bezeichnung | Motorschutz | Geber |")
add("|---|---|---|---|---|")
for dr in doc.drives:
add(f"| {dr.conveyor or '-'} | {dr.motor} | {dr.bezeichnung} "
f"| {dr.protection or '-'} | {dr.encoder or '-'} |")
else:
add("- keine Motoren")
add("")
conveyors = Counter(d.conveyor for d in doc.field_devices() if d.conveyor)
add("## Foerderer der E-Planung (Feldgeraete, die ihn im Klartext nennen)")
add("")
if conveyors:
for name, n in sorted(conveyors.items()):
add(f"- {name}: {n}")
else:
add("- keine")
add("")
add("## Gegenpruefung")
add("")
for c in doc.checks or ["keine Befunde"]:
add(f"- {c}")
add("")
if doc.constants:
add("## Konstanten ohne Signal")
add("")
add(", ".join(c.name for c in doc.constants))
add("")
if doc.warnings:
add("## Hinweise")
add("")
for w in doc.warnings:
add(f"- {w}")
add("")
return "\n".join(out)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
_POSITION_SUFFIXES = ("_positionsconv", "_positionsdraw", "_positions")
def project_name(tia_path: Path | None, positions_path: Path | None) -> str:
"""Projektname aus den Dateinamen: Mubea_positionsconv.json -> Mubea."""
if positions_path is not None:
stem = positions_path.stem
for suffix in _POSITION_SUFFIXES:
if stem.lower().endswith(suffix):
return stem[: -len(suffix)]
return stem
stem = tia_path.stem if tia_path else "anlage"
return stem[:-4] if stem.lower().endswith("_tia") else stem
def main(argv: list[str] | None = None) -> int:
from material_flow import env_dir, resolve_input
args = parse_args(argv)
tia_path = resolve_input(args.tia) if args.tia else None
positions_path = resolve_input(args.positions) if args.positions else None
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path)):
if path is not None and not path.is_file():
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
return 1
project = args.project or project_name(tia_path, positions_path)
try:
doc = build_hardware(tia_path, positions_path, project)
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError) as exc:
print(f"FEHLER: {exc}", file=sys.stderr)
return 1
results = env_dir("SKEL_RESULTS", "results")
results.mkdir(parents=True, exist_ok=True)
json_file = results / f"{project}_hw.json"
md_file = results / f"{project}_hw.md"
json_file.write_text(doc.to_json(), encoding="utf-8")
md_file.write_text(render_report(doc), encoding="utf-8")
n_tia = sum(1 for d in doc.devices if d.in_tia)
n_pos = sum(1 for d in doc.devices if "positions" in d.sources)
print("")
print("================================================================")
print("HARDWAREPLANUNG EINLESEN")
print("================================================================")
print(f"TIA-Import = {tia_path or '-'}")
print(f"Positionsdatei = {positions_path or '-'}")
print(f"Projekt = {project}")
print(f"Geraete = {len(doc.devices)} (TIA {n_tia}, Positionsdatei {n_pos}, "
f"beide {sum(1 for d in doc.devices if len(d.sources) == 2)})")
print(f"Feld mit Lage = {len(doc.field_devices())}")
print(f"Index-Konstanten = {sum(1 for d in doc.devices if d.constant)} "
f"(+ {len(doc.constants)} ohne Signal)")
print(f"Antriebe = {len(doc.drives)} "
f"({sum(1 for d in doc.drives if d.protection)} mit Motorschutz, "
f"{sum(1 for d in doc.drives if d.conveyor)} mit Foerderer)")
for c in doc.checks:
print(f" ? {c}")
for w in doc.warnings:
print(f" ! {w}")
print(f"JSON = {json_file}")
print(f"Bericht = {md_file}")
print("================================================================")
print("")
return 0
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="hw_import",
description="Liest die Hardwareplanung ein - TIA-Import (PLC Tags + "
"Constants) und die Positionsdatei des Kabellaengen-Tools - "
"und schreibt <projekt>_hw.json fuer merge_hw_layout plus einen "
"Bericht <projekt>_hw.md.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Beispiele:\n"
" hw_import.bat --tia Mubea/mubea-1_TIA.xlsx "
"--positions Mubea/Mubea_positionsconv.json\n"
" hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea\n"
"\n"
"Exit-Codes:\n"
" 0 ok (Befunde der Gegenpruefung stehen im Bericht)\n"
" 1 Eingabe- oder Lesefehler\n",
)
parser.add_argument("--tia", required=True, metavar="NAME",
help="TIA-Import *_TIA.xlsx in %%SKEL_DATA%% (oder ein Pfad, "
"auch mit Unterordner)")
parser.add_argument("--positions", metavar="NAME",
help="*_positionsconv.json bzw. *_positionsdraw.json in "
"%%SKEL_DATA%% (oder ein Pfad); ohne sie keine Lage")
parser.add_argument("--project", metavar="NAME",
help="Projektname fuer <projekt>_hw.json. Standard: aus dem "
"Namen der Positionsdatei (Mubea_positionsconv.json -> Mubea)")
return parser.parse_args(argv)
if __name__ == "__main__":
sys.exit(main())
+391
View File
@@ -0,0 +1,391 @@
# -*- coding: utf-8 -*-
"""
kreisel_junctions.py - Weichenknoten zwischen Kreiseln aus der Geometrie.
Zwei Kreisel, die sich beruehren, bilden eine Weichenstelle: am Beruehrpunkt
laufen beide Umlaeufe in dieselbe Richtung, ein Traeger kann dort von einem
Kreisel auf den anderen wechseln. dxfmakros exportiert solche Stellen als
"ILS Weiche" (Merkmal "Erzeugt aus: Kreisel-Beruehrpunkt") - aber nicht
immer: in data/export.csv fehlen die Weichen zwischen Kreisel 9/8, 2/3 und
3/4, obwohl sich die Kreisel dort beruehren. Darum wird hier jede Weiche aus
der Kreisel-Geometrie bestimmt und eine exportierte ILS Weiche nur noch ihrem
Beruehrpunkt zugeordnet (Id und Name).
Geometrie
---------
Ein Kreisel ist eine Stadion-Form: zwei Umlenkungen (Halbkreise vom Radius
r) im Abstand L, verbunden durch die beiden geraden Bahnen. Mittelpunkt ist
die CSV-Position, die Laengsachse zeigt in Richtung "Drehung". r und L kommen
aus der Boundingbox (kurze Seite = 2r, lange Seite = L + 2r), bei schraeger
Lage aus dem Merkmal "Abstand (Kreiselachse A - Kreiselachse) in Meter".
Jeder Punkt auf dem Umlauf hat eine Laufposition s (mm) in Drehrichtung
("Drehrichtung" UZS/GUZ) - damit ist "stromauf"/"stromab" definiert.
Weichenknoten und steuernde Separatoren
---------------------------------------
Beruehrpunkte, die auf derselben Umlenkung eines Kreisels liegen, bilden
EINEN Weichenknoten (Kreisel 5 beruehrt mit seiner Umlenkung Kreisel 6 und
Kreisel 4 - zwei Weichen, eine Uebergabestelle). Ein Separator steuert den
Knoten, wenn er auf einem der beteiligten Kreisel unmittelbar stromauf vor
dem Knoten liegt: vom Knoten gegen die Drehrichtung zurueckgegangen, kommt
zuerst dieser Separator und keine andere Weiche. Liegt dazwischen eine
fremde Weiche, steuert der Separator diese, nicht den Knoten.
Aus der Anzahl steuernder Separatoren und Weichen ergibt sich der TRO-Typ
(tro_flow.derive_tros, Schritt 3c): 1Sep1Swi, 2Sep1Swi, 2Sep2Swi, 3Sep2Swi.
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from material_flow import (
LANE_LEFT,
LANE_RIGHT,
ROTATION_CCW,
ROTATION_CW,
Element,
_as_float,
_merkmal,
)
from parameter import get_float, get_int
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [kreisel_junctions] - Bedeutung
# und Einheit stehen dort (lib/parameter.py).
_P = "kreisel_junctions"
CONTACT_TOLERANCE = get_float(_P, "contact_tolerance")
SEPARATOR_TOLERANCE = get_float(_P, "separator_tolerance")
WEICHE_TOLERANCE = get_float(_P, "weiche_tolerance")
SAMPLE_STEP = get_float(_P, "sample_step")
MIN_SAMPLES = get_int(_P, "min_samples")
CONTACT_TIE = get_float(_P, "contact_tie")
MAX_CONTACT_LENGTH = get_float(_P, "max_contact_length")
END_TOLERANCE = get_float(_P, "end_tolerance")
DEFAULT_RADIUS = get_float(_P, "default_radius")
DIRECTION_STEP = get_float(_P, "direction_step")
GLEICHLAUF_MIN_COS = get_float(_P, "gleichlauf_min_cos")
# Abschnitte eines Umlaufs: die beiden Umlenkungen und die beiden Geraden.
END_PLUS = "+" # Umlenkung in Achsrichtung
END_MINUS = "-" # Umlenkung gegen die Achsrichtung
STRAIGHT = "gerade"
@dataclass
class Loop:
"""Umlauf eines Kreisels als Stadion-Form mit Laufrichtung.
Lokale Koordinaten: a entlang der Achse ("Drehung"), b quer dazu (links
positiv). Die Umlenkungen liegen bei a = +half und a = -half.
"""
kreisel: Element
cx: float
cy: float
ux: float # Achsrichtung (Einheitsvektor)
uy: float
half: float # halber Umlenkungsabstand L/2 (mm)
radius: float # Bahnabstand / 2 (mm)
clockwise: bool # True = UZS
@property
def length(self) -> float:
"""Umfang des Umlaufs (mm)."""
return 4 * self.half + 2 * math.pi * self.radius
def _local(self, x: float, y: float) -> tuple[float, float]:
"""Weltpunkt -> lokale Koordinaten (a, b)."""
dx, dy = x - self.cx, y - self.cy
return dx * self.ux + dy * self.uy, -dx * self.uy + dy * self.ux
def lane(self, x: float, y: float) -> str:
"""Bahn eines Punkts: LANE_LEFT links der Achse, sonst LANE_RIGHT
(dieselbe Konvention wie tro_flow._kreisel_bahn)."""
_a, b = self._local(x, y)
return LANE_LEFT if b > 0 else LANE_RIGHT
def project(self, x: float, y: float) -> tuple[float, float, str]:
"""Naechster Punkt auf dem Umlauf.
Parameter:
x, y: Weltpunkt (mm).
Rueckgabe:
(s, Abstand, Abschnitt): s = Laufposition in Drehrichtung
(0 <= s < length), Abstand des Punkts vom Umlauf (mm), Abschnitt
END_PLUS, END_MINUS oder STRAIGHT.
"""
a, b = self._local(x, y)
h, r = self.half, self.radius
cands: list[tuple[float, float, str]] = []
# Gegen den Uhrzeigersinn (lokal): untere Gerade (b=-r) in +a, rechte
# Umlenkung, obere Gerade (b=+r) in -a, linke Umlenkung.
if -h <= a <= h:
cands.append((abs(b + r), a + h, STRAIGHT))
cands.append((abs(b - r), 2 * h + math.pi * r + (h - a), STRAIGHT))
if a >= h:
theta = math.atan2(b, a - h) # -pi/2 .. pi/2
cands.append((abs(math.hypot(a - h, b) - r),
2 * h + r * (theta + math.pi / 2), END_PLUS))
if a <= -h:
phi = math.atan2(b, a + h) % (2 * math.pi) # pi/2 .. 3pi/2
cands.append((abs(math.hypot(a + h, b) - r),
4 * h + math.pi * r + r * (phi - math.pi / 2), END_MINUS))
if not cands: # Innenpunkt nahe Achse
cands.append((abs(b + r), a + h, STRAIGHT))
dist, s_ccw, section = min(cands)
if section == STRAIGHT and a >= h - END_TOLERANCE:
section = END_PLUS
elif section == STRAIGHT and a <= -h + END_TOLERANCE:
section = END_MINUS
s = (self.length - s_ccw) % self.length if self.clockwise else s_ccw % self.length
return s, dist, section
def point(self, s: float) -> tuple[float, float]:
"""Weltpunkt zur Laufposition s (Umkehrung von project)."""
s_ccw = (self.length - s) % self.length if self.clockwise else s % self.length
h, r = self.half, self.radius
if s_ccw < 2 * h:
a, b = s_ccw - h, -r
elif s_ccw < 2 * h + math.pi * r:
theta = (s_ccw - 2 * h) / r - math.pi / 2
a, b = h + r * math.cos(theta), r * math.sin(theta)
elif s_ccw < 4 * h + math.pi * r:
a, b = h - (s_ccw - 2 * h - math.pi * r), r
else:
phi = (s_ccw - 4 * h - math.pi * r) / r + math.pi / 2
a, b = -h + r * math.cos(phi), r * math.sin(phi)
return (self.cx + a * self.ux - b * self.uy, self.cy + a * self.uy + b * self.ux)
def direction(self, s: float) -> tuple[float, float]:
"""Laufrichtung (Einheitsvektor) an der Laufposition s."""
x0, y0 = self.point(s - DIRECTION_STEP)
x1, y1 = self.point(s + DIRECTION_STEP)
n = math.hypot(x1 - x0, y1 - y0) or 1.0
return (x1 - x0) / n, (y1 - y0) / n
def kreisel_loop(kreisel: Element) -> Loop | None:
"""Umlauf eines Kreisels aus Position, Drehung, Boundingbox, Drehrichtung.
Parameter:
kreisel: Element mit kind "Kreisel".
Rueckgabe:
Loop oder None, wenn Position, Drehrichtung oder Abmessungen fehlen.
"""
if kreisel.x is None or kreisel.y is None:
return None
if kreisel.rotation not in (ROTATION_CW, ROTATION_CCW):
return None
drehung = _as_float(_merkmal(kreisel.merkmale, "Drehung")) or 0.0
rad = math.radians(drehung)
bw, bh = kreisel.bbox_x, kreisel.bbox_y
if bw and bh and abs(drehung) % 90 < 1e-6:
short, long_ = min(bw, bh), max(bw, bh)
radius, half = short / 2, (long_ - short) / 2
else:
abstand = _as_float(_merkmal(
kreisel.merkmale, "Abstand (Kreiselachse A - Kreiselachse) in Meter"))
if abstand is None:
return None
radius, half = DEFAULT_RADIUS, abstand * 1000 / 2
return Loop(kreisel, kreisel.x, kreisel.y, math.cos(rad), math.sin(rad),
half, radius, kreisel.rotation == ROTATION_CW)
@dataclass
class Contact:
"""Beruehrpunkt zweier Kreisel = eine Weiche."""
weiche_id: str # TeileId der ILS Weiche oder "W<a>_<b>"
kreisel: tuple[str, str] # beide Kreisel-Ids, sortiert
x: float
y: float
exported: bool # True = als ILS Weiche im CSV
s: dict[str, float] = field(default_factory=dict) # Kreisel -> s
section: dict[str, str] = field(default_factory=dict) # Kreisel -> Abschnitt
@dataclass
class Junction:
"""Weichenknoten: Beruehrpunkte auf derselben Umlenkung, plus Steuerung."""
contacts: list[Contact]
kreisel: list[str] # beteiligte Kreisel
separators: list[str] = field(default_factory=list) # steuernde Separatoren
sep_kreisel: dict[str, str] = field(default_factory=dict) # Separator -> Kreisel
sep_lane: dict[str, str] = field(default_factory=dict) # Separator -> "<Id>-L/R"
@property
def type_name(self) -> str:
"""TRO-Typ aus Separator- und Weichenzahl, z. B. "2Sep1Swi"."""
return f"{len(self.separators)}Sep{len(self.contacts)}Swi"
def _find_contact(a: Loop, b: Loop) -> tuple[float, float, float] | None:
"""Beruehrpunkt (x, y, Laenge des Beruehrbereichs) oder None."""
steps = max(int(a.length / SAMPLE_STEP), MIN_SAMPLES)
near: list[tuple[float, float, float]] = []
for i in range(steps):
x, y = a.point(i * a.length / steps)
_s, dist, _sec = b.project(x, y)
if dist <= CONTACT_TOLERANCE:
near.append((dist, x, y))
if not near:
return None
# Bei tangentialer Beruehrung sind viele Abtastpunkte fast gleich nah -
# der Mittelpunkt der naechsten liegt am eigentlichen Beruehrpunkt.
best = min(d for d, _x, _y in near)
mitte = [(x, y) for d, x, y in near if d <= best + CONTACT_TIE]
x = sum(p[0] for p in mitte) / len(mitte)
y = sum(p[1] for p in mitte) / len(mitte)
return x, y, len(near) * a.length / steps
def find_contacts(elements: list[Element], findings: list[str]) -> tuple[dict[str, Loop], list[Contact]]:
"""Alle Beruehrpunkte zwischen Kreiseln, mit Zuordnung der ILS Weichen.
Parameter:
elements: alle Elemente des CSV-Exports (Kreisel, ILS Weichen).
findings: Fundliste; Hinweise zu fehlenden/gegenlaeufigen Weichen.
Rueckgabe:
(Kreisel-Id -> Loop, Beruehrpunkte). Ein Beruehrpunkt ohne
exportierte ILS Weiche bekommt die Id "W<KreiselA>_<KreiselB>".
"""
loops = {e.teile_id: lp for e in elements if e.kind == "Kreisel"
and (lp := kreisel_loop(e)) is not None}
weichen = [e for e in elements if e.kind == "Weiche"]
ids = sorted(loops)
contacts: list[Contact] = []
for i, ka in enumerate(ids):
for kb in ids[i + 1:]:
hit = _find_contact(loops[ka], loops[kb])
if hit is None:
continue
x, y, laenge = hit
if laenge > MAX_CONTACT_LENGTH:
findings.append(
f"Kreisel {ka}/{kb}: beruehren sich auf {laenge:.0f}mm Laenge "
f"(parallele Bahnen?) - keine Weiche angenommen")
continue
c = Contact("", (ka, kb), x, y, False)
for k in (ka, kb):
c.s[k], _d, c.section[k] = loops[k].project(x, y)
da = loops[ka].direction(c.s[ka])
db = loops[kb].direction(c.s[kb])
if da[0] * db[0] + da[1] * db[1] < GLEICHLAUF_MIN_COS:
findings.append(
f"Kreisel {ka}/{kb}: Beruehrpunkt bei {x:.0f}, {y:.0f} laeuft "
f"gegenlaeufig (Drehrichtung pruefen) - kein Weichenknoten")
continue
contacts.append(c)
for c in contacts:
passend = [w for w in weichen if w.x is not None and w.y is not None
and math.hypot(w.x - c.x, w.y - c.y) <= WEICHE_TOLERANCE]
if passend:
w = min(passend, key=lambda w: math.hypot(w.x - c.x, w.y - c.y))
c.weiche_id, c.exported = w.teile_id, True
else:
c.weiche_id = f"W{c.kreisel[0]}_{c.kreisel[1]}"
findings.append(
f"Kreisel {c.kreisel[0]}/{c.kreisel[1]}: Beruehrpunkt bei "
f"{c.x:.0f}, {c.y:.0f} ohne ILS Weiche im Export - als Weiche "
f"{c.weiche_id} angenommen")
# ILS Weichen ohne passenden Beruehrpunkt (z. B. an gegenlaeufigen
# Kreiseln) bleiben unberuehrt - sie wertet tro_flow wie bisher ueber ihre
# CSV-Nachbarn aus (_find_weiche_separators).
return loops, contacts
def _group(contacts: list[Contact]) -> list[list[Contact]]:
"""Beruehrpunkte auf derselben Umlenkung eines Kreisels zusammenfassen."""
parent = list(range(len(contacts)))
def root(i: int) -> int:
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
seen: dict[tuple[str, str], int] = {}
for i, c in enumerate(contacts):
for k, section in c.section.items():
if section == STRAIGHT:
continue
j = seen.setdefault((k, section), i)
parent[root(i)] = root(j)
groups: dict[int, list[Contact]] = {}
for i, c in enumerate(contacts):
groups.setdefault(root(i), []).append(c)
return list(groups.values())
def find_junctions(
elements: list[Element],
findings: list[str],
separators: list[Element] | None = None,
) -> list[Junction]:
"""Weichenknoten zwischen Kreiseln mit ihren steuernden Separatoren.
Parameter:
elements: alle Elemente des CSV-Exports.
findings: Fundliste (siehe find_contacts).
separators: Separatoren, die steuern duerfen; None = alle.
Rueckgabe:
Weichenknoten, sortiert nach erster Weichen-Id. Knoten ohne
steuernden Separator sind enthalten (separators leer) - der Aufrufer
entscheidet, was damit geschieht.
"""
loops, contacts = find_contacts(elements, findings)
if separators is None:
separators = [e for e in elements if e.kind == "Separator"]
# Separator -> (Kreisel, s): auf dem naechstgelegenen Umlauf
sep_pos: dict[str, tuple[str, float]] = {}
sep_lane: dict[str, str] = {}
for sep in separators:
if sep.x is None or sep.y is None:
continue
best: tuple[float, str, float] | None = None
for k, lp in loops.items():
s, dist, _sec = lp.project(sep.x, sep.y)
if dist <= SEPARATOR_TOLERANCE and (best is None or dist < best[0]):
best = (dist, k, s)
if best is not None:
sep_pos[sep.teile_id] = (best[1], best[2])
sep_lane[sep.teile_id] = f"{best[1]}-{loops[best[1]].lane(sep.x, sep.y)}"
junctions: list[Junction] = []
for group in _group(contacts):
kreisel = sorted({k for c in group for k in c.kreisel})
j = Junction(sorted(group, key=lambda c: c.weiche_id), kreisel)
in_group = {id(c) for c in group}
for k in kreisel:
# Ereignisse auf Kreisel k: (s, Art, Id); Art "sep" oder "swi"
events = [(s, "sep", sid) for sid, (kk, s) in sep_pos.items() if kk == k]
events += [(c.s[k], "own" if id(c) in in_group else "swi", c.weiche_id)
for c in contacts if k in c.s]
events.sort()
n = len(events)
for idx, (_s, art, _id) in enumerate(events):
if art != "own":
continue
# gegen die Laufrichtung zurueck bis zum ersten fremden Ereignis
for step in range(1, n):
_ps, part, pid = events[(idx - step) % n]
if part == "own":
continue
if part == "sep" and pid not in j.separators:
j.separators.append(pid)
j.sep_kreisel[pid] = k
j.sep_lane[pid] = sep_lane[pid]
break
j.separators.sort()
junctions.append(j)
junctions.sort(key=lambda j: j.contacts[0].weiche_id)
return junctions
+914 -103
View File
File diff suppressed because it is too large. Load diff
+969
View File
@@ -0,0 +1,969 @@
# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
merge_hw_layout.py - Layout und Hardwareplanung zusammenfuehren -> <csv>_anlage.json.
Stufe 3 des Ablaufs in doc/Konzept_Layout_Hardware_SCL.md:
<csv>_tro.json (tro_flow --json) + <projekt>_hw.json (hw_import)
\\ /
merge_hw_layout -> <csv>_anlage.json + Bericht .md
Das CSV wird nicht gelesen: die TRO-JSON traegt die Slots der TROs und die
Lage der Layout-Bauteile (``parts``). Ablauf:
1. Lagebezug E-Zeichnung -> Layout (lib/dxf_registration.py) ueber die
Separator-Sensoren (VERW "IN SEP") gegen die Separatoren des Layouts;
abgelegt in SKEL_CFG/dxf_registration.json mit Pruefsumme der
Positionsdatei - ein neuer Stand gleichen Namens wird neu ermittelt.
2. Regeln aus SKEL_CFG/e_planning.ini (raeumlich mit Hoechst-/Sollabstand,
Nummernregel MB = BG + 1) ordnen E-Bauteile Layout-Bauteilen zu.
3. Jede Zuordnung geht an die Slot-Rolle des TRO, der das Bauteil traegt
(Schluessel TRO-ID + Slot + Rolle). Bauteile ohne Slot (Ausschleus-
elemente, Weichen) bleiben als ``ohne_slot`` sichtbar.
4. Foerderer: die Namen der E-Planung (K0102) und des Layouts (Kreisel3)
passen nicht zusammen. Alle Feldgeraete, deren Klartext einen Foerderer
nennt, stimmen fuer den naechsten Layout-Foerderer ab; die Mehrheit
entscheidet (cfg/parameter.ini [merge_hw_layout]). Daraus Antrieb, Motorschutz
und Geber je Layout-Foerderer und die Antriebs-Slots der Vario-TROs.
5. Handkorrekturen aus SKEL_CFG/hw_overrides.ini, zuletzt.
6. Pflichtrollen ohne BMK werden ``offen`` (spaeter TODO(E-Planung)), ein
BMK, das nicht im TIA-Import steht, ``widerspruch``.
Lieber eine Luecke als ein falscher Name: was keine Regel sicher trifft,
bleibt offen und steht im Bericht.
Die Zuordnungslogik (Regeln, raeumlich, Nummernregel) stand bis 2026-10-15
in e_planning.py; e_planning importiert sie jetzt von hier, damit
``scl_skeleton --eplan`` bis zur Umstellung auf die Anlagen-JSON weiterlaeuft.
Aufruf ueber bin/merge_hw_layout.bat bzw. bin/merge_hw_layout.sh.
"""
from __future__ import annotations
import argparse
import configparser
import fnmatch
import math
import sys
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from pydantic import BaseModel, ConfigDict, ValidationError
from dxf_registration import (
RegistrationError,
Transform,
derive_transform,
load_transform,
save_transform,
verify_transform,
)
from hw_import import HW_SCHEMA, EDevice, HardwareDocument, HwDevice
from model import (
AnlageDocument,
Binding,
ConveyorBinding,
LayoutPart,
Registration,
TroDocument,
)
from parameter import get_float, get_int
__all__ = [
"RULES_FILE",
"OVERRIDES_FILE",
"LAYOUT_KINDS",
"CONVEYOR_KINDS",
"LayoutPoint",
"Rule",
"Assignment",
"load_rules",
"assign",
"resolve_transform",
"points_from_parts",
"bind_slots",
"assign_conveyors",
"load_overrides",
"build_anlage",
]
RULES_FILE = "e_planning.ini"
OVERRIDES_FILE = "hw_overrides.ini"
# Zahlenwerte aus cfg/parameter.ini [merge_hw_layout] - Bedeutung und Einheit stehen dort.
_P = "merge_hw_layout"
DEFAULT_TOLERANCE_MM = get_float(_P, "toleranz_mm")
CONVEYOR_MAX_MM = get_float(_P, "conveyor_max_mm")
CONVEYOR_MIN_VOTES = get_int(_P, "conveyor_min_votes")
CONVEYOR_MIN_SHARE = get_float(_P, "conveyor_min_share")
REGISTRATION_MIN_SHARE = get_float(_P, "registration_min_share")
_CONFIG = ConfigDict(extra="forbid")
# Punktartige Layout-Bauteile (Element.kind bzw. Teil der TeileArt)
LAYOUT_KINDS = ("Separator", "Scanner", "Weiche", "Ausschleus", "Einschleus",
"Beladung", "Entladung")
# Foerderer, die fuer einen Foerderer der E-Planung in Frage kommen
CONVEYOR_KINDS = ("Kreisel", "Strecke", "Gefaellestrecke")
# Antriebs-Rollen eines Slots, die aus dem zugeordneten Foerderer folgen
_DRIVE_ROLES = {"motor": "motor", "motorProtection": "protection"}
# ---------------------------------------------------------------------------
# Regeln und Zuordnung (bis 2026-10-15 in e_planning.py)
# ---------------------------------------------------------------------------
@dataclass
class LayoutPoint:
"""Ein punktartiges Bauteil des Layouts in Layout-Koordinaten."""
layout_id: str
kind: str
x: float
y: float
name: str = ""
@dataclass
class Rule:
"""Eine Zuordnungsregel aus e_planning.ini (Abschnitt "<Art>.<Rolle>")."""
role: str # "Separator.sensor"
layout: list[str] # Muster fuer die Layout-Art
verw: list[str] # Muster fuer VERW
max_mm: float | None = None # raeumlich: Hoechstabstand
number_from: str = "" # Nummernregel: Rolle des Partners ...
number_offset: int = 0 # ... und Versatz der Nummer
prefix: str = "" # BMK-Praefix, leer = egal
# Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil.
# Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand.
target_mm: float | None = None
tolerance_mm: float = DEFAULT_TOLERANCE_MM
@property
def slot_role(self) -> str:
"""Rolle im Slot: "Separator.sensor" -> "sensor"."""
return self.role.split(".", 1)[-1]
def score(self, dist: float) -> float | None:
"""Rangfolge eines Kandidaten (kleiner = besser), None = nicht zulaessig."""
if self.max_mm is not None and dist > self.max_mm:
return None
if self.target_mm is None:
return dist
dev = abs(dist - self.target_mm)
return dev if dev <= self.tolerance_mm else None
class Assignment(BaseModel):
"""Ein E-Bauteil in einer Rolle an einem Layout-Bauteil."""
model_config = _CONFIG
layout_id: str
layout_kind: str
role: str
bmk: str
verw: str = ""
bezeichnung: str = ""
controller: str = ""
station: str = ""
# Abstand nach dem Lagebezug in mm (auch bei der Nummernregel zur Kontrolle)
distance: float
# "raeumlich" | "nummer"
rule: str
# Abstand des naechstbesten Kandidaten derselben Rolle (None: keiner)
runner_up: float | None = None
def _patterns(text: str) -> list[str]:
return [p.strip() for p in text.split(",") if p.strip()]
def _match(text: str, patterns: list[str]) -> bool:
return any(fnmatch.fnmatchcase(text.upper(), p.upper()) for p in patterns)
def load_rules(path: Path) -> tuple[list[Rule], list[str]]:
"""Regeln und Anker-Verwendungen aus e_planning.ini lesen.
Abschnitt [Registrierung] -> anker (VERW-Muster fuer den Lagebezug);
jeder Abschnitt "<Art>.<Rolle>" ist eine Regel, in Dateireihenfolge.
"""
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
interpolation=None)
parser.optionxform = str # Schluessel nicht kleinschreiben
with path.open(encoding="utf-8") as fh:
parser.read_file(fh)
anchors = _patterns(parser.get("Registrierung", "anker", fallback="IN SEP"))
rules = []
for section in parser.sections():
if "." not in section:
continue
s = parser[section]
max_mm = s.get("max_mm")
soll = s.get("soll_mm")
rules.append(Rule(
role=section,
layout=_patterns(s.get("layout", section.split(".", 1)[0])),
verw=_patterns(s.get("verw", "*")),
max_mm=float(max_mm) if max_mm else None,
number_from=s.get("nummer_von", "").strip(),
number_offset=int(s.get("nummer_versatz", "0")),
prefix=s.get("praefix", "").strip(),
target_mm=float(soll) if soll else None,
tolerance_mm=float(s.get("toleranz_mm", str(DEFAULT_TOLERANCE_MM))),
))
known = {r.role for r in rules}
for r in rules:
if r.number_from and r.number_from not in known:
raise ValueError(f"{path.name} [{r.role}]: nummer_von = {r.number_from} "
f"ist keine Regel")
return rules, anchors
@dataclass
class _State:
positions: dict[str, tuple[float, float]]
used: set[str] = field(default_factory=set)
result: list[Assignment] = field(default_factory=list)
# (layout_id, role) -> Assignment
by_slot: dict[tuple[str, str], Assignment] = field(default_factory=dict)
def _make(point: LayoutPoint, rule: Rule, dev: EDevice, dist: float, how: str,
runner_up: float | None) -> Assignment:
return Assignment(
layout_id=point.layout_id, layout_kind=point.kind, role=rule.role,
bmk=dev.bmk, verw=dev.verw, bezeichnung=dev.bezeichnung,
controller=dev.controller, station=dev.station,
distance=round(dist, 1), rule=how,
runner_up=None if runner_up is None else round(runner_up, 1),
)
def _candidates(rule: Rule, devices: list[EDevice], state: _State) -> list[EDevice]:
return [
d for d in devices
if d.bmk not in state.used and d.bmk in state.positions
and _match(d.verw, rule.verw)
and (not rule.prefix or d.prefix.upper() == rule.prefix.upper())
]
def _assign_spatial(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
state: _State) -> None:
"""Beste Bewertung zuerst (Rule.score); je Bauteil und E-Bauteil hoechstens einmal."""
cands = _candidates(rule, devices, state)
pairs = []
for p in points:
if (p.layout_id, rule.role) in state.by_slot:
continue
for d in cands:
dist = math.dist((p.x, p.y), state.positions[d.bmk])
score = rule.score(dist)
if score is not None:
pairs.append((score, dist, p.layout_id, d.bmk))
pairs.sort()
# je Bauteil die Kandidaten in Rangfolge - fuer "naechstbester Kandidat"
by_point: dict[str, list[tuple[str, float]]] = {}
for _, dist, pid, bmk in pairs:
by_point.setdefault(pid, []).append((bmk, dist))
point_of = {p.layout_id: p for p in points}
dev_of = {d.bmk: d for d in cands}
for _, dist, pid, bmk in pairs:
if (pid, rule.role) in state.by_slot or bmk in state.used:
continue
others = [x for b, x in by_point[pid] if b != bmk]
a = _make(point_of[pid], rule, dev_of[bmk], dist, "raeumlich",
others[0] if others else None)
state.used.add(bmk)
state.by_slot[(pid, rule.role)] = a
state.result.append(a)
def _assign_by_number(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
state: _State) -> None:
"""Nummer = Nummer des Partners (Rolle nummer_von) + Versatz."""
cands = {(d.number): d for d in _candidates(rule, devices, state)
if d.number is not None}
for p in points:
if (p.layout_id, rule.role) in state.by_slot:
continue
partner = state.by_slot.get((p.layout_id, rule.number_from))
if partner is None or partner.bmk == "":
continue
number = next((d.number for d in devices if d.bmk == partner.bmk), None)
if number is None:
continue
dev = cands.get(number + rule.number_offset)
if dev is None or dev.bmk in state.used:
continue
dist = math.dist((p.x, p.y), state.positions[dev.bmk])
# Auch die Nummernregel darf nicht quer ueber die Anlage greifen
if rule.max_mm is not None and dist > rule.max_mm:
continue
a = _make(p, rule, dev, dist, "nummer", None)
state.used.add(dev.bmk)
state.by_slot[(p.layout_id, rule.role)] = a
state.result.append(a)
def assign(points: list[LayoutPoint], devices: list[EDevice], rules: list[Rule],
transform: Transform) -> list[Assignment]:
"""Regeln der Reihe nach anwenden.
Eine Regel mit nummer_von versucht zuerst die Nummernregel und nimmt fuer
die verbleibenden Bauteile den Abstand (wenn max_mm/soll_mm gesetzt ist). So wird
das Ventil eines Separators ueber die Nummer gefunden, auch wenn das
Ventil des Nachbarn naeher liegt (Speicherlinien im 800-mm-Raster).
"""
state = _State(positions={
d.bmk: transform.to_csv(*d.pos) for d in devices
if d.pos is not None and not d.cabinet
})
for rule in rules:
targets = [p for p in points if _match(p.kind, rule.layout)]
if rule.number_from:
_assign_by_number(rule, targets, devices, state)
if rule.max_mm is not None or rule.target_mm is not None:
_assign_spatial(rule, targets, devices, state)
return sorted(state.result, key=lambda a: (a.layout_id, a.role))
# ---------------------------------------------------------------------------
# Lagebezug
# ---------------------------------------------------------------------------
def _anchor_points(points: list[LayoutPoint], devices: list[EDevice],
anchors: list[str]):
layout_pts = {"Separator": [(p.x, p.y) for p in points if p.kind == "Separator"]}
e_pts = {"Separator": [d.pos for d in devices
if d.pos is not None and not d.cabinet
and _match(d.verw, anchors)]}
return layout_pts, e_pts
def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str,
eplan_name: str, re_register: bool,
warnings: list[str], fingerprint: str = "") -> Transform:
"""Abgelegten Lagebezug nachrechnen oder neu ermitteln und ablegen.
Mit ``fingerprint`` (Pruefsumme der Positionsdatei) gilt ein abgelegter
Bezug nur fuer genau diesen Dateistand.
"""
layout_pts, e_pts = _anchor_points(points, devices, anchors)
if not re_register:
stored = load_transform(cfg_dir, csv_name, eplan_name, fingerprint)
if stored:
verify_transform(stored, layout_pts, e_pts)
return stored
transform = derive_transform(layout_pts, e_pts, csv_name, eplan_name)
transform.fingerprint = fingerprint
saved = save_transform(cfg_dir, transform)
warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}")
return transform
# ---------------------------------------------------------------------------
# Slots
# ---------------------------------------------------------------------------
def points_from_parts(parts: list[LayoutPart]) -> list[LayoutPoint]:
"""Punktartige Bauteile aus TroDocument.parts."""
points = []
for p in parts:
if p.position is None:
continue
kind = next((k for k in LAYOUT_KINDS if k.lower() in p.kind.lower()), "")
if kind:
points.append(LayoutPoint(p.layout_id, kind, p.position[0], p.position[1],
p.name))
return points
def _binding_from(a: Assignment, device: HwDevice | None, tro: str = "",
slot: str = "", role: str = "") -> Binding:
status, note = "zugeordnet", ""
if device is None or not device.in_tia:
status, note = "widerspruch", "BMK nicht im TIA-Import"
if not tro:
status = "ohne_slot" if status == "zugeordnet" else status
note = note or f"{a.layout_kind} {a.layout_id} haengt an keinem TRO-Slot"
return Binding(
tro=tro, slot=slot, role=role or a.role, layout_id=a.layout_id, bmk=a.bmk,
constant=device.constant if device else "",
address=device.address if device else "",
source="positionsdraw", rule=a.rule, distance=a.distance,
runner_up=a.runner_up, status=status, note=note,
)
def bind_slots(doc: TroDocument, assignments: list[Assignment],
rules: list[Rule], hw: HardwareDocument) -> list[Binding]:
"""Zuordnungen an die Slot-Rollen der TROs haengen.
Ein Bauteil kann in zwei Slots desselben TRO stehen (Speicher:
``separator1`` und ``line1``) - dann bekommen beide die Zuordnung.
"""
devices = hw.by_bmk()
slots_of: dict[str, list[tuple[str, str, list[str]]]] = {}
for tro in doc.tros:
for s in tro.slots:
if s.layout_id:
roles = [r.rstrip("?") for r in s.roles]
slots_of.setdefault(s.layout_id, []).append((tro.tro_id, s.slot, roles))
rule_of = {r.role: r for r in rules}
bindings: list[Binding] = []
for a in assignments:
role = rule_of[a.role].slot_role if a.role in rule_of else a.role.split(".")[-1]
targets = [(t, s) for t, s, roles in slots_of.get(a.layout_id, []) if role in roles]
if not targets:
bindings.append(_binding_from(a, devices.get(a.bmk)))
continue
for tro_id, slot in targets:
bindings.append(_binding_from(a, devices.get(a.bmk), tro_id, slot, role))
return bindings
# ---------------------------------------------------------------------------
# Foerderer
# ---------------------------------------------------------------------------
def assign_conveyors(doc: TroDocument, hw: HardwareDocument, transform: Transform
) -> list[ConveyorBinding]:
"""Foerderer der E-Planung (K0102) per Mehrheit einem Layout-Foerderer zuordnen.
Jedes Feldgeraet, dessen Klartext einen Foerderer nennt, stimmt fuer den
Layout-Foerderer, dessen Huellbox ihm am naechsten liegt (hoechstens
CONVEYOR_MAX_MM). Zugeordnet wird bei mindestens CONVEYOR_MIN_VOTES
Stimmen und CONVEYOR_MIN_SHARE Anteil; je Layout-Foerderer hoechstens ein
Foerderer der E-Planung (der mit den meisten Stimmen).
"""
conveyors = [p for p in doc.parts if p.kind in CONVEYOR_KINDS and p.bbox and p.position]
drives = {dr.conveyor: dr for dr in hw.drives if dr.conveyor}
kind_of = _conveyor_kinds(hw)
votes: dict[str, Counter] = {}
totals: Counter = Counter()
for d in hw.field_devices():
if not d.conveyor:
continue
totals[d.conveyor] += 1
# Die Huellboxen ueberlappen (Speicherlinien reichen bis an den
# Kreisel); nennt der Klartext die Art, stimmen nur Foerderer dieser Art.
kinds = _LAYOUT_KINDS_FOR.get(kind_of.get(d.conveyor, ""), CONVEYOR_KINDS)
x, y = transform.to_csv(*d.pos)
best = min(((c.distance_to_box(x, y), c.layout_id) for c in conveyors
if c.kind in kinds), default=(None, ""))
if best[0] is not None and best[0] <= CONVEYOR_MAX_MM:
votes.setdefault(d.conveyor, Counter())[best[1]] += 1
names = sorted(set(totals) | set(drives))
name_of = {p.layout_id: p.name for p in doc.parts}
candidates = []
for name in names:
counter = votes.get(name, Counter())
layout_id, n = counter.most_common(1)[0] if counter else ("", 0)
candidates.append((n, name, layout_id))
taken: dict[str, str] = {}
result = []
for n, name, layout_id in sorted(candidates, key=lambda c: (-c[0], c[1])):
total = totals.get(name, 0)
dr = drives.get(name)
cb = ConveyorBinding(eplan_name=name, votes=n, total=total, rule="mehrheit",
motor=dr.motor if dr else "",
protection=dr.protection if dr else "",
encoder=dr.encoder if dr else "")
share = n / total if total else 0.0
if not layout_id:
cb.status = "offen"
cb.note = ("kein Feldgeraet nennt diesen Foerderer" if not total else
f"keine Stimme innerhalb {CONVEYOR_MAX_MM:.0f} mm eines Layout-Foerderers")
elif n < CONVEYOR_MIN_VOTES or share < CONVEYOR_MIN_SHARE:
cb.status = "offen"
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
cb.note = (f"unsicher: {n} von {total} Stimmen fuer {cb.layout_name or layout_id} "
f"(noetig {CONVEYOR_MIN_VOTES} und {CONVEYOR_MIN_SHARE:.0%})")
elif layout_id in taken:
cb.status = "widerspruch"
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
cb.note = f"{layout_id} ist schon {taken[layout_id]} zugeordnet (mehr Stimmen)"
else:
taken[layout_id] = name
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
others = votes.get(name, Counter())
rest = [f"{name_of.get(k, k)} {v}" for k, v in others.most_common() if k != layout_id]
if rest:
cb.note = "Gegenstimmen: " + ", ".join(rest)
result.append(cb)
return sorted(result, key=lambda c: c.eplan_name)
# Art des Foerderers im Klartext der E-Planung -> Arten im Layout
_LAYOUT_KINDS_FOR = {"Kreisel": ("Kreisel",), "Strecke": ("Strecke", "Gefaellestrecke")}
def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
"""Art je Foerderer der E-Planung: Mehrheit aller Klartexte, die ihn nennen.
"PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)" und "ANTRIEB
KREISEL-K0102" sagen Kreisel; ein Geraet ohne Artwort erbt die Art.
"""
counts: dict[str, Counter] = {}
for d in hw.devices:
if d.conveyor and d.conveyor_kind:
counts.setdefault(d.conveyor, Counter())[d.conveyor_kind] += 1
for dr in hw.drives:
if dr.conveyor and dr.conveyor_kind:
counts.setdefault(dr.conveyor, Counter())[dr.conveyor_kind] += 1
return {name: c.most_common(1)[0][0] for name, c in counts.items()}
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
hw: HardwareDocument) -> list[Binding]:
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers."""
devices = hw.by_bmk()
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
bindings = []
for tro in doc.tros:
for s in tro.slots:
if not s.layout_id:
continue
conv = by_layout.get(s.layout_id.split("-", 1)[0])
if conv is None:
continue
roles = [r.rstrip("?") for r in s.roles]
for role, attr in _DRIVE_ROLES.items():
bmk = getattr(conv, attr)
if role not in roles or not bmk:
continue
dev = devices.get(bmk)
bindings.append(Binding(
tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id,
bmk=bmk, constant=dev.constant if dev else "",
address=dev.address if dev else "", source="positionsdraw",
rule="mehrheit", status="zugeordnet" if dev and dev.in_tia else "widerspruch",
note=f"Antrieb von {conv.eplan_name}"))
return bindings
# ---------------------------------------------------------------------------
# Handkorrekturen
# ---------------------------------------------------------------------------
@dataclass
class Overrides:
"""Handkorrekturen aus hw_overrides.ini fuer eine Anlage."""
# (tro, slot, role) -> BMK ("" = Zuordnung loeschen, Rolle offen)
bindings: dict[tuple[str, str, str], str] = field(default_factory=dict)
# Foerderer der E-Planung -> Layout-ID ("" = Zuordnung loeschen)
conveyors: dict[str, str] = field(default_factory=dict)
def load_overrides(path: Path, layout_source: str) -> Overrides:
"""Abschnitte ``[<csv>.bindings]`` und ``[<csv>.conveyors]`` lesen.
``TRO01.separator1.sensor = BG0532`` setzt ein BMK, ``= -`` loescht die
Zuordnung (Rolle bleibt offen); ``K0102 = 0010`` setzt einen Foerderer.
"""
result = Overrides()
if not path.is_file():
return result
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
interpolation=None)
parser.optionxform = str
with path.open(encoding="utf-8") as fh:
parser.read_file(fh)
section = f"{layout_source}.bindings"
if parser.has_section(section):
for key, value in parser[section].items():
parts = key.split(".")
if len(parts) != 3:
raise ValueError(f"{path.name} [{section}] '{key}': erwartet "
f"<TRO-ID>.<Slot>.<Rolle>")
result.bindings[(parts[0], parts[1], parts[2].rstrip("?"))] = \
"" if value.strip() == "-" else value.strip()
section = f"{layout_source}.conveyors"
if parser.has_section(section):
for key, value in parser[section].items():
result.conveyors[key.strip()] = "" if value.strip() == "-" else value.strip()
return result
def _apply_overrides(doc: TroDocument, bindings: list[Binding],
conveyors: list[ConveyorBinding], overrides: Overrides,
hw: HardwareDocument, warnings: list[str]) -> list[Binding]:
devices = hw.by_bmk()
slots = {(t.tro_id, s.slot): s for t in doc.tros for s in t.slots}
for (tro, slot, role), bmk in overrides.bindings.items():
s = slots.get((tro, slot))
if s is None:
warnings.append(f"{OVERRIDES_FILE}: {tro}.{slot} gibt es in der TRO-JSON nicht "
f"- Korrektur nicht angewendet")
continue
bindings = [b for b in bindings
if not (b.tro == tro and b.slot == slot and b.role == role)]
if not bmk:
continue
# dasselbe BMK nicht zusaetzlich automatisch an anderer Stelle
bindings = [b for b in bindings if b.bmk != bmk or b.rule == "hand"]
dev = devices.get(bmk)
bindings.append(Binding(
tro=tro, slot=slot, role=role, layout_id=s.layout_id, bmk=bmk,
constant=dev.constant if dev else "", address=dev.address if dev else "",
source=OVERRIDES_FILE, rule="hand",
status="zugeordnet" if dev and dev.in_tia else "widerspruch",
note="" if dev and dev.in_tia else
("BMK nicht im TIA-Import" if dev else "BMK unbekannt in der Hardwareplanung")))
name_of = {p.layout_id: p.name for p in doc.parts}
for name, layout_id in overrides.conveyors.items():
cb = next((c for c in conveyors if c.eplan_name == name), None)
if cb is None:
cb = ConveyorBinding(eplan_name=name)
conveyors.append(cb)
if layout_id and doc.part(layout_id) is None:
warnings.append(f"{OVERRIDES_FILE}: Foerderer {name} -> {layout_id}: Layout-ID "
f"unbekannt - Korrektur nicht angewendet")
continue
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
cb.rule = "hand"
cb.status = "zugeordnet" if layout_id else "offen"
cb.note = "von Hand"
return bindings
# ---------------------------------------------------------------------------
# Zusammenbau
# ---------------------------------------------------------------------------
def _open_and_conflicts(doc: TroDocument, bindings: list[Binding]) -> list[Binding]:
"""Pflichtrollen ohne BMK als ``offen``; doppelt vergebene BMK als Widerspruch."""
have = {(b.tro, b.slot, b.role) for b in bindings if b.tro}
for tro in doc.tros:
for s in tro.slots:
for role in s.roles:
if role.endswith("?") or (tro.tro_id, s.slot, role) in have:
continue
bindings.append(Binding(tro=tro.tro_id, slot=s.slot, role=role,
layout_id=s.layout_id, status="offen"))
# Ein BMK an zwei verschiedenen Bauteilen ist ein Widerspruch. Derselbe
# Separator in zwei Slots eines Speichers (separator1/line1) ist keiner.
places: dict[str, set[tuple[str, str]]] = {}
for b in bindings:
if b.bmk and b.status != "offen":
places.setdefault(b.bmk, set()).add((b.layout_id, b.role))
for b in bindings:
if b.bmk and len(places.get(b.bmk, ())) > 1 and b.status == "zugeordnet":
b.status = "widerspruch"
b.note = "BMK mehrfach vergeben"
order = {t.tro_id: i for i, t in enumerate(doc.tros)}
return sorted(bindings, key=lambda b: (b.tro == "", order.get(b.tro, 0), b.slot,
b.role, b.layout_id))
def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
anchors: list[str], cfg_dir: Path, layout_file: str, hw_file: str,
overrides: Overrides | None = None, re_register: bool = False,
transform: Transform | None = None) -> AnlageDocument:
"""Kompletter Lauf ohne Ausgabe: TRO-JSON + Hardware-JSON -> AnlageDocument.
``transform`` vorgeben (Tests) ueberspringt die Ermittlung des Lagebezugs.
"""
warnings: list[str] = []
if not doc.parts:
raise ValueError(f"{layout_file} enthaelt keine 'parts' (Lage der Bauteile) - "
f"mit tro_flow --json neu erzeugen")
points = points_from_parts(doc.parts)
devices: list[EDevice] = list(hw.devices)
assignments: list[Assignment] = []
conveyors: list[ConveyorBinding] = []
if hw.field_devices():
if transform is None:
transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source,
hw.positions_file, re_register, warnings,
hw.positions_sha256)
share = transform.matched / transform.candidates if transform.candidates else 1.0
if share < REGISTRATION_MIN_SHARE:
warnings.append(
f"Lagebezug passt nur fuer {transform.matched} von {transform.candidates} "
f"Separatoren ({share:.0%}) - Layout und E-Zeichnung sind vermutlich "
f"verschiedene Planungsstaende; raeumliche Zuordnungen pruefen")
assignments = assign(points, devices, rules, transform)
conveyors = assign_conveyors(doc, hw, transform)
else:
warnings.append("Hardwareplanung ohne Lage (keine Positionsdatei) - keine "
"raeumliche Zuordnung, alle Rollen offen")
bindings = bind_slots(doc, assignments, rules, hw)
if overrides is not None:
bindings = _apply_overrides(doc, bindings, conveyors, overrides, hw, warnings)
bindings += bind_drives(doc, conveyors, hw)
bindings = _open_and_conflicts(doc, bindings)
used = {b.bmk for b in bindings if b.bmk}
unassigned = sorted(d.bmk for d in hw.field_devices() if d.bmk not in used)
registration = None
if transform is not None:
registration = Registration(
dx=round(transform.dx, 1), dy=round(transform.dy, 1),
rotation=round(transform.rotation, 4), source=transform.source,
matched=transform.matched, candidates=transform.candidates,
residual_max=round(transform.residual_max, 1),
description=transform.describe())
return AnlageDocument(
layout_file=layout_file, hw_file=hw_file, project=hw.project,
registration=registration, layout=doc, bindings=bindings,
conveyors=sorted(conveyors, key=lambda c: c.eplan_name),
unassigned=unassigned, warnings=warnings + hw.checks,
)
# ---------------------------------------------------------------------------
# Bericht
# ---------------------------------------------------------------------------
def render_report(anlage: AnlageDocument, hw: HardwareDocument) -> str:
out: list[str] = []
add = out.append
devices = hw.by_bmk()
add(f"# Anlage: {anlage.layout.source} + {anlage.project}")
add("")
add(f"- Layout: `{anlage.layout_file}`")
add(f"- Hardwareplanung: `{anlage.hw_file}` (TIA `{hw.tia_file or '-'}`, "
f"Positionen `{hw.positions_file or '-'}`)")
add(f"- Erzeugt: {anlage.created}")
if anlage.registration:
add(f"- Lagebezug: {anlage.registration.description}")
add("")
add("## Slot-Rollen")
add("")
add("| Slot-Art | Rolle | zugeordnet | offen | Widerspruch |")
add("|---|---|---|---|---|")
stats: dict[tuple[str, str], Counter] = {}
for b in anlage.bindings:
if not b.tro:
continue
kind = b.slot.rstrip("0123456789")
stats.setdefault((kind, b.role), Counter())[b.status] += 1
for (kind, role), c in sorted(stats.items()):
add(f"| {kind} | {role} | {c['zugeordnet']} | {c['offen']} | {c['widerspruch']} |")
add("")
add("## Foerderer (E-Planung -> Layout)")
add("")
if anlage.conveyors:
add("| E-Planung | Layout | Stimmen | Motor | Motorschutz | Geber | Status | Hinweis |")
add("|---|---|---|---|---|---|---|---|")
for c in anlage.conveyors:
layout = f"{c.layout_name} ({c.layout_id})" if c.layout_id else "-"
add(f"| {c.eplan_name} | {layout} | {c.votes}/{c.total} | {c.motor or '-'} "
f"| {c.protection or '-'} | {c.encoder or '-'} | {c.status} | {c.note} |")
else:
add("- keine")
add("")
add("## Zuordnungen je TRO")
add("")
add("Abstand nach dem Lagebezug; \"2.\" = naechstbester Kandidat (knapp = pruefen).")
add("")
add("| TRO | Slot | Rolle | Layout | BMK | Konstante | Adresse | VERW | Abstand mm "
"| 2. | Regel | Status |")
add("|---|---|---|---|---|---|---|---|---|---|---|---|")
for b in anlage.bindings:
if not b.tro or b.status == "offen":
continue
d = devices.get(b.bmk)
dist = "" if b.distance is None else f"{b.distance:.0f}"
second = "" if b.runner_up is None else f"{b.runner_up:.0f}"
status = b.status + (f" ({b.note})" if b.note and b.status != "zugeordnet" else "")
add(f"| {b.tro} | {b.slot} | {b.role} | {b.layout_id} | {b.bmk} | {b.constant} "
f"| {b.address} | {d.verw if d else ''} | {dist} | {second} | {b.rule} | {status} |")
add("")
add("## Offene Pflichtrollen")
add("")
open_by: dict[str, list[str]] = {}
for b in anlage.bindings:
if b.status == "offen":
open_by.setdefault(b.tro, []).append(f"{b.slot}.{b.role}")
if open_by:
for tro, roles in open_by.items():
add(f"- **{tro}**: {', '.join(roles)}")
else:
add("- keine")
add("")
loose = [b for b in anlage.bindings if not b.tro]
add("## Zugeordnet, aber ohne TRO-Slot")
add("")
if loose:
add("Diese Bauteile haengen (noch) an keinem Slot der TRO-JSON, z. B. "
"Ausschleuselemente: welcher TRO-Ausgang zu welchem Element gehoert, "
"weiss tro_flow noch nicht.")
add("")
add("| Regel | Layout | BMK | Abstand mm | Status |")
add("|---|---|---|---|---|")
for b in loose:
dist = "" if b.distance is None else f"{b.distance:.0f}"
add(f"| {b.role} | {b.layout_id} | {b.bmk} | {dist} | {b.status} |")
else:
add("- keine")
add("")
add("## Feldgeraete ohne Zuordnung")
add("")
if anlage.unassigned:
add("| BMK | VERW | Bezeichnung | Station |")
add("|---|---|---|---|")
for bmk in anlage.unassigned:
d = devices[bmk]
add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |")
else:
add("- keine")
add("")
if anlage.warnings:
add("## Hinweise")
add("")
for w in anlage.warnings:
add(f"- {w}")
add("")
return "\n".join(out)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _in_results(name: str, results: Path) -> Path:
path = Path(name)
if path.is_absolute() or path.parent != Path(".") or path.is_file():
return path
return results / name
def main(argv: list[str] | None = None) -> int:
from material_flow import env_dir
args = parse_args(argv)
results = env_dir("SKEL_RESULTS", "results")
cfg_dir = env_dir("SKEL_CFG", "cfg")
tro_path = _in_results(args.tro, results)
hw_path = _in_results(args.hw, results)
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE
for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path),
("Regeln", rules_path)):
if not path.is_file():
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
return 1
try:
doc = TroDocument.from_json(tro_path.read_text(encoding="utf-8"))
except ValidationError as exc:
print(f"FEHLER: {tro_path} ist keine gueltige TRO-JSON ({exc.error_count()} "
f"Abweichungen) - mit tro_flow --json neu erzeugen.", file=sys.stderr)
return 1
try:
hw = HardwareDocument.from_json(hw_path.read_text(encoding="utf-8"))
except ValidationError as exc:
print(f"FEHLER: {hw_path} passt nicht zum Schema '{HW_SCHEMA}' "
f"({exc.error_count()} Abweichungen) - mit hw_import neu erzeugen.",
file=sys.stderr)
return 1
try:
rules, anchors = load_rules(rules_path)
overrides = load_overrides(overrides_path, doc.source)
anlage = build_anlage(doc, hw, rules, anchors, cfg_dir, tro_path.name, hw_path.name,
overrides, args.re_register)
except RegistrationError as exc:
print(f"FEHLER: Lagebezug E-Zeichnung -> Layout nicht nachweisbar: {exc}",
file=sys.stderr)
print(f" Passen Layout und E-Zeichnung zusammen? Anker-VERW in "
f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr)
return 1
except (OSError, ValueError) as exc:
print(f"FEHLER: {exc}", file=sys.stderr)
return 1
stem = Path(doc.source).stem or tro_path.stem.removesuffix("_tro")
results.mkdir(parents=True, exist_ok=True)
json_file = results / f"{stem}_anlage.json"
md_file = results / f"{stem}_anlage.md"
json_file.write_text(anlage.to_json(), encoding="utf-8")
md_file.write_text(render_report(anlage, hw), encoding="utf-8")
status = Counter(b.status for b in anlage.bindings)
print("")
print("================================================================")
print("LAYOUT + HARDWAREPLANUNG -> ANLAGE")
print("================================================================")
print(f"TRO-JSON = {tro_path}")
print(f"Hardware-JSON = {hw_path}")
print(f"Lagebezug = {anlage.registration.description if anlage.registration else '-'}")
print(f"Slot-Rollen = {status['zugeordnet']} zugeordnet, {status['offen']} offen, "
f"{status['widerspruch']} Widerspruch")
print(f"Ohne Slot = {status['ohne_slot']} Zuordnungen an Bauteilen ohne TRO-Slot")
conv_ok = [c for c in anlage.conveyors if c.status == "zugeordnet"]
print(f"Foerderer = {len(conv_ok)} von {len(anlage.conveyors)} zugeordnet"
+ (": " + ", ".join(f"{c.eplan_name}={c.layout_name or c.layout_id}"
for c in conv_ok) if conv_ok else ""))
print(f"Ohne Zuordnung = {len(anlage.unassigned)} Feldgeraete")
for w in anlage.warnings:
print(f" ! {w}")
print(f"JSON = {json_file}")
print(f"Bericht = {md_file}")
print("================================================================")
print("")
return 0
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="merge_hw_layout",
description="Fuehrt die TRO-JSON (tro_flow --json) und die Hardware-JSON "
"(hw_import) zusammen: ordnet Sensoren, Ventile und Antriebe den "
"Slots der TROs zu und schreibt <csv>_anlage.json plus einen "
"Bericht <csv>_anlage.md.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Beispiele:\n"
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json\n"
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json --re-register\n"
"\n"
"Exit-Codes:\n"
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
)
parser.add_argument("--tro", required=True, metavar="NAME",
help="TRO-JSON <csv>_tro.json in %%SKEL_RESULTS%% (oder ein Pfad)")
parser.add_argument("--hw", required=True, metavar="NAME",
help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% "
"(oder ein Pfad)")
parser.add_argument("--rules", metavar="DATEI",
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
parser.add_argument("--overrides", metavar="DATEI",
help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} "
f"(fehlt sie, keine)")
parser.add_argument("--re-register", action="store_true",
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
"einer abgelegt ist")
return parser.parse_args(argv)
if __name__ == "__main__":
sys.exit(main())
+681
View File
@@ -0,0 +1,681 @@
# -*- coding: utf-8 -*-
"""
model.py - Gemeinsames Datenmodell der TRO-Instanzen und der TRO-JSON.
Schichten (wer darf wen importieren):
tro_catalog.py Typ-Ebene: was ein TRO-*Typ* ist (Bauteile, FB, Farbe, Symbol)
model.py Instanz-Ebene: ein konkreter TRO, die ganze TRO-JSON <- hier
importiert nur tro_catalog
tro_flow.py Ableitung: CSV-Graph -> Analysis -> TroDocument
scl_skeleton.py Ausgabe: TroDocument -> FB_Main.scl
tro_annotate.py Ausgabe: Analysis -> annotierte Zeichnung (nur Ansicht)
Klassen:
TroBase was jeder TRO hat, egal woher er stammt: ID, Typ, Separatoren,
Vorgaenger/Nachfolger, Position. FB-Baustein und Bauteile
werden aus dem Katalog gelesen, nie gespeichert.
DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss
(Hosts, Scanner, Begruendung, Positionsquelle ...)
Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen
TroCandidate eine im Review gemeldete Stelle, an der ein TRO erwartet
wird, die aber (noch) keiner ist
Slot eine Stelle eines TRO, an die Hardware gehoert (Separator,
Weiche, Scanner, Antrieb) mit den Rollen, die die
Hardwareplanung dort fuellen muss
JamArea ein Staubereich: Eingang genau eines TRO (je Separator
laut Katalog), mit allen Vorgaengern, die dort einfahren
LayoutPart ein Bauteil des Layouts mit Lage und Huellbox - das Ziel
der raeumlichen Zuordnung in merge_hw_layout (die TRO-JSON traegt
es, damit merge_hw_layout das CSV nicht lesen muss)
Analysis Arbeitsergebnis von tro_flow.analyse_tros
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
geschrieben, von scl_skeleton gelesen und geprueft
Binding eine Zuordnung Hardware -> TRO-Slot-Rolle (merge_hw_layout)
ConveyorBinding ein Foerderer der Hardwareplanung (K0102) an einem
Layout-Foerderer, mit Antrieb, Motorschutz, Geber
AnlageDocument die Anlagen-JSON (<csv>_anlage.json): Layout unveraendert
plus bindings[] - von merge_hw_layout geschrieben
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter
`tro_id`/`type_name` (Aliase, `populate_by_name`). Gleiche Daten heissen in
allen Klassen gleich (auch in tro_overrides.py): `type_name`, `separators`,
`hosts`, `position`, `reason`, `confidence`.
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, Any
from pydantic import (
BaseModel,
ConfigDict,
Field,
computed_field,
field_serializer,
field_validator,
model_validator,
)
from tro_catalog import TroDefinition, TroItem, get_tro
__all__ = [
"SCHEMA",
"TroBase",
"Slot",
"JamArea",
"LayoutPart",
"DerivedTro",
"Connection",
"TroCandidate",
"Analysis",
"TroDocument",
"Registration",
"Binding",
"ConveyorBinding",
"AnlageDocument",
"ANLAGE_SCHEMA",
"BINDING_STATUS",
"derive_slots",
"derive_jam_areas",
"storage_lines",
]
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit
# `candidates` statt `openPoints`, v4 wie v3 mit `slots` je TRO und `jamAreas`.
# Eine v3-Datei liest sich weiter (die neuen Felder fehlen dann und werden beim
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived). Seit 2026-10-15
# traegt v4 optional `parts` (Lage der Layout-Bauteile fuer merge_hw_layout); eine
# v4-Datei ohne `parts` liest sich weiter, nur merge_hw_layout braucht sie.
SCHEMA = "sps_skel-tro-v4"
# Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout)
ANLAGE_SCHEMA = "sps_skel-anlage-v1"
# Zustand einer Zuordnung (doc/Konzept_Layout_Hardware_SCL.md, Umbau 3):
# zugeordnet ein BMK steht fest
# offen Pflichtrolle eines Slots ohne BMK -> TODO(E-Planung)
# widerspruch BMK steht fest, passt aber nicht (fehlt im TIA-Import,
# doppelt vergeben, Handkorrektur auf unbekanntes BMK)
# ohne_slot E-Bauteil einem Layout-Bauteil zugeordnet, das (noch) an
# keinem TRO-Slot haengt (z. B. Ausschleuselemente)
BINDING_STATUS = ("zugeordnet", "offen", "widerspruch", "ohne_slot")
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
# still auf einen Standardwert zurueckzufallen), Aliase und Feldnamen gelten
# beide.
_CONFIG = ConfigDict(
validate_assignment=True,
extra="forbid",
populate_by_name=True,
)
class _Positioned(BaseModel):
"""
Anlagenkoordinate (mm) als Feld ``position``, in der JSON {"x", "y", "z"}.
Das Feld selbst deklariert jede Unterklasse, damit es dort in der
JSON-Reihenfolge an seiner Stelle steht.
"""
if TYPE_CHECKING:
position: Any
@field_validator("position", mode="before", check_fields=False)
@classmethod
def _position_from_json(cls, value: Any) -> Any:
if isinstance(value, dict):
return (value["x"], value["y"], value.get("z", 0.0))
return value
@field_serializer("position", when_used="json-unless-none", check_fields=False)
def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]:
return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)}
@property
def x(self) -> float | None:
return self.position[0] if self.position else None
@property
def y(self) -> float | None:
return self.position[1] if self.position else None
@property
def z(self) -> float | None:
return self.position[2] if self.position else None
def describe_position(self) -> str:
"""Koordinate als Text, z. B. '1030, 4700'."""
if not self.position:
return "-"
return f"{self.position[0]:.0f}, {self.position[1]:.0f}"
class TroBase(_Positioned):
"""
Kern eines TRO (Instanz in einer Anlage), unabhaengig von seiner Quelle.
Gegenstueck zu :class:`tro_catalog.TroDefinition`: die Definition beschreibt
den *Typ* (FB-Baustein, Bauteile), ein TRO die konkrete Stelle im Layout.
Verknuepft ueber ``type_name``, siehe :meth:`definition`.
FB-Baustein und Bauteile haengen nur am Typ. Sie werden nicht gespeichert,
sondern bei jedem Zugriff aus dem Katalog gelesen (``fb_block``, ``items``)
und so auch in die JSON geschrieben. Steht beim Einlesen einer JSON ein
Wert drin, der nicht zum Katalog passt, ist das ein Fehler - den Typ
aendern, nicht die Bauteile.
Listenfelder werden beim Pruefen kopiert; ``tro.hosts.append(...)`` aendert
also nur diesen TRO.
"""
model_config = _CONFIG
tro_id: str = Field(alias="id")
type_name: str = Field(alias="type")
confidence: str = ""
separators: list[str] = Field(default_factory=list)
predecessors: list[str] = Field(default_factory=list)
successors: list[str] = Field(default_factory=list)
# Anlagenkoordinate (mm); in der JSON als {"x": .., "y": .., "z": ..}
position: tuple[float, float, float] | None = None
# -- aus dem Katalog -----------------------------------------------------
def definition(self) -> TroDefinition | None:
"""Typdefinition aus tro_catalog (None wenn Typ unbekannt)."""
return get_tro(self.type_name)
@computed_field(alias="fbType") # type: ignore[prop-decorator]
@property
def fb_block(self) -> str:
"""FB-Baustein laut Katalog, z. B. 'FB_ILS_MTRO_1Sep' ("" wenn Typ unbekannt)."""
definition = self.definition()
return definition.get_fb_block() if definition else ""
@computed_field # type: ignore[prop-decorator]
@property
def items(self) -> dict[str, int]:
"""Bauteile laut Katalog, z. B. {'Separator': 1} ({} wenn Typ unbekannt)."""
definition = self.definition()
return definition.get_items() if definition else {}
@model_validator(mode="before")
@classmethod
def _check_catalog_fields(cls, data: Any) -> Any:
"""`items`/`fbType` aus einer JSON gegen den Katalog pruefen und entfernen."""
if not isinstance(data, dict) or not ({"items", "fbType", "fb_block"} & data.keys()):
return data
data = dict(data)
items = data.pop("items", None)
fb_block = data.pop("fbType", data.pop("fb_block", None))
type_name = data.get("type", data.get("type_name"))
definition = get_tro(type_name) if isinstance(type_name, str) else None
expected_items = definition.get_items() if definition else {}
expected_fb = definition.get_fb_block() if definition else ""
if items is not None and dict(items) != expected_items:
raise ValueError(
f"items {items} passt nicht zum Typ '{type_name}' laut tro_catalog "
f"({expected_items}) - den Typ aendern, nicht die Bauteile"
)
if fb_block is not None and (fb_block or "") != expected_fb:
raise ValueError(
f"fbType '{fb_block}' passt nicht zum Typ '{type_name}' laut "
f"tro_catalog ('{expected_fb}')"
)
return data
class Slot(BaseModel):
"""
Eine Stelle eines TRO, an die Hardware gehoert (Konzept Umbau 1).
``slot`` ist der stabile Schluessel innerhalb des TRO (``separator1``,
``switch1``, ``scanner1``, ``drive``); zusammen mit der TRO-ID adressiert er
die Zuordnung der Hardwareplanung. ``layout_id`` ist die TeileId des
Bauteils im aktuellen Export - nur zur Nachverfolgung, nicht stabil ("",
wenn das Bauteil im Layout nicht bekannt ist). Rollen mit ``?`` sind
optional: fehlt das Geraet, wird der zugehoerige xPlugged*-Schalter FALSE.
"""
model_config = _CONFIG
slot: str
layout_id: str = Field(default="", alias="layoutId")
roles: list[str] = Field(default_factory=list)
class JamArea(_Positioned):
"""
Ein Staubereich: die Warteschlange vor einem Separator, Eingang genau eines
TRO (``tro``, Eingang Nummer ``entry``).
Kennung ``J<Separator>`` (Mubea: Sep1024 -> J1024). Mehrere Vorgaenger
koennen in denselben Staubereich einfahren (Zusammenfuehrung); deren
Ausgang zeigt dann auf diesen Staubereich. Der SCL-Name
``"DB_JamArea<NNNN>".stJam<NNNN>_<k>`` (Standard UH01-UH05) braucht die
Foerderernummer aus der Hardwareplanung und steht deshalb nicht hier.
"""
model_config = _CONFIG
jam_id: str = Field(alias="id")
tro: str
entry: int = 1
separator: str = ""
# tragender Foerderer (erster Host des TRO im Layout)
host: str = ""
predecessors: list[str] = Field(default_factory=list)
note: str = ""
position: tuple[float, float, float] | None = None
class LayoutPart(_Positioned):
"""
Ein Bauteil des Layouts (eine CSV-Zeile mit Lage), z. B. Separator,
Ausschleuselement, Kreisel.
Ziel der raeumlichen Zuordnung in merge_hw_layout: die Slots nennen nur die
``layout_id``, die Lage steht hier. ``bbox`` ist die achsparallele
Ausdehnung (mm) um ``position`` aus der CSV-Spalte "Boundingbox" -
gebraucht, um Geraete einem langen Foerderer (Kreisel) zuzuordnen.
"""
model_config = _CONFIG
layout_id: str = Field(alias="layoutId")
kind: str
name: str = ""
position: tuple[float, float, float] | None = None
bbox: tuple[float, float, float] | None = None
def distance_to_box(self, x: float, y: float) -> float | None:
"""Abstand eines Punkts zur Huellbox (0 innerhalb); None ohne Lage/Box."""
if not self.position or not self.bbox:
return None
dx = max(abs(x - self.position[0]) - self.bbox[0] / 2, 0.0)
dy = max(abs(y - self.position[1]) - self.bbox[1] / 2, 0.0)
return (dx * dx + dy * dy) ** 0.5
class DerivedTro(TroBase):
"""
Ein von tro_flow aus dem CSV abgeleiteter TRO (oder eine Systemgrenze).
Zusaetzlich zum Kern: aus welchen Bauteilen er gebildet wurde, warum er
diesen Typ hat, und woher seine Position stammt.
"""
label: str
hosts: list[str]
scanners: list[str]
reason: str
colocated: list[str] = Field(default_factory=list)
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde
position_from: str = Field(default="", alias="positionFrom")
position_points: int = Field(default=0, alias="positionPoints")
# Stellen fuer die Hardwareplanung (derive_slots); leer bei Systemgrenzen
slots: list[Slot] = Field(default_factory=list)
@property
def connected(self) -> bool:
return bool(self.successors or self.predecessors or self.colocated)
class Connection(BaseModel):
"""Gerichtete Verbindung zwischen zwei TROs/Stationen (JSON: from/to/kind)."""
model_config = _CONFIG
src: str = Field(alias="from")
dst: str = Field(alias="to")
# transfer / umlauf / weiche / unbestimmt / normal (siehe tro_flow.link_tros)
kind: str = ""
class TroCandidate(_Positioned):
"""
Eine im Review gemeldete Stelle, an der ein TRO erwartet wird.
Kein TRO: keine Separatoren, keine Verbindungen, keine SCL. Das Bauteil
steckt heute in einem anderen TRO (``belongs_to``). ``type_name`` ist der
hier erwartete Katalogtyp.
"""
model_config = _CONFIG
name: str
type_name: str = Field(default="", alias="type")
position: tuple[float, float, float]
belongs_to: str = Field(default="", alias="belongsTo")
note: str = ""
class Analysis(BaseModel):
"""Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde."""
model_config = _CONFIG
tros: list[DerivedTro] = Field(default_factory=list)
edges: list[tuple[str, str]] = Field(default_factory=list)
# (von, nach) -> Kantenart (transfer / umlauf / weiche / unbestimmt / normal)
edge_kinds: dict[tuple[str, str], str] = Field(default_factory=dict)
isolated: list[DerivedTro] = Field(default_factory=list)
unknown_types: list[str] = Field(default_factory=list)
findings: list[str] = Field(default_factory=list)
# Wie Host/Fanout ermittelt wurden (Doku/Bericht)
method: str = "Materialfluss-Graph (Zuordnung/Nachbarn)"
# Systemgrenzen (BTMT Be-/Entladestationen) - siehe derive_boundaries; kein
# TRO, nur damit der tatsaechliche Materialein-/ausgang sichtbar wird.
boundaries: list[DerivedTro] = Field(default_factory=list)
def by_id(self, tro_id: str) -> DerivedTro | None:
for tro in self.tros + self.boundaries:
if tro.tro_id == tro_id:
return tro
return None
class TroDocument(BaseModel):
"""
Die TRO-JSON einer Anlage (``<csv>_tro.json``).
Geschrieben von ``tro_flow --json`` (:meth:`from_analysis`, :meth:`to_json`),
gelesen von scl_skeleton (:meth:`from_json`). Weil das Modell beide Seiten
definiert, faellt ein umbenannter oder falscher Schluessel beim Einlesen
als Fehler auf.
"""
model_config = _CONFIG
schema_id: str = Field(default=SCHEMA, alias="schema")
source: str
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
note: str = ""
tros: list[DerivedTro] = Field(default_factory=list)
# Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL
boundaries: list[DerivedTro] = Field(default_factory=list)
connections: list[Connection] = Field(default_factory=list)
candidates: list[TroCandidate] = Field(default_factory=list)
jam_areas: list[JamArea] = Field(default_factory=list, alias="jamAreas")
# Layout-Bauteile mit Lage (fuer merge_hw_layout); leer in aelteren Dateien
parts: list[LayoutPart] = Field(default_factory=list)
findings: list[str] = Field(default_factory=list)
@classmethod
def from_analysis(cls, analysis: Analysis, source: str, note: str = "",
parts: list[LayoutPart] | None = None) -> TroDocument:
"""Dokument aus dem Ergebnis von tro_flow.analyse_tros bilden.
Slots und Staubereiche werden hier abgeleitet, damit die JSON sie
traegt und nicht erst das Rendern sie kennt. ``parts`` (Lage der
Layout-Bauteile) gibt tro_flow aus dem CSV mit.
"""
tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros]
return cls(
source=source,
note=note,
tros=tros,
boundaries=analysis.boundaries,
jam_areas=derive_jam_areas(tros),
parts=list(parts or []),
connections=[
Connection(src=src, dst=dst, kind=analysis.edge_kinds.get((src, dst), ""))
for src, dst in analysis.edges
],
findings=analysis.findings,
)
def part(self, layout_id: str) -> LayoutPart | None:
"""Layout-Bauteil zur TeileId; ein Bahn-Suffix ("0010-R") zaehlt nicht."""
base = layout_id.split("-", 1)[0]
return next((p for p in self.parts if p.layout_id == base), None)
@classmethod
def from_json(cls, text: str) -> TroDocument:
"""JSON-Text einlesen und pruefen (pydantic.ValidationError bei Fehlern)."""
return cls.model_validate_json(text)
def jam_areas_or_derived(self) -> list[JamArea]:
"""jamAreas aus der Datei; bei einer v3-Datei (ohne jamAreas) abgeleitet."""
return self.jam_areas or derive_jam_areas(self.tros)
def to_json(self) -> str:
"""Als JSON-Text mit den Schluesseln der TRO-JSON (id, type, fbType ...)."""
return self.model_dump_json(by_alias=True, indent=2) + "\n"
def by_id(self, tro_id: str) -> DerivedTro | None:
for tro in self.tros + self.boundaries:
if tro.tro_id == tro_id:
return tro
return None
# ---------------------------------------------------------------------------
# Anlagen-JSON: Layout + Zuordnungen der Hardwareplanung (merge_hw_layout)
# ---------------------------------------------------------------------------
class Registration(BaseModel):
"""Lagebezug E-Zeichnung -> Layout, mit dem die Zuordnung lief (Nachweis).
Kopie der Kennwerte aus dxf_registration.Transform - model.py importiert
dxf_registration bewusst nicht (Schichten, siehe Kopf).
"""
model_config = _CONFIG
dx: float
dy: float
rotation: float = 0.0
source: str = ""
matched: int = 0
candidates: int = 0
residual_max: float = Field(default=0.0, alias="residualMax")
description: str = ""
class Binding(BaseModel):
"""
Eine Zuordnung: Rolle ``role`` im Slot ``slot`` des TRO ``tro`` <- BMK.
Stabiler Schluessel ist TRO-ID + Slot + Rolle (TRO-IDs aus der ID-Datei);
``layout_id`` dient nur zur Nachverfolgung im aktuellen Layoutstand. Bei
``status == "ohne_slot"`` sind ``tro``/``slot`` leer und ``role`` ist die
Regel (``Ausschleus.stau``). ``constant``/``address`` kommen aus dem
TIA-Import und sind das, was im SCL steht (``cInBG0532``, ``%E053.2``).
"""
model_config = _CONFIG
tro: str = ""
slot: str = ""
role: str
layout_id: str = Field(default="", alias="layoutId")
bmk: str = ""
constant: str = ""
address: str = ""
# positionsdraw | hw_overrides.ini
source: str = ""
# raeumlich | nummer | hand
rule: str = ""
distance: float | None = None
runner_up: float | None = Field(default=None, alias="runnerUp")
status: str = "zugeordnet"
note: str = ""
@field_validator("status")
@classmethod
def _known_status(cls, value: str) -> str:
if value not in BINDING_STATUS:
raise ValueError(f"status '{value}' unbekannt, erlaubt: {', '.join(BINDING_STATUS)}")
return value
class ConveyorBinding(BaseModel):
"""
Ein Foerderer der Hardwareplanung (``K0102``, ``K0106``) an einem
Layout-Foerderer (``layout_id`` "0010" = Kreisel3).
Die Namenswelten passen nicht zusammen (Layout: Kreisel1..3, E-Planung:
K0102/K0104/K0105), deshalb raeumlich: alle Feldgeraete, deren
Bezeichnung den Foerderer nennt, stimmen fuer den naechsten
Layout-Foerderer ab (``votes`` von ``total``).
"""
model_config = _CONFIG
eplan_name: str = Field(alias="eplanName")
layout_id: str = Field(default="", alias="layoutId")
layout_name: str = Field(default="", alias="layoutName")
votes: int = 0
total: int = 0
motor: str = ""
protection: str = ""
encoder: str = ""
# mehrheit | hand
rule: str = ""
status: str = "zugeordnet"
note: str = ""
class AnlageDocument(BaseModel):
"""
Die Anlagen-JSON ``<csv>_anlage.json``: das Layout (TRO-JSON) unveraendert
unter ``layout``, daneben die Zuordnungen der Hardwareplanung.
Geschrieben von merge_hw_layout, kuenftig die eine Eingabe von scl_skeleton
(doc/Konzept_Layout_Hardware_SCL.md, Umbau 4).
"""
model_config = _CONFIG
schema_id: str = Field(default=ANLAGE_SCHEMA, alias="schema")
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
layout_file: str = Field(default="", alias="layoutFile")
hw_file: str = Field(default="", alias="hwFile")
project: str = ""
registration: Registration | None = None
layout: TroDocument
bindings: list[Binding] = Field(default_factory=list)
conveyors: list[ConveyorBinding] = Field(default_factory=list)
# Feldgeraete mit Lage, die keine Zuordnung bekommen haben (BMK)
unassigned: list[str] = Field(default_factory=list)
warnings: list[str] = Field(default_factory=list)
def binding(self, tro_id: str, slot: str, role: str) -> Binding | None:
"""Zuordnung einer Slot-Rolle (Rolle ohne "?"), None wenn keine."""
role = role.rstrip("?")
return next((b for b in self.bindings
if b.tro == tro_id and b.slot == slot and b.role == role), None)
def conveyor_for(self, layout_id: str) -> ConveyorBinding | None:
"""Foerderer der Hardwareplanung an einem Layout-Foerderer ("0010-R" geht)."""
base = layout_id.split("-", 1)[0]
return next((c for c in self.conveyors
if c.layout_id == base and c.status == "zugeordnet"), None)
def to_json(self) -> str:
return self.model_dump_json(by_alias=True, indent=2) + "\n"
@classmethod
def from_json(cls, text: str) -> AnlageDocument:
return cls.model_validate_json(text)
# ---------------------------------------------------------------------------
# Ableitungen aus Katalogtyp und Topologie
# ---------------------------------------------------------------------------
# Rollen je Slot-Art (doc/Konzept_Layout_Hardware_SCL.md, Umbau 1); "?" = optional
_SEPARATOR_ROLES = ("sensor", "stopper", "jamSensor?", "partSensor?")
_SWITCH_ROLES = ("valve", "exitSensor?")
_SCANNER_ROLES = ("reader",)
_DRIVE_ROLES = ("motor", "motorProtection", "senCarrInPos", "senFinger",
"senLastPos", "senJam")
_STORAGE_LINE_ROLES = ("sensor", "stopper", "jamSensor?")
def _at(values: list[str], index: int) -> str:
return values[index] if index < len(values) else ""
def storage_lines(tro: TroBase) -> list[str]:
"""Separatoren der Lagerlinien eines Speicher-TRO.
tro_flow legt sie als [steuernde ..., Linien ...] ab; die Linien sind die
letzten len(hosts) (eine je Speicherbahn). Fehlen Hosts, zaehlt jeder
Separator als Linie - dieselbe Regel wie scl_context._store.
"""
hosts = list(getattr(tro, "hosts", []) or [])
seps = list(tro.separators)
count = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
return seps[len(seps) - count:]
def derive_slots(tro: DerivedTro) -> list[Slot]:
"""Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO.
Weichen tragen noch keine Layout-ID - tro_flow speichert das Weichen-
bzw. Ausschleuselement heute nicht am TRO. Ein Speicher bekommt je
Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und
``line1`` koennen dasselbe Bauteil sein (wie im PinStore-Template).
"""
items = tro.items
slots: list[Slot] = []
for i in range(items.get(TroItem.SEPARATOR, 0)):
slots.append(Slot(slot=f"separator{i + 1}", layout_id=_at(tro.separators, i),
roles=list(_SEPARATOR_ROLES)))
if items.get(TroItem.STORAGE_LINE):
for n, sep in enumerate(storage_lines(tro), start=1):
slots.append(Slot(slot=f"line{n}", layout_id=sep,
roles=list(_STORAGE_LINE_ROLES)))
for i in range(items.get(TroItem.SWITCH, 0)):
slots.append(Slot(slot=f"switch{i + 1}", roles=list(_SWITCH_ROLES)))
for i in range(items.get(TroItem.SCANNER, 0)):
slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i),
roles=list(_SCANNER_ROLES)))
if items.get(TroItem.VARIO_DRIVE):
slots.append(Slot(slot="drive", layout_id=_at(tro.hosts, 0),
roles=list(_DRIVE_ROLES)))
return slots
def derive_jam_areas(tros: list[DerivedTro]) -> list[JamArea]:
"""Staubereiche: je Separator laut Katalog ein Eingang des TRO.
Kennung J<Separator>; fehlt der Separator im Layout, J<TRO-ID>_<n>. Alle
Vorgaenger fahren in den ersten Eingang ein - bei zwei Eingaengen
(2Sep-Typen) ist die Aufteilung aus dem Layout nicht ableitbar und als
Hinweis vermerkt.
"""
areas: list[JamArea] = []
for tro in tros:
n_entries = max(1, tro.items.get(TroItem.SEPARATOR, 1))
if n_entries > 1 and tro.predecessors:
note = "Aufteilung der Vorgaenger auf die Eingaenge offen"
elif tro.items.get(TroItem.STORAGE_LINE):
# Die Separatoren eines Speichers sitzen auf den Lagerlinien; der
# Eingang liegt davor und hat im Layout keinen eigenen Separator.
note = "Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator"
else:
note = ""
for n in range(1, n_entries + 1):
sep = _at(tro.separators, n - 1)
areas.append(JamArea(
jam_id=f"J{sep}" if sep else f"J{tro.tro_id}_{n}",
tro=tro.tro_id,
entry=n,
separator=sep,
host=_at(tro.hosts, 0),
predecessors=list(tro.predecessors) if n == 1 else [],
note=note,
position=tro.position if n == 1 else None,
))
return areas
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""
parameter.py - Zahlenwerte der Bibliotheken aus cfg/parameter.ini.
Toleranzen, Schwellen, Massstaebe und Zeichnungsgroessen stehen nicht im Code,
sondern in einer INI-Datei: ein Abschnitt je Modul, der Schluessel ist der
Name der Konstanten in Kleinbuchstaben ([kreisel_junctions]
contact_tolerance -> kreisel_junctions.CONTACT_TOLERANCE).
Gelesen wird zuerst die Grunddatei des Repositorys (<Projekt>/cfg/
parameter.ini, versioniert, muss vollstaendig sein). Liegt in %SKEL_CFG% eine
weitere parameter.ini, ueberschreibt sie einzelne Werte - so kann eine Anlage
abweichende Toleranzen bekommen, ohne die Grunddatei zu aendern.
Fehlt die Grunddatei oder ein Schluessel, bricht der Import mit
ParameterError ab: einen stillen Ersatzwert im Code gibt es bewusst nicht,
sonst stuende die Zahl doch wieder im Code.
Unterste Schicht: importiert nichts aus lib/, damit jedes Modul es nutzen kann.
"""
from __future__ import annotations
import configparser
import os
from functools import lru_cache
from pathlib import Path
PARAMETER_FILE = "parameter.ini"
__all__ = ["PARAMETER_FILE", "ParameterError", "files", "get_float", "get_int"]
class ParameterError(RuntimeError):
"""Grunddatei fehlt oder ein Wert fehlt bzw. ist keine Zahl."""
def _base_file() -> Path:
"""Grunddatei im Repository: <Projekt>/cfg/parameter.ini."""
return Path(__file__).resolve().parent.parent / "cfg" / PARAMETER_FILE
def files() -> list[Path]:
"""Gelesene Dateien in Lesereihenfolge (Grunddatei, ggf. %SKEL_CFG%)."""
result = [_base_file()]
cfg = os.environ.get("SKEL_CFG")
if cfg:
override = Path(cfg) / PARAMETER_FILE
if override.is_file() and override.resolve() != result[0].resolve():
result.append(override)
return result
@lru_cache(maxsize=1)
def _parser() -> configparser.ConfigParser:
"""Alle Parameterdateien einlesen (einmal je Prozess)."""
base = _base_file()
if not base.is_file():
raise ParameterError(f"Parameterdatei fehlt: {base}")
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
for path in files():
with path.open(encoding="utf-8") as handle:
parser.read_file(handle, source=str(path))
return parser
def _raw(section: str, key: str) -> str:
"""Rohwert; ParameterError, wenn Abschnitt oder Schluessel fehlt."""
parser = _parser()
if not parser.has_option(section, key):
raise ParameterError(
f"[{section}] {key} fehlt in {', '.join(str(p) for p in files())}")
return parser.get(section, key)
def get_float(section: str, key: str) -> float:
"""Zahl aus [section] key.
Parameter:
section: Abschnitt, in der Regel der Modulname (z. B. "tro_flow").
key: Schluessel, der Konstantenname in Kleinbuchstaben.
Rueckgabe:
Der Wert als float.
"""
raw = _raw(section, key)
try:
return float(raw)
except ValueError:
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine Zahl") from None
def get_int(section: str, key: str) -> int:
"""Ganze Zahl aus [section] key (Anzahlen, Schrittweiten)."""
raw = _raw(section, key)
try:
return int(raw)
except ValueError:
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine ganze Zahl") from None
+649
View File
@@ -0,0 +1,649 @@
# -*- coding: utf-8 -*-
"""
scl_context.py - Render-Kontext fuer das FB_Main-Geruest.
Schritt 2 aus doc/SCL_Templates.md: alles, was scl_skeleton aus der TRO-JSON
*ableitet*, steht hier - Reihenfolge, Bauteilbloecke, Staubereiche,
Weichenziele, Namen. Die Ausgabe (heute die emit_*-Funktionen, spaeter die
Templates in lib/scl_templates/) liest nur noch diesen Kontext und rechnet
nichts mehr selbst.
Schichten:
model.py TroDocument (die TRO-JSON)
scl_context.py TroDocument -> SclFile <- hier
scl_skeleton.py SclFile -> SCL-Text
Namen (Separator-, Weichen-, MainTro-Nummer, Adressen ...) sind ein
:class:`SclName`: Wert plus Merker, ob er noch aus der E-Planung kommen muss.
Ohne E-Planung sind alle Platzhalter ``todo=True``. Mit einer Zuordnung aus
lib/e_planning.py (``eplan``) werden Separator-Sensor und Stopperventil der
zugeordneten Separatoren eingesetzt und ``todo`` geloescht; alles andere
bleibt offen. Die Namensregeln aus Schritt 4 (cfg/scl_naming.ini) arbeiten
spaeter genauso: Wert ersetzen, Merker loeschen.
"""
from __future__ import annotations
import re
from collections import deque
from datetime import datetime
from typing import TYPE_CHECKING
from pydantic import BaseModel, ConfigDict, Field
from model import JamArea, TroBase, TroDocument
from tro_catalog import TroItem
if TYPE_CHECKING:
from e_planning import EplanDocument
__all__ = [
"TODO",
"SclName",
"SclSeparator",
"SclSwitch",
"SclSwitchExit",
"SclScanner",
"SclJam",
"SclSscc",
"SclWorkStation",
"SclStoreLine",
"SclStore",
"SclTro",
"SclFile",
"build_context",
"flow_order",
"reachable_via",
]
TODO = "TODO(E-Planung)"
_CONFIG = ConfigDict(extra="forbid")
class SclName(BaseModel):
"""Ein Name/eine Adresse in der SCL und ob er noch offen ist."""
model_config = _CONFIG
value: str
# True: Platzhalter, muss aus der E-Planung kommen -> TODO-Kommentar
todo: bool = True
def _offen(platzhalter: str) -> SclName:
return SclName(value=platzhalter, todo=True)
def _bekannt(wert: str) -> SclName:
return SclName(value=wert, todo=False)
# Rolle in e_planning.ini -> wie der Name in der SCL geschrieben wird
# (Sensor als Konstante der DB_Inputs-Sensorliste, Ventil als Variable)
_SENSOR = "cIn{bmk}"
_VENTIL = "{bmk}"
class SclSeparator(BaseModel):
"""Ein stInSeparator<n>-Block."""
model_config = _CONFIG
n: int
# Separatornummer aus dem Layout ("?" wenn der Typ mehr Separatoren hat
# als im Layout gefunden wurden)
layout: str
no: SclName
sensor: SclName
stopper: SclName
# Teilesensor (stSenPart) - nur in Templates, deren Referenz ihn hat (1Sep2Swi)
part_sensor: SclName
class SclSwitchExit(BaseModel):
"""Ein Abgang eines Weichen-TRO und die darueber erreichbaren TROs."""
model_config = _CONFIG
via: str
targets: list[str]
class SclSwitch(BaseModel):
"""Ein stInSwitch<n>-Block plus sein FC_Direction-Aufruf."""
model_config = _CONFIG
n: int
no: SclName
valve: SclName
# None: mehrere Weichen teilen sich die Abgaenge - Aufteilung ist eine
# Planungsentscheidung und wird nicht geraten
to1_destinations: int | None
# Ziel von nOutDirection, z. B. "#TRO07.nInDirection" bzw. "...nInDirection2"
direction_target: str
# Staubereich, dessen erster Traeger in FC_Direction ausgewertet wird
jam_db: SclName
class SclScanner(BaseModel):
"""Ein stInBarcodeReader-Block ("" als Suffix, wenn es nur einen gibt).
Die Cognex-Verdrahtung (nur in den Templates): ``interface`` ist der
Schluessel in #arCognexInterface (z. B. "cScannerBX2100"), ``tag`` der
Praefix der Hardware-Symbole ("bx2100-TriggerReady" ...), ``hw_results``
das Ergebnis-Symbol der Kamera.
"""
model_config = _CONFIG
n: int
suffix: str
interface: SclName
tag: SclName
hw_results: SclName
class SclJam(BaseModel):
"""Ein Staubereich am Ein- oder Ausgang (JamEntr<n> / JamExit<n>).
Ein Eingang ist ein Staubereich des TRO selbst (TroDocument.jamAreas), ein
Ausgang der Eingangs-Staubereich des Nachfolgers.
"""
model_config = _CONFIG
n: int
# Kennung aus der TRO-JSON (J<Separator>); "" am Ausgang zu einer Station
jam_id: str = ""
# Eingang: die Vorgaenger, Ausgang: der Nachfolger - als Text fuer den
# Kommentar, SYSn als Station markiert
neighbour: str
db: SclName
# Stausensor des Ausgangs (stInSenJam<n>); None an Eingaengen
sensor: SclName | None = None
# Traeger-Array und Staudaten des Bereichs - im Normalfall <db>.arCarrier
# bzw. <db>.stData; an Uebergaben auch eigene Variablen (#arCarrierHandover...)
carriers: SclName
data: SclName
@property
def title(self) -> str:
"""Fuer den Kommentar: 'Staubereich J0003' (ohne Kennung nur 'Staubereich')."""
return f"Staubereich {self.jam_id}" if self.jam_id else "Staubereich"
class SclSscc(BaseModel):
"""SSCC-Scanner eines 1Sep_SSCC: zweite Kamera, Endmessung, HMI-Anzeige."""
model_config = _CONFIG
# interface ist hier die Struktur selbst (z. B. "#stCognexIntfTro105")
scanner: SclScanner
end_sensor: SclName
# Schluessel der HMI-Anzeige, z. B. "SSCC1107"
hmi: SclName
class SclWorkStation(BaseModel):
"""Arbeitsplatz eines Vario_workStation."""
model_config = _CONFIG
station_sensor: SclName
station_release: SclName
loaded_error_sensor: SclName
class SclStoreLine(BaseModel):
"""Eine Speicherlinie eines PinStore_Auto (arInSep[n])."""
model_config = _CONFIG
n: int
# Separatornummer der Linie aus dem Layout
layout: str
jam_length: SclName
sensor: SclName
jam_sensor: SclName
stopper: SclName
class SclStore(BaseModel):
"""Alles fuer ein automatisches Pin-Lager (PinStore_Auto)."""
model_config = _CONFIG
encoder: SclName
dest: SclName
conv_stations: SclName
stations_max: SclName
threshold: SclName
reference_sensor: SclName
station: SclName
quit_local: SclName
test_target: SclName
run_in: SclName
run_out: SclName
db: SclName
removal: SclName
conv_db: SclName
# Einschleus-Separator (Pin) - aus dem Layout, wenn tro_flow einen
# steuernden Separator vor den Linien gefunden hat, sonst "?"
pin_layout: str
pin_sensor: SclName
pin_stopper: SclName
# Eine Linie je Speicherbahn im Layout (nicht die 19 aus dem Katalog)
lines: list[SclStoreLine]
@property
def title(self) -> str:
"""Fuer den Kommentar: 'Staubereich J0003' (ohne Kennung nur 'Staubereich')."""
return f"Staubereich {self.jam_id}" if self.jam_id else "Staubereich"
class SclTro(BaseModel):
"""Alles, was eine REGION fuer einen TRO braucht."""
model_config = _CONFIG
id: str
# Nummernteil der ID, wie geschrieben ("TRO01" -> "01"); "" ohne Ziffern
nr: str
type: str
fb: str
region_title: str
layout_separators: list[str]
position: tuple[float, float] | None
predecessors: list[str]
successors: list[str]
confidence: str
separators: list[SclSeparator]
switches: list[SclSwitch]
switch_exits: list[SclSwitchExit]
has_vario: bool
# Vario-Sensoren in Aufrufreihenfolge (Parametername -> Sensor); leer ohne Vario
vario_sensors: dict[str, SclName]
scanners: list[SclScanner]
storage_lines: int
jam_entries: list[SclJam]
jam_exits: list[SclJam]
main_tro: SclName
safety: SclName
release: str
motor_protection: SclName
vario_motor: SclName
machine_state: SclName
# Nur in den Templates (Referenz-Aufrufstellen), nicht im generischen Aufbau:
# UDT des Prioritaetsmanagers ("udt_TRO107") und ob Richtung 2 Vorrang hat
priority_udt: SclName
prio_dir2: SclName
# Kreisel-Freigabe (xInCarouselRun1). Ob ein TRO am Kreisel liegt, steht
# nicht in der TRO-JSON - deshalb immer offen (doc/SCL_Templates.md §6).
carousel_run: SclName
# -- nur fuer die Templates (Referenz-Aufrufstellen, doc/SCL_Templates.md §6)
# Einziger Staubereich am Ein-/Ausgang fuer FBs ohne Nummerierung
# (Vario: JamEntr/JamFinger, PinStore: JamEntr/JamExit): der erste
# Vorgaenger bzw. Nachfolger, sonst ein offener Platzhalter
jam_in: SclJam
jam_out: SclJam
error_quit: SclName
automatic: SclName
# Vario: stInSettings.xIsBorner (Bedeutung mit SPS klaeren)
is_border: SclName
# Station fuer FC_CARR_GLOB_CORRECTION (1Sep_SSCC, PinStore_Auto)
correction_station: SclName
sscc: SclSscc | None = None
work_station: SclWorkStation | None = None
store: SclStore | None = None
@property
def has_scanner(self) -> bool:
return bool(self.scanners)
class SclFile(BaseModel):
"""Kontext der ganzen Datei: Kopf und die TROs in Fliessrichtung."""
model_config = _CONFIG
source: str
created: str
start_reason: str
tros: list[SclTro] = Field(default_factory=list)
# Zuordnung zur E-Planung, aus der Namen eingesetzt wurden ("" = keine)
eplan_source: str = ""
# ---------------------------------------------------------------------------
# Graph
# ---------------------------------------------------------------------------
def flow_order(tros: list[TroBase], start_id: str = "") -> tuple[list[TroBase], str]:
"""TROs in Fliessrichtung sortieren.
Startpunkt ist ein TRO ohne Vorgaenger - also die Stelle, an der Material
in die Anlage kommt. Solange Be-/Entladestation fehlen, ist der Fluss ein
geschlossener Kreis und es gibt keinen solchen TRO; dann muss der Kreis
irgendwo aufgetrennt werden. Das ist eine willkuerliche Wahl, deshalb wird
sie zurueckgegeben und im Kopf der SCL vermerkt (und laesst sich mit
--start ueberschreiben). Nicht erreichte TROs haengt die Funktion hinten
an, damit nie einer verlorengeht.
"""
nach_id = {t.tro_id: t for t in tros}
ohne_vorgaenger = sorted(t.tro_id for t in tros if not t.predecessors)
# Materialeingang ueber eine Station (SYSn von tro_flow.py): der TRO, den
# sie speist, ist der natuerliche Anfang - auch wenn er zusaetzlich einen
# TRO-Vorgaenger hat (Kreisel-Umlauf).
eingaenge = sorted({
t.tro_id for t in tros
for vor in t.predecessors if vor not in nach_id
})
if start_id and start_id in nach_id:
start, grund = [start_id], "per --start gesetzt"
elif ohne_vorgaenger:
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
elif eingaenge:
stationen = sorted({
vor for i in eingaenge for vor in nach_id[i].predecessors
if vor not in nach_id
})
start = eingaenge
grund = f"Materialeingang ueber {', '.join(stationen)}"
else:
start = [min(nach_id)]
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
"Be-/Entladestation im Export stehen, ergibt sich der Anfang")
gesehen: list[str] = []
queue = deque(start)
while queue:
tro_id = queue.popleft()
if tro_id in gesehen:
continue
gesehen.append(tro_id)
for nxt in nach_id[tro_id].successors if tro_id in nach_id else []:
# SYSn (Materialausgang) ist kein TRO - keine REGION, keine Instanz.
if nxt in nach_id and nxt not in gesehen:
queue.append(nxt)
for tro_id in sorted(nach_id):
if tro_id not in gesehen:
gesehen.append(tro_id)
return [nach_id[i] for i in gesehen], grund
def reachable_via(start: str, first: str, tros: dict[str, TroBase]) -> list[str]:
"""Welche TROs sind ueber diesen Abgang erreichbar?
Das ist die Zielliste eines Weichenabgangs: von `first` aus weiterlaufen,
ohne ueber die Weiche selbst zurueckzugehen. Genau die Information, die
FC_Direction braucht, um zu entscheiden - und sie steht im Graphen.
"""
gesehen: set[str] = set()
queue = deque([first])
while queue:
node = queue.popleft()
if node in gesehen or node == start:
continue
gesehen.add(node)
if node in tros:
queue.extend(tros[node].successors)
return sorted(gesehen)
def _nachbar(node: str, alle: dict[str, TroBase]) -> str:
"""Nachbar fuer Kommentare; SYSn als Station kenntlich machen."""
return node if node in alle else f"{node} (Station, kein TRO)"
def _region_title(tro: TroBase) -> str:
seps = ", ".join(tro.separators)
teile = f" Sep {seps}" if seps else ""
return f"{tro.tro_id} ({tro.type_name}){teile}"
def _nummer(tro_id: str) -> str:
treffer = re.search(r"(\d+)$", tro_id)
return treffer.group(1) if treffer else ""
# ---------------------------------------------------------------------------
# Kontext bauen
# ---------------------------------------------------------------------------
def _scanner(n: int, suffix: str, interface: str = "cScanner____") -> SclScanner:
return SclScanner(
n=n, suffix=suffix, interface=_offen(interface), tag=_offen("bx____"),
hw_results=_offen("BX____~Ergebnisse_\u2013_64_Bytes_1"),
)
def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None,
jam_id: str = "") -> SclJam:
return SclJam(
n=n, jam_id=jam_id, neighbour=neighbour, db=_offen(jam_db), sensor=sensor,
carriers=_offen(f"{jam_db}.arCarrier"), data=_offen(f"{jam_db}.stData"),
)
def _store(tro: TroBase, eplan: EplanDocument | None = None) -> SclStore:
"""PinStore_Auto: eine Linie je Speicherbahn (Host) des Layouts.
tro_flow legt die Separatoren als [steuernde ..., Linien ...] ab; die
Linien sind die letzten len(hosts). Fehlen Hosts (fremde JSON), zaehlt
jeder Separator als Linie. Sensor und Stopper jeder Linie (und des
Pin-Separators, falls tro_flow einen gefunden hat) kommen aus der
E-Planung wie bei den Separatorbloecken.
"""
hosts = list(getattr(tro, "hosts", []) or [])
seps = list(tro.separators)
anzahl = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
steuer, linien = seps[:len(seps) - anzahl], seps[len(seps) - anzahl:]
pin_sensor, pin_stopper = _sep_namen(steuer[0], eplan) if steuer else (
_offen("cInBG____"), _offen("MB____"))
zeilen = []
for n, sep in enumerate(linien, start=1):
sensor, stopper = _sep_namen(sep, eplan)
zeilen.append(SclStoreLine(n=n, layout=sep,
jam_length=_offen('"cStorage____ExitJam"'),
sensor=sensor, jam_sensor=_offen("cInBG____"),
stopper=stopper))
return SclStore(
encoder=_offen("#fbEncoder____"),
dest=_offen("cDest____"),
conv_stations=_offen("cHooksSorterMaxPin____"),
stations_max=_offen("cStationsMax____"),
threshold=_offen("cNoPositionsBlock____"),
reference_sensor=_offen("cInBG____"),
station=_offen("____"),
quit_local=_offen('"SF____"'),
test_target=_offen('"IBN".nTestTarget____'),
run_in=_offen("#fbSortConv____.xOutConveyorFullSpeed"),
run_out=_offen("#fbExitConv____.xOutConveyorFullSpeed"),
db=_offen('"DB_Storage____"'),
removal=_offen('"DB_HandshakeStorage".Str_StorageRemoval.'
'arrudt_StorageRemovalHandshakeTROsLvl1[____]'),
conv_db=_offen('"DB_ConveyorStations".stConveyorStations____'),
pin_layout=", ".join(steuer) or "?",
pin_sensor=pin_sensor,
pin_stopper=pin_stopper,
lines=zeilen,
)
def _sep_namen(layout: str, eplan: EplanDocument | None) -> tuple[SclName, SclName]:
"""Sensor und Stopperventil eines Layout-Separators aus der E-Planung.
Gilt fuer die Separatorbloecke (stInSeparator<n>) und genauso fuer die
Linien eines PinStore_Auto - beide sind Separatoren im Layout. Ohne
Zuordnung bleiben beide offen.
"""
rollen = eplan.for_layout(layout) if eplan is not None and layout != "?" else {}
sensor = rollen.get("sensor")
stopper = rollen.get("stopper")
return (_bekannt(_SENSOR.format(bmk=sensor.bmk)) if sensor else _offen("cInBG____"),
_bekannt(_VENTIL.format(bmk=stopper.bmk)) if stopper else _offen("MB____"))
def _separator(i: int, layout: str, eplan: EplanDocument | None) -> SclSeparator:
sensor, stopper = _sep_namen(layout, eplan)
return SclSeparator(
n=i,
layout=layout,
no=_offen("cSep_____"),
sensor=sensor,
stopper=stopper,
# Teilesensor kennt die E-Planung-Bruecke noch nicht - offen
part_sensor=_offen("cInBG____"),
)
def _entries(tro: TroBase, eigene: list[JamArea],
alle: dict[str, TroBase], jam_db: str) -> list[SclJam]:
"""Ein JamEntr je eigenem Staubereich - nicht je Vorgaenger: mehrere
Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05)."""
eingaenge = []
for area in eigene:
von = ", ".join(_nachbar(v, alle) for v in area.predecessors) or "-"
if area.note:
von = f"{von} ({area.note})"
eingaenge.append(_jam(area.entry, von, jam_db, jam_id=area.jam_id))
return eingaenge
def _exit_area(tro_id: str, nach: str,
bereiche: dict[str, list[JamArea]]) -> str:
"""Kennung des Staubereichs, in den tro_id beim Nachfolger einfaehrt."""
ziel = bereiche.get(nach, [])
passend = [a for a in ziel if tro_id in a.predecessors] or ziel
return passend[0].jam_id if passend else ""
def _tro_context(tro: TroBase, alle: dict[str, TroBase],
eplan: EplanDocument | None = None,
bereiche: dict[str, list[JamArea]] | None = None) -> SclTro:
items = tro.items
n_sep = items.get(TroItem.SEPARATOR, 1)
n_swi = items.get(TroItem.SWITCH, 0)
n_scan = items.get(TroItem.SCANNER, 0)
tro_id = tro.tro_id
jam_db = '"DB_JamArea____".stJam____'
bereiche = bereiche or {}
separators = [
_separator(i, tro.separators[i - 1] if i <= len(tro.separators) else "?", eplan)
for i in range(1, n_sep + 1)
]
switches = [
SclSwitch(
n=j,
no=_offen("cSwi_____"),
valve=_offen("MB____"),
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
to1_destinations=len(tro.successors) if n_swi == 1 else None,
direction_target=(f"#{tro_id}.nInDirection" if n_swi == 1
else f"#{tro_id}.nInDirection{j}"),
jam_db=_offen(jam_db),
)
for j in range(1, n_swi + 1)
]
has_vario = bool(items.get(TroItem.VARIO_DRIVE))
vario_sensors = {
name: _offen("cInBG____")
for name in ("stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam")
} if has_vario else {}
switch_exits = [
SclSwitchExit(via=abgang, targets=reachable_via(tro_id, abgang, alle))
for abgang in tro.successors
] if n_swi else []
# Staubereiche aus der TRO-JSON (jamAreas): Eingaenge sind die eigenen
# (ein JamEntr je eigenem Staubereich, nicht je Vorgaenger), Ausgaenge die
# Eingaenge der Nachfolger. Der DB-Name braucht die Foerderernummer aus der
# Hardwareplanung und bleibt offen.
jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, jam_db)
jam_exits = [
_jam(n, _nachbar(nach, alle), jam_db, sensor=_offen("cInBG____"),
jam_id=_exit_area(tro_id, nach, bereiche))
for n, nach in enumerate(tro.successors, start=2)
]
return SclTro(
id=tro_id,
nr=_nummer(tro_id),
type=tro.type_name,
fb=tro.fb_block or "FB_UNBEKANNT",
region_title=_region_title(tro),
layout_separators=list(tro.separators),
position=(tro.x, tro.y) if tro.position else None,
predecessors=list(tro.predecessors),
successors=list(tro.successors),
confidence=tro.confidence,
separators=separators,
switches=switches,
switch_exits=switch_exits,
has_vario=has_vario,
vario_sensors=vario_sensors,
scanners=[
_scanner(i, "" if n_scan == 1 else str(i))
for i in range(1, n_scan + 1)
],
storage_lines=items.get(TroItem.STORAGE_LINE, 0),
jam_entries=jam_entries,
jam_exits=jam_exits,
main_tro=_offen("cMainTro____"),
safety=_offen('"DB_InterfaceSafety".stFromSafety.EStopMain.Q'),
release=f'"DB_TRO_TEST".{tro_id}',
motor_protection=_offen('"FC____"'),
vario_motor=_offen("MA____"),
# Schaltschrank ist anlagenspezifisch ("...OF1" in der Referenz)
machine_state=_offen("#stInOutControlUnitCabinet"),
priority_udt=_offen("udt_____"),
# Vorschlag aus der Referenz TRO107, Prioritaet bleibt Planungsentscheidung
prio_dir2=_offen("TRUE"),
carousel_run=_offen("#fbCarousel____.xOutConveyorFullSpeed"),
# dasselbe Objekt wie der erste Ein-/Ausgang: ein Name, den die
# Namensregeln dort setzen, gilt damit auch hier
jam_in=jam_entries[0] if jam_entries else _jam(1, "-", jam_db),
jam_out=jam_exits[0] if jam_exits else _jam(2, "-", jam_db),
error_quit=_offen('"SF____"'),
automatic=_offen("#t_automatik"),
is_border=_offen("TRUE"),
correction_station=_offen("0"),
sscc=(SclSscc(scanner=_scanner(1, "SSCC", "#stCognexIntf____"),
end_sensor=_offen("cInBG____"), hmi=_offen("SSCC____"))
if items.get(TroItem.SSCC) else None),
work_station=(SclWorkStation(station_sensor=_offen("cInBG____"),
station_release=_offen('#"rTrig_Workstation ____".CLK'),
loaded_error_sensor=_offen("cInBG____"))
if items.get(TroItem.WORK_STATION) else None),
store=_store(tro, eplan) if items.get(TroItem.STORAGE_LINE) else None,
)
def build_context(document: TroDocument, source: str, start_id: str = "",
created: str | None = None,
eplan: EplanDocument | None = None) -> SclFile:
"""TroDocument -> SclFile (TROs in Fliessrichtung).
eplan: Zuordnung Layout -> E-Planung (e_planning.py); ohne sie bleiben
alle Namen offen und die Ausgabe ist die bisherige.
"""
alle = {t.tro_id: t for t in document.tros}
bereiche: dict[str, list[JamArea]] = {}
for area in document.jam_areas_or_derived():
bereiche.setdefault(area.tro, []).append(area)
geordnet, startgrund = flow_order(document.tros, start_id)
return SclFile(
source=source,
created=created or datetime.now().strftime("%Y-%m-%d %H:%M"),
start_reason=startgrund,
tros=[_tro_context(t, alle, eplan, bereiche) for t in geordnet],
eplan_source=(f"{eplan.eplan_file} (Zuordnung vom {eplan.created})"
if eplan is not None else ""),
)
+345 -204
View File
@@ -7,11 +7,14 @@ scl_skeleton.py - FB_Main-Geruest als SCL aus der TRO-Liste erzeugen.
Letzter Schritt der Kette:
material_flow.py Objekte -> Materialfluss
tro_flow.py Fluss -> TROs
tro_annotate.py TROs -> Marker in der Zeichnung
tro_extract.py Zeichnung -> JSON
tro_flow.py Fluss -> TROs -> <name>_tro.json (--json)
scl_skeleton.py JSON -> FB_Main.scl <- hier
Die TRO-JSON wird ueber das gemeinsame Modell lib/model.py (TroDocument)
eingelesen und dabei geprueft: ein falscher oder umbenannter Schluessel bricht
mit einer Fehlermeldung ab, statt still einen Standardwert zu nehmen. Bauteile
(items) und FB-Typ kommen aus tro_catalog, ueber den TRO-Typ.
Erzeugt wird ein **Geruest**, kein fertiges Programm: je TRO eine REGION mit
Instanzaufruf und allen Parameterzeilen in der richtigen Reihenfolge, so wie es
die realen Aufrufstellen in doc/TRO_Katalog/scl_templates/*.scl zeigen.
@@ -37,13 +40,61 @@ from __future__ import annotations
import argparse
import json
import sys
from collections import deque
from datetime import datetime
from pathlib import Path
from material_flow import env_dir
from jinja2 import (
Environment,
FileSystemLoader,
StrictUndefined,
TemplateError,
TemplateNotFound,
)
from pydantic import ValidationError
TODO = "TODO(E-Planung)"
from e_planning import EPLAN_SCHEMA, EplanDocument
from material_flow import env_dir
from model import SCHEMA, TroDocument
from scl_context import TODO, SclFile, SclName, SclTro, build_context
# Parametrisierte Aufrufstellen je FB (doc/SCL_Templates.md); Dateiname = fbType.
TEMPLATE_DIR = Path(__file__).resolve().parent / "scl_templates"
TEMPLATE_SUFFIX = ".scl.j2"
class SclTemplates:
"""Die FB-Templates eines Ordners; ohne Template -> generischer Aufbau."""
def __init__(self, folder: Path) -> None:
self.folder = folder
self.env = Environment(
loader=FileSystemLoader(str(folder)),
# Tippfehler im Template -> Fehler statt leerer SCL-Stelle
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
keep_trailing_newline=True,
autoescape=False,
)
self.env.globals["TODO"] = TODO
def has(self, fb: str) -> bool:
return (self.folder / f"{fb}{TEMPLATE_SUFFIX}").is_file()
def render(self, tro: SclTro) -> str:
"""REGION eines TRO aus seinem Template (ohne abschliessenden Umbruch)."""
try:
template = self.env.get_template(f"{tro.fb}{TEMPLATE_SUFFIX}")
except TemplateNotFound as exc:
raise TemplateError(f"Template fuer {tro.fb} fehlt: {exc}") from exc
# Leerzeilen hier zusammenfassen statt im Template mit Whitespace-
# Steuerung ({%- ... -%}) - die macht Templates schwer lesbar und
# bricht leicht die Einrueckung.
zeilen: list[str] = []
for zeile in template.render(t=tro).split("\n"):
zeile = zeile.rstrip()
if zeile or (zeilen and zeilen[-1]):
zeilen.append(zeile)
return "\n".join(zeilen).strip("\n")
# Zeitwerte aus den Referenz-Aufrufstellen. Sie sind plausible Startwerte, KEINE
# fuer diese Anlage ermittelten - deshalb ueberall mit Pruefhinweis.
@@ -55,123 +106,63 @@ DEFAULT_SEP_TIMES = (
)
def load_tros(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def load_document(path: Path) -> TroDocument:
"""TRO-JSON einlesen und gegen das Modell pruefen."""
return TroDocument.from_json(path.read_text(encoding="utf-8"))
def flow_order(tros: list[dict], start_id: str = "") -> tuple[list[dict], str]:
"""TROs in Fliessrichtung sortieren.
Startpunkt ist ein TRO ohne Vorgaenger - also die Stelle, an der Material
in die Anlage kommt. Solange Be-/Entladestation fehlen, ist der Fluss ein
geschlossener Kreis und es gibt keinen solchen TRO; dann muss der Kreis
irgendwo aufgetrennt werden. Das ist eine willkuerliche Wahl, deshalb wird
sie zurueckgegeben und im Kopf der SCL vermerkt (und laesst sich mit
--start ueberschreiben). Nicht erreichte TROs haengt die Funktion hinten
an, damit nie einer verlorengeht.
"""
nach_id = {t["id"]: t for t in tros}
ohne_vorgaenger = sorted(t["id"] for t in tros if not t.get("predecessors"))
if start_id and start_id in nach_id:
start, grund = [start_id], "per --start gesetzt"
elif ohne_vorgaenger:
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
else:
start = [min(nach_id)]
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
"Be-/Entladestation im Export stehen, ergibt sich der Anfang")
gesehen: list[str] = []
queue = deque(start)
while queue:
tro_id = queue.popleft()
if tro_id in gesehen:
continue
gesehen.append(tro_id)
for nxt in nach_id.get(tro_id, {}).get("successors", []):
if nxt not in gesehen:
queue.append(nxt)
for tro_id in sorted(nach_id):
if tro_id not in gesehen:
gesehen.append(tro_id)
return [nach_id[i] for i in gesehen], grund
def _todo(name: SclName, abstand: str) -> str:
"""TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist."""
return f"{abstand}// {TODO}" if name.todo else ""
def reachable_via(start: str, first: str, tros: dict[str, dict]) -> list[str]:
"""Welche TROs sind ueber diesen Abgang erreichbar?
Das ist die Zielliste eines Weichenabgangs: von `first` aus weiterlaufen,
ohne ueber die Weiche selbst zurueckzugehen. Genau die Information, die
FC_Direction braucht, um zu entscheiden - und sie steht im Graphen.
"""
gesehen: set[str] = set()
queue = deque([first])
while queue:
node = queue.popleft()
if node in gesehen or node == start:
continue
gesehen.add(node)
queue.extend(tros.get(node, {}).get("successors", []))
return sorted(gesehen)
def region_title(tro: dict) -> str:
seps = ", ".join(tro.get("separators", []))
teile = f" Sep {seps}" if seps else ""
return f"{tro['id']} ({tro['type']}){teile}"
def emit_separators(add, tro: dict, anzahl: int, hat_scanner: bool) -> None:
seps = tro.get("separators", [])
for i in range(1, anzahl + 1):
pfad = f"#{tro['id']}.stInSeparator{i}"
layout = seps[i - 1] if i <= len(seps) else "?"
add(f" // Separator {i} = Bauteil {layout} aus dem Layout")
add(f' {pfad}.Settings.nSeparatorNo := "cSep_____"; // {TODO}')
def emit_separators(add, tro: SclTro) -> None:
for s in tro.separators:
pfad = f"#{tro.id}.stInSeparator{s.n}"
add(f" // Separator {s.n} = Bauteil {s.layout} aus dem Layout")
add(f' {pfad}.Settings.nSeparatorNo := "{s.no.value}";{_todo(s.no, " ")}')
for name, wert, hinweis in DEFAULT_SEP_TIMES:
kommentar = f" // {TODO}: {hinweis}" if hinweis else ""
add(f" {pfad}.Settings.{name} := {wert};{kommentar}")
add(f" {pfad}.Settings.xPluggedJam := TRUE;")
add(f" {pfad}.Settings.xPluggedPart := FALSE;")
add(f" {pfad}.Settings.xPluggedScanner := "
f"{'TRUE' if hat_scanner else 'FALSE'};")
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
f"{'TRUE' if tro.has_scanner else 'FALSE'};")
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["{s.sensor.value}"];'
f'{_todo(s.sensor, " ")}')
add(f" {pfad}.Settings.tSenFree := T#1500ms; // {TODO}: pruefen")
add(f" {pfad}.Settings.tSenWait := T#4000ms; // {TODO}: pruefen")
add("")
def emit_switches(add, tro: dict, anzahl: int, alle: dict[str, dict]) -> None:
abgaenge = tro.get("successors", [])
for j in range(1, anzahl + 1):
pfad = f"#{tro['id']}.stInSwitch{j}"
add(f' {pfad}.Settings.nSwitchNo := "cSwi_____"; // {TODO}')
def emit_switches(add, tro: SclTro) -> None:
for w in tro.switches:
pfad = f"#{tro.id}.stInSwitch{w.n}"
add(f' {pfad}.Settings.nSwitchNo := "{w.no.value}";{_todo(w.no, " ")}')
add(f" {pfad}.Settings.xPluggedExSen3 := FALSE;")
add(f" {pfad}.Settings.xPluggedExSen4 := FALSE;")
if anzahl == 1:
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
add(f" {pfad}.Settings.nTo1Destinations := {len(abgaenge)};"
if w.to1_destinations is not None:
add(f" {pfad}.Settings.nTo1Destinations := {w.to1_destinations};"
f" // Abgaenge laut Topologie")
else:
# Bei mehreren Weichen ist es eine Planungsentscheidung, welche
# Weiche welche Abgaenge bedient - die Summe ist bekannt, die
# Aufteilung nicht. Deshalb hier kein geratener Wert.
add(f" {pfad}.Settings.nTo1Destinations := _;"
f" // {TODO}: dieser TRO hat {len(abgaenge)} "
f"Abgaenge auf {anzahl} Weichen - Aufteilung festlegen")
f" // {TODO}: dieser TRO hat {len(tro.successors)} "
f"Abgaenge auf {len(tro.switches)} Weichen - Aufteilung festlegen")
add("")
if abgaenge:
if tro.switch_exits:
add(" // Zielliste je Abgang (aus dem TRO-Graphen berechnet) -")
add(" // Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:")
for abgang in abgaenge:
ziele = reachable_via(tro["id"], abgang, alle)
add(f" // ueber {abgang}: {', '.join(ziele) if ziele else '-'}")
for abgang in tro.switch_exits:
ziele = ", ".join(abgang.targets) if abgang.targets else "-"
add(f" // ueber {abgang.via}: {ziele}")
add("")
def emit_vario(add, tro: dict) -> None:
pfad = f"#{tro['id']}.stInVario"
def emit_vario(add, tro: SclTro) -> None:
pfad = f"#{tro.id}.stInVario"
add(f" {pfad}.Settings.xContinousEmptying := FALSE;")
add(f" {pfad}.Settings.tWaitForJog := T#300ms; // {TODO}: pruefen")
add(f" {pfad}.Settings.tConvEmpty := T#45000ms; // {TODO}: pruefen")
@@ -181,17 +172,18 @@ def emit_vario(add, tro: dict) -> None:
add(f" {pfad}.Settings.tCarrierInPosEdgeDelay := t#100ms;")
add(f" {pfad}.Settings.xJamSenDelayActive := TRUE;")
add(f" {pfad}.Settings.tJamSenDelay := T#2s;")
add(f' {pfad}.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
add(f' {pfad}.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
add(f' {pfad}.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
add(f' {pfad}.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
# Der Kommentar steht bei allen Sensoren in derselben Spalte
# (Parametername + Abstand = 16 Zeichen).
for name, sensor in tro.vario_sensors.items():
abstand = " " * (16 - len(name))
add(f' {pfad}.{name} := "DB_Inputs".Sensors["{sensor.value}"];'
f"{_todo(sensor, abstand)}")
add("")
def emit_barcode(add, tro: dict, anzahl: int) -> None:
for i in range(1, anzahl + 1):
suffix = "" if anzahl == 1 else str(i)
pfad = f"#{tro['id']}.stInBarcodeReader{suffix}"
def emit_barcode(add, tro: SclTro) -> None:
for scanner in tro.scanners:
pfad = f"#{tro.id}.stInBarcodeReader{scanner.suffix}"
add(f" {pfad}.Settings.xActivateCheckASCI := TRUE;")
add(f" {pfad}.Settings.tTimeout := T#3000ms;")
add(f' // {TODO}: Cognex-Interface verdrahten '
@@ -199,49 +191,55 @@ def emit_barcode(add, tro: dict, anzahl: int) -> None:
add("")
def emit_call(add, tro: dict, items: dict[str, int]) -> None:
def emit_call(add, tro: SclTro) -> None:
"""Aufrufstelle schreiben.
Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim
Zusammensetzen gesetzt - sonst landet es hinter dem "//" und ist Teil des
Kommentars, womit die Parameterliste nicht mehr uebersetzt.
"""
tro_id = tro["id"]
def offen(name: SclName, text: str = "") -> str:
if not name.todo:
return text
return f"{TODO} - {text}" if text else TODO
zeilen: list[tuple[str, str]] = [
('nInMainTroNo := "cMainTro____"', TODO),
('xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q', TODO),
(f'nInMainTroNo := "{tro.main_tro.value}"', offen(tro.main_tro)),
(f"xInSftyOk := {tro.safety.value}", offen(tro.safety)),
("xInAllRdyToStart := TRUE", ""),
(f'xInRelease := "DB_TRO_TEST".{tro_id}', ""),
(f"xInRelease := {tro.release}", ""),
]
if items.get("VarioDrive"):
zeilen.append(('xInMotorProtection := "FC____"', TODO))
if tro.has_vario:
zeilen.append((f"xInMotorProtection := {tro.motor_protection.value}",
offen(tro.motor_protection)))
zeilen.append(("xInAutomatic := #t_automatik", ""))
zeilen.append(("stInOutMachineState := #stInOutControlUnitCabinet", ""))
# Der generische Aufbau hat den Schaltschrank nie als offen markiert;
# das bleibt so, damit seine Ausgabe stabil ist (Golden-Files). Die
# Templates markieren ihn.
zeilen.append((f"stInOutMachineState := {tro.machine_state.value}", ""))
zeilen.append(("stInOutCognexInterface := #stCognexInterfaceDummy", ""))
# Staubereiche: je ankommender/abgehender Verbindung einer. Welche
# DB-Struktur dahintersteht, ist eine Planungsentscheidung - hier steht
# nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren.
for n, vor in enumerate(tro.get("predecessors", []), start=1):
zeilen.append((f'arInOutJamEntr{n} := "DB_JamArea____".stJam____.arCarrier',
f"{TODO} - Staubereich von {vor}"))
zeilen.append((f'stInOutJamEntr{n} := "DB_JamArea____".stJam____.stData', ""))
for n, nach in enumerate(tro.get("successors", []), start=2):
zeilen.append((f'arInOutJamExit{n} := "DB_JamArea____".stJam____.arCarrier',
f"{TODO} - Staubereich nach {nach}"))
zeilen.append((f'stInOutJamExit{n} := "DB_JamArea____".stJam____.stData', ""))
for jam in tro.jam_entries:
zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier",
offen(jam.db, f"{jam.title} von {jam.neighbour}")))
zeilen.append((f"stInOutJamEntr{jam.n} := {jam.db.value}.stData", ""))
for jam in tro.jam_exits:
zeilen.append((f"arInOutJamExit{jam.n} := {jam.db.value}.arCarrier",
offen(jam.db, f"{jam.title} vor {jam.neighbour}")))
zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", ""))
for i in range(1, items.get("Separator", 1) + 1):
zeilen.append((f'xOutStopper{i} => "MB____"', TODO))
if items.get("VarioDrive"):
zeilen.append(('xOutVarioMotor => "MA____"', TODO))
for j in range(1, items.get("Switch", 0) + 1):
zeilen.append((f'xOutSw{j}ExTo3 => "MB____"', TODO))
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro_id}', ""))
zeilen.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]',
TODO))
for s in tro.separators:
zeilen.append((f'xOutStopper{s.n} => "{s.stopper.value}"', offen(s.stopper)))
if tro.has_vario:
zeilen.append((f'xOutVarioMotor => "{tro.vario_motor.value}"',
offen(tro.vario_motor)))
for w in tro.switches:
zeilen.append((f'xOutSw{w.n}ExTo3 => "{w.valve.value}"', offen(w.valve)))
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro.id}', ""))
zeilen.append((f'nOutStateLast => "DB_GLOB_TroState".arTroState["{tro.main_tro.value}"]',
offen(tro.main_tro)))
kopf = f" #{tro_id}("
kopf = f" #{tro.id}("
einzug = " " * len(kopf)
for index, (code, kommentar) in enumerate(zeilen):
trenner = ");" if index == len(zeilen) - 1 else ","
@@ -250,34 +248,75 @@ def emit_call(add, tro: dict, items: dict[str, int]) -> None:
add(f"{text} // {kommentar}" if kommentar else text)
def emit_direction(add, tro: dict, anzahl: int) -> None:
def emit_direction(add, tro: SclTro) -> None:
"""Weichenlogik - je Weiche ein FC_Direction-Aufruf."""
add("")
add(" REGION Direction")
for j in range(1, anzahl + 1):
ziel = (f"#{tro['id']}.nInDirection" if anzahl == 1
else f"#{tro['id']}.nInDirection{j}")
add(f' "FC_Direction"(nInSwitchNo := "cSwi_____", // {TODO}')
add(f" nOutDirection => {ziel},")
add(f' InOutCarrier := "DB_JamArea____".stJam____'
f".arCarrier[1]);")
if j < anzahl:
for w in tro.switches:
add(f' "FC_Direction"(nInSwitchNo := "{w.no.value}",{_todo(w.no, " ")}')
add(f" nOutDirection => {w.direction_target},")
add(f" InOutCarrier := {w.jam_db.value}.arCarrier[1]);")
if w.n < len(tro.switches):
add("")
add(" END_REGION")
def render(daten: dict, quelle: str, start_id: str = "") -> str:
tros = daten["tros"]
alle = {t["id"]: t for t in tros}
geordnet, startgrund = flow_order(tros, start_id)
def emit_region(add, tro: SclTro) -> None:
"""Eine REGION je TRO (heutiger, generischer Aufbau aus den Bauteilen)."""
add("")
add(f"REGION {tro.region_title}")
if tro.position:
add(f" // Anlagenkoordinate x/y: {tro.position[0]:.0f} / {tro.position[1]:.0f} mm")
add(f" // Vorgaenger: {', '.join(tro.predecessors) or '-'}")
add(f" // Nachfolger: {', '.join(tro.successors) or '-'}")
if tro.confidence:
add(f" // Einstufung: {tro.confidence}")
add("")
emit_separators(add, tro)
if tro.switches:
emit_switches(add, tro)
if tro.has_vario:
emit_vario(add, tro)
if tro.scanners:
emit_barcode(add, tro)
if tro.storage_lines:
add(f" // {TODO}: Linienspeicher mit {tro.storage_lines} Linien - "
f"Parametrierung je Linie")
add("")
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro.id)
add("")
emit_call(add, tro)
if tro.switches:
emit_direction(add, tro)
add("END_REGION")
def template_usage(context: SclFile, templates: SclTemplates | None
) -> tuple[list[str], list[str]]:
"""FB-Typen mit bzw. ohne Template (je sortiert, ohne Doppelte)."""
fbs = sorted({t.fb for t in context.tros})
if templates is None:
return [], fbs
return ([fb for fb in fbs if templates.has(fb)],
[fb for fb in fbs if not templates.has(fb)])
def render_context(context: SclFile, templates: SclTemplates | None = None) -> str:
"""SCL-Text aus dem fertigen Kontext.
Mit ``templates`` wird jeder TRO, fuer dessen FB ein Template existiert, aus
dem Template erzeugt, alle anderen generisch. Ohne ``templates`` ist die
Ausgabe die bisherige (Golden-Files in tests/fixtures/scl/).
"""
out: list[str] = []
add = out.append
add("// ============================================================")
add("// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py")
add(f"// Quelle : {quelle}")
add(f"// Erzeugt: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
add(f"// Quelle : {context.source}")
add(f"// Erzeugt: {context.created}")
add("//")
add("// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum")
add("// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem")
@@ -290,58 +329,39 @@ def render(daten: dict, quelle: str, start_id: str = "") -> str:
add("// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.")
add("//")
add("// Reihenfolge der REGIONs = Materialfluss.")
add(f"// Anfang bei {geordnet[0]['id']}: {startgrund}.")
add(f"// Anfang bei {context.tros[0].id}: {context.start_reason}.")
if context.eplan_source:
add("//")
add(f"// E-Planung: {context.eplan_source}")
add("// Separator-Sensoren und Stopperventile aus der Zuordnung sind")
add("// eingesetzt (ohne Marker); was die Zuordnung nicht sicher fand,")
add(f"// bleibt '{TODO}'. Details: <csv>_eplan.md (lib/e_planning.py).")
if templates is not None:
mit, ohne = template_usage(context, templates)
add("//")
add(f"// Aus Template : {', '.join(mit) or '-'}")
add(f"// Generisch : {', '.join(ohne) or '-'} (kein Template - von Hand pruefen)")
add("// ============================================================")
add("")
add('FUNCTION_BLOCK "FB_Main"')
add("{ S7_Optimized_Access := 'TRUE' }")
add("VERSION : 0.1")
add(" VAR ")
for tro in geordnet:
fb = tro.get("fbType") or "FB_UNBEKANNT"
kommentar = f" // {tro['type']}"
if tro.get("separators"):
kommentar += f", Sep {', '.join(tro['separators'])}"
add(f' {tro["id"]} : "{fb}";{kommentar}')
for tro in context.tros:
kommentar = f" // {tro.type}"
if tro.layout_separators:
kommentar += f", Sep {', '.join(tro.layout_separators)}"
add(f' {tro.id} : "{tro.fb}";{kommentar}')
add(" END_VAR")
add("")
add("BEGIN")
for tro in geordnet:
items = tro.get("items", {})
n_sep = items.get("Separator", 1)
n_swi = items.get("Switch", 0)
n_scan = items.get("Scanner", 0)
add("")
add(f"REGION {region_title(tro)}")
pos = tro.get("position")
if pos:
add(f" // Anlagenkoordinate x/y: {pos['x']:.0f} / {pos['y']:.0f} mm")
add(f" // Vorgaenger: {', '.join(tro.get('predecessors', [])) or '-'}")
add(f" // Nachfolger: {', '.join(tro.get('successors', [])) or '-'}")
if tro.get("confidence"):
add(f" // Einstufung: {tro['confidence']}")
add("")
emit_separators(add, tro, n_sep, hat_scanner=bool(n_scan))
if n_swi:
emit_switches(add, tro, n_swi, alle)
if items.get("VarioDrive"):
emit_vario(add, tro)
if n_scan:
emit_barcode(add, tro, n_scan)
if items.get("StorageLine"):
add(f" // {TODO}: Linienspeicher mit {items['StorageLine']} Linien - "
f"Parametrierung je Linie")
for tro in context.tros:
if templates is not None and templates.has(tro.fb):
add("")
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro["id"])
add("")
emit_call(add, tro, items)
if n_swi:
emit_direction(add, tro, n_swi)
add("END_REGION")
out.extend(templates.render(tro).split("\n"))
else:
emit_region(add, tro)
add("")
add("END_FUNCTION_BLOCK")
@@ -349,41 +369,106 @@ def render(daten: dict, quelle: str, start_id: str = "") -> str:
return "\n".join(out)
def render(document: TroDocument, quelle: str, start_id: str = "",
templates: SclTemplates | None = None) -> str:
"""TRO-JSON -> SCL-Text (Kontext bauen, dann ausgeben)."""
return render_context(build_context(document, quelle, start_id), templates)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
results = env_dir("SKEL_RESULTS", "results")
json_file = Path(args.json)
if not json_file.is_absolute() and json_file.parent == Path("."):
json_file = results / args.json
json_file = _in_results(args.json, results)
if not json_file.is_file():
print(f"FEHLER: JSON nicht gefunden: {json_file}", file=sys.stderr)
print(" Zuerst bin/tro_extract.bat laufen lassen.", file=sys.stderr)
print(" Zuerst bin/tro_flow.bat --file <name>.csv --json laufen lassen.",
file=sys.stderr)
return 1
try:
daten = load_tros(json_file)
except (OSError, ValueError) as exc:
document = load_document(json_file)
except OSError as exc:
print(f"FEHLER: {json_file} nicht lesbar: {exc}", file=sys.stderr)
return 1
if not daten.get("tros"):
except ValidationError as exc:
schema = _schema_of(json_file)
if schema and schema != SCHEMA:
# Anderes Format (z. B. die alte tro_extract-JSON) - die einzelnen
# Feldfehler helfen dann nicht weiter.
print(f"FEHLER: {json_file} hat das Schema '{schema}', erwartet wird "
f"'{SCHEMA}' ({exc.error_count()} Abweichungen).", file=sys.stderr)
print(" Mit bin/tro_flow.bat --file <name>.csv --json neu erzeugen.",
file=sys.stderr)
return 1
print(f"FEHLER: {json_file} passt nicht zum TRO-Modell (lib/model.py):",
file=sys.stderr)
print(exc, file=sys.stderr)
return 1
if not document.tros:
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
return 1
text = render(daten, json_file.name, args.start)
templates = None
if args.templates is not None:
ordner = Path(args.templates)
if not ordner.is_dir():
print(f"FEHLER: Template-Ordner nicht gefunden: {ordner}", file=sys.stderr)
return 1
templates = SclTemplates(ordner)
eplan = None
if args.eplan:
eplan_file = _in_results(args.eplan, results)
try:
eplan = EplanDocument.from_json(eplan_file.read_text(encoding="utf-8"))
except OSError as exc:
print(f"FEHLER: E-Planungs-Zuordnung nicht lesbar: {exc}", file=sys.stderr)
print(" Zuerst bin/e_planning.bat --file <name>.csv --eplan "
"<zeichnung>_positionsdraw.json laufen lassen.", file=sys.stderr)
return 1
except ValidationError as exc:
print(f"FEHLER: {eplan_file} passt nicht zum Schema '{EPLAN_SCHEMA}' "
f"({exc.error_count()} Abweichungen).", file=sys.stderr)
return 1
# Eine Zuordnung zu einem anderen Layout setzt falsche Namen ein -
# Separatornummern sind nur innerhalb eines CSV-Exports eindeutig.
if Path(eplan.csv_file).name != Path(document.source).name:
print(f"FEHLER: {eplan_file.name} gehoert zu '{eplan.csv_file}', die "
f"TRO-JSON zu '{document.source}'.", file=sys.stderr)
return 1
context = build_context(document, json_file.name, args.start, eplan=eplan)
mit, ohne = template_usage(context, templates)
if args.strict and ohne:
print(f"FEHLER: kein Template fuer {', '.join(ohne)} (--strict) - "
f"keine SCL geschrieben.", file=sys.stderr)
return 1
try:
text = render_context(context, templates)
except TemplateError as exc:
# StrictUndefined, Syntaxfehler, fehlendes Makro ... - lieber keine SCL
# als eine mit stillen Luecken.
print(f"FEHLER im Template: {exc}", file=sys.stderr)
return 1
out_file = results / (args.out or f"{json_file.stem.replace('_tro', '')}_FB_Main.scl")
out_file.write_text(text, encoding="utf-8")
offen = text.count(TODO)
geordnet, startgrund = flow_order(daten["tros"], args.start)
print("")
print("================================================================")
print("FB_MAIN-GERUEST")
print("================================================================")
print(f"Quelle = {json_file}")
print(f"TROs = {len(daten['tros'])}")
print(f"Reihenfolge = {' -> '.join(t['id'] for t in geordnet)}")
print(f"Anfang = {geordnet[0]['id']} ({startgrund})")
print(f"TROs = {len(context.tros)}")
print(f"Reihenfolge = {' -> '.join(t.id for t in context.tros)}")
print(f"Anfang = {context.tros[0].id} ({context.start_reason})")
if templates is not None:
print(f"Templates = {len(mit)} von {len(mit) + len(ohne)} Typen "
f"(generisch: {', '.join(ohne) or '-'})")
if eplan is not None:
print(f"E-Planung = {eplan_file}")
print(f"Eingesetzt = {_eingesetzt(context)} Namen aus der E-Planung")
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
print(f"SCL = {out_file}")
print("================================================================")
@@ -391,25 +476,58 @@ def main(argv: list[str] | None = None) -> int:
return 0
def _in_results(name: str, results: Path) -> Path:
"""Blosser Dateiname -> in SKEL_RESULTS; Pfadangaben bleiben unveraendert."""
path = Path(name)
if not path.is_absolute() and path.parent == Path("."):
return results / name
return path
def _eingesetzt(context: SclFile) -> int:
"""Wie viele Separator-Namen aus der E-Planung kamen (auch Speicherlinien)."""
namen = []
for tro in context.tros:
if tro.store is not None:
# Speicher: Pin-Separator und Linien; der Separatorblock wiederholt
# nur die erste Linie und wird im PinStore-Template nicht geschrieben
namen += [tro.store.pin_sensor, tro.store.pin_stopper]
namen += [n for linie in tro.store.lines for n in (linie.sensor, linie.stopper)]
else:
namen += [n for s in tro.separators for n in (s.sensor, s.stopper)]
return sum(not name.todo for name in namen)
def _schema_of(path: Path) -> str:
"""Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar)."""
try:
return str(json.loads(path.read_text(encoding="utf-8")).get("schema", ""))
except (OSError, ValueError, AttributeError):
return ""
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="scl_skeleton",
description="Erzeugt aus der extrahierten TRO-Liste ein FB_Main-Geruest "
description="Erzeugt aus der TRO-JSON (tro_flow --json) ein FB_Main-Geruest "
"als SCL zum Import in TIA Portal.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Beispiele:\n"
" scl_skeleton.bat\n"
" scl_skeleton.bat --json export_tro.json --out FB_Main.scl\n"
" scl_skeleton.bat --json mubea_tro.json --templates\n"
" scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json\n"
"\n"
"Exit-Codes:\n"
" 0 ok\n"
" 1 Eingabe- oder Aufruffehler\n",
" 0 ok (auch wenn TROs ohne Template generisch erzeugt wurden)\n"
" 1 Eingabe-, Aufruf- oder Templatefehler; mit --strict auch\n"
" ein FB-Typ ohne Template\n",
)
parser.add_argument(
"--json",
default="export_tro.json",
metavar="NAME",
help="TRO-JSON aus tro_extract.py in %%SKEL_RESULTS%% "
help="TRO-JSON aus tro_flow.py --json in %%SKEL_RESULTS%% "
"(oder ein vollstaendiger Pfad). Standard: %(default)s",
)
parser.add_argument(
@@ -424,6 +542,29 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help="TRO, bei dem die Reihenfolge beginnt. Nur noetig, solange der "
"Fluss ein geschlossener Kreis ohne Be-/Entladestation ist.",
)
parser.add_argument(
"--templates",
nargs="?",
const=str(TEMPLATE_DIR),
default=None,
metavar="DIR",
help="REGIONs aus den FB-Templates erzeugen (doc/SCL_Templates.md). "
"Ohne DIR: lib/scl_templates. FB-Typen ohne Template bleiben "
"generisch. Ohne diesen Schalter: bisherige Ausgabe.",
)
parser.add_argument(
"--eplan",
metavar="NAME",
help="Zuordnung zur E-Planung aus e_planning.py (<csv>_eplan.json in "
"%%SKEL_RESULTS%% oder ein Pfad): Separator-Sensoren und "
"Stopperventile werden eingesetzt statt als TODO markiert.",
)
parser.add_argument(
"--strict",
action="store_true",
help="Nur mit --templates: abbrechen (Exit 1), wenn ein FB-Typ der "
"Anlage kein Template hat.",
)
return parser.parse_args(argv)
+66
View File
@@ -0,0 +1,66 @@
{#-
FB_ILS_MTRO_1Sep - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep.scl
(TRO 100, =A01+UH01-KF00 FB_Main). Zeilen und Reihenfolge folgen
dieser Aufrufstelle; geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Separatorzeiten 600/200 ms wie Referenz, "pruefen"
tSenFree/tSenWait 1500/4000 ms wie Referenz, "pruefen"
stInSenJam<n> je Ausgang einer, Sensor offen
xReleaseOk TRUE wie Referenz (braucht keine Vorfahrtsregelung)
xInCarouselRun immer, offen - ob der TRO am Kreisel liegt, steht
nicht in der TRO-JSON; sonst Zeile loeschen
xInRelease "DB_TRO_TEST".<id> wie Referenz
stInOutMachineState Schaltschrank offen (anlagenspezifisch)
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#600ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
{% endfor %}
{{ m.jam_sensors(t) }}
{% for s in t.separators %}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1500ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4000ms; // {{ TODO }}: pruefen
{% endfor %}
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
{% set z = [] %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(("xInCarouselRun := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% for j in t.jam_entries %}
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% for j in t.jam_exits %}
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% for s in t.separators %}
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
{% endfor %}
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{{ m.call(t, z) }}
END_REGION
@@ -0,0 +1,64 @@
{#-
FB_ILS_MTRO_1Sep1Swi - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl
(TRO 107, =A01+UH01-KF00 FB_Main). Zeilen und Reihenfolge folgen
dieser Aufrufstelle; geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
stInSenJam<n> je Ausgang einer, Sensor offen
stInSettings.xPrioDir2 TRUE wie Referenz, aber offen (Planungsentscheidung)
PriorityManagerDir2 immer, UDT-Name offen
xInCarouselRun1 immer, offen - ob der TRO am Kreisel liegt, steht
nicht in der TRO-JSON; sonst Zeile loeschen
xInRelease TRUE wie Referenz (generisch: "DB_TRO_TEST")
Separatorzeiten 100/50 ms wie Referenz, "pruefen"
stInOutMachineState Schaltschrank offen (anlagenspezifisch)
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
{{ m.separator(t, s) }}
{% endfor %}
{{ m.jam_sensors(t) }}
{% for w in t.switches %}
{{ m.switch(t, w) }}
{% endfor %}
{{ m.ziele(t) }}
{% set z = [] %}
{% set _ = z.append(('stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroIn', m.note(t.priority_udt))) %}
{% set _ = z.append(('stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroOut', m.note(t.priority_udt))) %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(("xInCarouselRun1 := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
{% set _ = z.append(("xInRelease := TRUE", "")) %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% for j in t.jam_entries %}
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " von " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
{% endfor %}
{% for j in t.jam_exits %}
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " vor " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
{% endfor %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
{% for s in t.separators %}
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
{% endfor %}
{% for w in t.switches %}
{% set _ = z.append(("xOutSw" ~ w.n ~ 'ExTo3 => "' ~ w.valve.value ~ '"', m.note(w.valve))) %}
{% endfor %}
#{{ t.id }}.stInSettings.xPrioDir2 := {{ t.prio_dir2.value }};{{ m.todo(t.prio_dir2) }}
{{ m.call(t, z) }}
{{ m.direction(t) }}
END_REGION
@@ -0,0 +1,105 @@
{#-
FB_ILS_MTRO_1Sep2Swi - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl
(TRO 267 mit Scanner BX2100). Zeilen und Reihenfolge folgen dieser
Aufrufstelle; geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Separator tDelayToNextItem "_Medium", xPluggedPart TRUE und
stSenPart wie Referenz; keine Separatorzeiten
(stehen in der Referenz nicht)
stInSenJam<n> je Ausgang einer, Sensor offen
nSwitchNo nur Weiche 1 wie Referenz; fuer Weiche 2 fehlt die
Zeile in der Referenz - als Kommentar vorgeschlagen
xNewSwitch TRUE wie Referenz
nTo1Destinations Aufteilung der Abgaenge auf die Weichen offen
tTimeout Scanner 5 s wie Referenz
stInSettings.bSepType.%X2 TRUE wie Referenz, Bedeutung klaeren ("pruefen")
xInCarouselRun<n> je Weiche einer, offen (Kreisel steht nicht in der JSON)
nOutStateLast fehlt in der Referenz - nicht geschrieben
FC_Direction steht in der Referenz nicht an dieser Stelle -
nur die Zielliste als Kommentar
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Medium";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
#{{ t.id }}.stInSeparator{{ s.n }}.stSenPart := "DB_Inputs".Sensors["{{ s.part_sensor.value }}"];{{ m.todo(s.part_sensor) }}
{% endfor %}
{{ m.jam_sensors(t) }}
{% for w in t.switches %}
{% if loop.first %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}";{{ m.todo(w.no) }}
{% else %}
// #{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}"; // {{ TODO }}: pruefen - fehlt in der Referenz TRO267
{% endif %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen3 := FALSE;
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen4 := FALSE;
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xNewSwitch := TRUE;
{% if w.to1_destinations is not none %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := {{ w.to1_destinations }}; // Info noetig fuer HMI - {{ TODO }}: pruefen
{% else %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := _; // {{ TODO }}: {{ t.successors|length }} Abgaenge auf {{ t.switches|length }} Weichen - Aufteilung festlegen
{% endif %}
{% endfor %}
{{ m.ziele(t) }}
{% for sc in t.scanners %}
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.xActivateCheckASCI := TRUE;
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.tTimeout := T#5s;
{{ m.cognex_in(m.cognex(sc), sc) }}
{% endfor %}
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
#{{ t.id }}.stInSettings.bSepType.%X2 := TRUE; // {{ TODO }}: pruefen (Bedeutung)
{% set z = [] %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
{% for w in t.switches %}
{% set _ = z.append(("xInCarouselRun" ~ w.n ~ " := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
{% endfor %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% if t.scanners %}
{% set _ = z.append(("stInOutCognexInterface := " ~ m.cognex(t.scanners[0]), m.note(t.scanners[0].interface))) %}
{% else %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% endif %}
{% for j in t.jam_entries %}
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% for j in t.jam_exits %}
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% for s in t.separators %}
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
{% endfor %}
{% for w in t.switches %}
{% set _ = z.append(("xOutSw" ~ w.n ~ 'ExTo3 => "' ~ w.valve.value ~ '"', m.note(w.valve))) %}
{% endfor %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{{ m.call(t, z) }}
{% for sc in t.scanners %}
{{ m.cognex_out(m.cognex(sc), sc) }}
{% endfor %}
END_REGION
@@ -0,0 +1,112 @@
{#-
FB_ILS_MTRO_1Sep_SSCC - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl
(TRO 105, =A01+UH01-KF00 FB_Main, Scanner BX1104 + SSCC-Scanner
BX1107). Zeilen und Reihenfolge folgen dieser Aufrufstelle;
geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Separatorzeiten 600/200 ms, xPluggedConfirm/NullBT FALSE wie Referenz, "pruefen"
tSenFree/tSenWait 1500/4000 ms wie Referenz, "pruefen"
Scanner/SSCC-Scanner Cognex-Verdrahtung mit offenen Symbolen (E)
stSenEndMeasurement Sensor offen (Referenz: derselbe wie stInSenJam2)
xInCarouselRun offen (Referenz: TRUE "Gefaelle")
xInRelease "DB_TRO_TEST".<id> - in der Referenz haengt die
Freigabe an der WCS-Verbindung (Kommentar, offen)
Carr_Glob_Correction nInStation offen (Referenz 0), Traeger vom Eingang
SSCC an HMI Schluessel der HMI-Anzeige offen (Referenz SSCC1107)
WCS-Telegramm in der Referenz nur angedeutet - als TODO vermerkt
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
{% set ss = t.sscc %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#600ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedConfirm := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedNullBT := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
{% endfor %}
{{ m.jam_sensors(t) }}
{% for s in t.separators %}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1500ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4000ms; // {{ TODO }}: pruefen
{% endfor %}
{% for sc in t.scanners %}
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.xActivateCheckASCI := TRUE;
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.tTimeout := T#3000ms;
{{ m.cognex_in(m.cognex(sc), sc) }}
{% endfor %}
//SSCC
{{ m.cognex_in(ss.scanner.interface.value, ss.scanner) }}
#{{ t.id }}.stInBarcodeReaderSSCC.Settings.xActivateCheckASCI := TRUE;
#{{ t.id }}.stInBarcodeReaderSSCC.Settings.tTimeout := T#3s;
#{{ t.id }}.stInBarcodeReaderSSCC.xInPluggedSSCC := TRUE;
#{{ t.id }}.stInBarcodeReaderSSCC.stSenEndMeasurement := "DB_Inputs".Sensors["{{ ss.end_sensor.value }}"];{{ m.todo(ss.end_sensor) }}
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
{% set z = [] %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
{% set _ = z.append(("xInCarouselRun := " ~ t.carousel_run.value, m.note(t.carousel_run, "am Kreisel dessen Freigabe, im Gefaelle TRUE"))) %}
{% set _ = z.append(("xInRelease := " ~ t.release, TODO ~ " - Referenz: Freigabe ueber die WCS-Verbindung")) %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% if t.scanners %}
{% set _ = z.append(("stInOutCognexInterface := " ~ m.cognex(t.scanners[0]), m.note(t.scanners[0].interface))) %}
{% else %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% endif %}
{% set _ = z.append(("stInOutCognexInterfaceSSCC := " ~ ss.scanner.interface.value, m.note(ss.scanner.interface))) %}
{% for j in t.jam_entries %}
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% for j in t.jam_exits %}
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
{% endfor %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
{% for s in t.separators %}
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
{% endfor %}
{{ m.call(t, z) }}
{% for sc in t.scanners %}
{{ m.cognex_out(m.cognex(sc), sc) }}
{% endfor %}
//SSCC
{{ m.cognex_out(ss.scanner.interface.value, ss.scanner) }}
{{ m.correction(t, "Carr_Glob_Correction") }}
REGION Write SSCC result TO HMI Interface
IF #{{ t.id }}.xOutNewScanSSCC THEN
"DB_Interface_HMI".stSSCC.{{ ss.hmi.value }}.stLastMeasurement.nBarcodeTrolley := {{ t.jam_in.carriers.value }}[1].nBarcode;{{ m.todo(ss.hmi) }}
"DB_Interface_HMI".stSSCC.{{ ss.hmi.value }}.stLastMeasurement.sSSCC := LINT_TO_STRING(#{{ t.id }}.nOutScanResultSSCC);{{ m.todo(ss.hmi) }}
END_IF;
END_REGION
// {{ TODO }}: WCS-Telegramm (SSCC) - in der Referenz eigene REGION
// "Send DReqM to Fortna WCS (SSCC)", siehe Json_Layout-Konzept.md Abschnitt 4
END_REGION
@@ -0,0 +1,134 @@
{#-
FB_ILS_MTRO_PinStore_Auto - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl
(BLOCK 1.1.1 / TRO 208, =A01+UH02-KF00 FB_Main, Scanner BX2104).
Die Referenz schreibt nur 3 der 19 Linien aus; der Rest folgt
demselben Schema. Geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Anzahl Linien aus dem Layout: eine je Speicherbahn (nNoOfLines),
nicht die 19 der Referenz
Encoder, Stationen,
Ziele, Schwellen offen (anlagenspezifische Konstanten)
Sep Pin Einschleus-Separator; Zeiten 0/50 ms wie Referenz, "pruefen"
Sep Store n nJamLength offen (Referenz: 4 bzw. "cStorage111ExitJam<n>"),
Zeiten ueber die Konstanten cTimeSep...StorageLine
arInSep[7..19].stSimu Simulationsmodus der Referenz - nicht geschrieben
JamEntr/JamExit je einer: der eigene Eingangs-Staubereich (mehrere
Zulaeufe laufen dort zusammen, TRO-JSON v4) und der
des ersten Nachfolgers. Hat das Lager mehr
Staubereiche, steht ein TODO im Kopf.
JamBypass Platzhalter wie Referenz
Carr_Glob_Correction nInStation offen (Referenz 1), Traeger vom Eingang
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
{% set st = t.store %}
{% set sc = t.scanners[0] %}
{#- Kameraname im REGION-Titel nur, wenn bekannt (kein TODO-Kommentar im Titel) -#}
{% set kamera = "" if sc.tag.todo else " (" ~ (sc.tag.value|upper) ~ ")" %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% if t.jam_entries|length > 1 or t.jam_exits|length > 1 %}
// {{ TODO }}: {{ t.jam_entries|length }} Eingangs- / {{ t.jam_exits|length }} Ausgangs-Staubereiche im Layout -
// der FB hat je nur einen (JamEntr/JamExit, dazu JamBypass)
{% endif %}
// Linien (Separatoren aus dem Layout): {{ st.lines|map(attribute="layout")|join(", ") or "-" }}
REGION Encoder Settings
#{{ t.id }}.stInEncoderParameter.nPosActDif := {{ st.encoder.value }}.nOutPosActDif;{{ m.todo(st.encoder) }}
#{{ t.id }}.stInEncoderParameter.nPosPitchPart := {{ st.encoder.value }}.nOutPosPitchPart;{{ m.todo(st.encoder) }}
END_REGION
REGION ConveyorStation Settings
#{{ t.id }}.stInSettings.nDestGeneral := "{{ st.dest.value }}";{{ m.todo(st.dest) }}
#{{ t.id }}.stInSettings.nConvStations := "{{ st.conv_stations.value }}";{{ m.todo(st.conv_stations) }}
#{{ t.id }}.stInSettings.nStationsMax := "{{ st.stations_max.value }}";{{ m.todo(st.stations_max) }}
#{{ t.id }}.stInSettings.nStorageThreshold := "{{ st.threshold.value }}";{{ m.todo(st.threshold) }}
#{{ t.id }}.stInSettings.nNoOfLines := {{ st.lines|length }};
END_REGION
REGION Pin Query / "Zappler"
#{{ t.id }}.stInSensorReference := "DB_Inputs".Sensors["{{ st.reference_sensor.value }}"];{{ m.todo(st.reference_sensor) }}
END_REGION
REGION Barcode Reader In{{ kamera }}
#{{ t.id }}.stInBarcodeReader.Settings.nStation := {{ st.station.value }};{{ m.todo(st.station) }}
#{{ t.id }}.stInBarcodeReader.Settings.nScannerPurpose := 0;
#{{ t.id }}.stInBarcodeReader.Settings.tTimeout := T#5s;
#{{ t.id }}.stInBarcodeReader.Settings.xActivateCheckASCI := TRUE;
{{ m.cognex_in(m.cognex(sc), sc, " ") }}
END_REGION
REGION Separator Settings
REGION Sep Pin
// Einschleus-Separator = Bauteil {{ st.pin_layout }} aus dem Layout
#{{ t.id }}.stInSeparatorPin.stSenTrolley := "DB_Inputs".Sensors["{{ st.pin_sensor.value }}"];{{ m.todo(st.pin_sensor) }}
#{{ t.id }}.stInSeparatorPin.stSettings.tHandlingTime := T#0ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparatorPin.stSettings.tTrailingTime := T#50ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparatorPin.stSettings.xPluggedScanner := TRUE;
END_REGION
{% for l in st.lines %}
REGION Sep Store {{ l.n }}
// Linie {{ l.n }} = Separator {{ l.layout }} aus dem Layout
#nActNo := {{ l.n }};
#{{ t.id }}.arInSep[#nActNo].stSettings.nJamLength := {{ l.jam_length.value }};{{ m.todo(l.jam_length) }}
#{{ t.id }}.arInSep[#nActNo].stSenTrolley := "DB_Inputs".Sensors["{{ l.sensor.value }}"];{{ m.todo(l.sensor) }}
#{{ t.id }}.arInSep[#nActNo].stSettings.tHandlingTime := "cTimeSepHandlingStorageLine";
#{{ t.id }}.arInSep[#nActNo].stSettings.tTrailingTime := "cTimeSepTrailingStorageLine";
#{{ t.id }}.arInSep[#nActNo].stSettings.xPluggedScanner := FALSE;
END_REGION
{% endfor %}
END_REGION
REGION UnloadingStations Settings
#{{ t.id }}.xInQuitLocal := {{ st.quit_local.value }};{{ m.todo(st.quit_local) }}
{% for l in st.lines %}
#{{ t.id }}.arInJamSensor[{{ l.n }}] := "DB_Inputs".Sensors["{{ l.jam_sensor.value }}"];{{ m.todo(l.jam_sensor) }}
{% endfor %}
END_REGION
REGION Aufruf: {{ t.id }}
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE;
{% set z = [] %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("nInTestTarget := " ~ st.test_target.value, m.note(st.test_target))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
{% set _ = z.append(("xInConveyorRunIn := " ~ st.run_in.value, m.note(st.run_in, "Foerderer am Zulauf"))) %}
{% set _ = z.append(("xInConveyorRunOut := " ~ st.run_out.value, m.note(st.run_out, "Foerderer am Ablauf"))) %}
{% set _ = z.append(("stInOutControlUnitCabinet := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% set _ = z.append(("arInOutStorageLines := " ~ st.db.value ~ ".arLine", m.note(st.db))) %}
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
{% set _ = z.append(("arInOutJamExit := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
{% set _ = z.append(("stInOutJamExit := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
{% set _ = z.append(("arInOutJamBypass := #arCarrierPlaceHolder", "")) %}
{% set _ = z.append(("stInOutJamBypass := #stJamDataPlaceHolder", "")) %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{% set _ = z.append(("stMFRStorageRemovalInterface := " ~ st.removal.value, m.note(st.removal))) %}
{% set _ = z.append(('xOutSeparatorPin => "' ~ st.pin_stopper.value ~ '"', m.note(st.pin_stopper))) %}
{% set _ = z.append(("stCognexInterfacePin := " ~ m.cognex(sc), m.note(sc.interface))) %}
{% set _ = z.append(("stConveyorStations := " ~ st.conv_db.value, m.note(st.conv_db))) %}
{{ m.call(t, z) }}
END_REGION
{{ m.correction(t, "CARR_GLOB_CORRECTION") }}
REGION Barcode Reader Out{{ kamera }}
{{ m.cognex_out(m.cognex(sc), sc, " ") }}
END_REGION
REGION Separator Store Out
{% for l in st.lines %}
"{{ l.stopper.value }}" := #{{ t.id }}.arOutSeparatorStore[{{ l.n }}];{{ m.todo(l.stopper) }}
{% endfor %}
END_REGION
// stMFRStorageRemovalInterface koppelt an FB_StockRemovalBLKModul / FB_StorageManagement
END_REGION
@@ -0,0 +1,83 @@
{#-
FB_ILS_MTRO_Vario - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_Vario.scl
(TRO 115). Zeilen und Reihenfolge folgen dieser Aufrufstelle;
geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Separatorzeiten 800/300 ms, xPluggedJam FALSE wie Referenz, "pruefen"
tSenFree/tSenWait 2000/5000 ms wie Referenz, "pruefen"
Vario-Zeiten wie Referenz; tWaitForJog/tConvEmpty/nCountCarrier "pruefen"
tRasterHigh/Low 5 s wie Referenz, "pruefen"
stInSettings.xIsBorner TRUE wie Referenz, aber offen (Bedeutung klaeren)
arInOutJamEntr Staubereich zum (ersten) Vorgaenger
arInOutJamFinger Staubereich zum (ersten) Nachfolger
xInErrorQuit/xInAutomatic offen (Quittier-Taster, Automatik des Schranks)
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
{% set v = t.vario_sensors %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#800ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#300ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := t#2000ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#5000ms; // {{ TODO }}: pruefen
{% endfor %}
#{{ t.id }}.stInVario.Settings.xContinousEmptying := FALSE;
#{{ t.id }}.stInVario.Settings.tWaitForJog := T#300ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.tConvEmpty := T#45000ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.nCountCarrier := 1; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.tWaitForStop := t#3s;
#{{ t.id }}.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#{{ t.id }}.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#{{ t.id }}.stInVario.Settings.tRasterHigh := t#5s; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.tRasterLow := t#5s; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["{{ v.stSenCarrInPos.value }}"]; // MT in Pos{{ m.todo_mehr(v.stSenCarrInPos) }}
#{{ t.id }}.stInVario.stSenFinger := "DB_Inputs".Sensors["{{ v.stSenFinger.value }}"]; // Pusher Dog in Pos{{ m.todo_mehr(v.stSenFinger) }}
#{{ t.id }}.stInVario.stSenLastPos := "DB_Inputs".Sensors["{{ v.stSenLastPos.value }}"]; // Conveyor full{{ m.todo_mehr(v.stSenLastPos) }}
#{{ t.id }}.stInVario.stSenJam := "DB_Inputs".Sensors["{{ v.stSenJam.value }}"]; // Jam detector{{ m.todo_mehr(v.stSenJam) }}
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
#{{ t.id }}.stInVario.Settings.xJamSenDelayActive := TRUE;
#{{ t.id }}.stInVario.Settings.tJamSenDelay := T#2s;
#{{ t.id }}.stInSettings.xIsBorner := {{ t.is_border.value }}; // {{ m.note(t.is_border, "pruefen - Bedeutung mit SPS klaeren") }}
{% set z = [] %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInMotorProtection := " ~ t.motor_protection.value, m.note(t.motor_protection))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(('xInReset := "cTodoFalse"', "")) %}
{% set _ = z.append(("xInErrorQuit := " ~ t.error_quit.value, m.note(t.error_quit, "Quittierung"))) %}
{% set _ = z.append(("xInAutomatic := " ~ t.automatic.value, m.note(t.automatic, "Automatik des Schaltschranks"))) %}
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
{% set _ = z.append(("arInOutJamFinger := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
{% set _ = z.append(("stInOutJamFinger := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
{% set _ = z.append(('xOutStopper => "' ~ t.separators[0].stopper.value ~ '"', m.note(t.separators[0].stopper))) %}
{% set _ = z.append(('xOutVarioMotor => "' ~ t.vario_motor.value ~ '"', m.note(t.vario_motor))) %}
{{ m.call(t, z) }}
END_REGION
@@ -0,0 +1,85 @@
{#-
FB_ILS_MTRO_Vario_workStation - Aufrufstelle in FB_Main
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_Vario_workStation_CallSite.scl
(TRO 421, =A01+UH04-KF00 FB_Main). Der FB-Koerper liegt daneben
(FB_ILS_MTRO_Vario_workStation.scl). Zeilen und Reihenfolge folgen
der Aufrufstelle; geprueft per tests/test_scl_templates.py.
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
Separatorzeiten tTrailingTime "cTimeSepTrailing", tHandlingTime 200 ms
wie Referenz, "pruefen"
xPluggedScanner FALSE wie Referenz - der Scanner des FB sitzt am
Arbeitsplatz, nicht am Separator ("pruefen")
tSenFree/tSenWait 1200/4300 ms wie Referenz, "pruefen"
Vario-Zeiten wie Referenz (tWaitForJog 400 ms, tConvEmpty 30 s), "pruefen"
fbConvVario.xBeladenError Sensor offen (E)
stInStationSensor/xInReleaseStation Arbeitsplatz-Sensor und -Freigabe offen (E)
arInOutJamEntr/JamFinger Staubereich zum (ersten) Vorgaenger / Nachfolger
Alle noch mit den SPS-Kollegen zu bestaetigen.
-#}
{% import "_macros.scl.j2" as m %}
{% set v = t.vario_sensors %}
{% set ws = t.work_station %}
REGION {{ t.region_title }}
{{ m.kopf(t) }}
{% for s in t.separators %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := "cTimeSepTrailing"; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := FALSE; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1200ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4300ms; // {{ TODO }}: pruefen
{% endfor %}
#{{ t.id }}.stInVario.stSenFinger := "DB_Inputs".Sensors["{{ v.stSenFinger.value }}"]; // Finger in Pos{{ m.todo_mehr(v.stSenFinger) }}
#{{ t.id }}.stInVario.stSenLastPos := "DB_Inputs".Sensors["{{ v.stSenLastPos.value }}"]; // Conveyor full{{ m.todo_mehr(v.stSenLastPos) }}
#{{ t.id }}.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["{{ v.stSenCarrInPos.value }}"]; // MT in Position{{ m.todo_mehr(v.stSenCarrInPos) }}
#{{ t.id }}.stInVario.stSenJam := "DB_Inputs".Sensors["{{ v.stSenJam.value }}"]; // Jam Detector{{ m.todo_mehr(v.stSenJam) }}
#{{ t.id }}.stInVario.Settings.xContinousEmptying := FALSE;
#{{ t.id }}.stInVario.Settings.tWaitForJog := T#400ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.tConvEmpty := T#30000ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.nCountCarrier := 1; // {{ TODO }}: pruefen
#{{ t.id }}.stInVario.Settings.tWaitForStop := T#3s;
#{{ t.id }}.stInVario.Settings.xJamSenDelayActive := TRUE;
#{{ t.id }}.stInVario.Settings.tJamSenDelay := T#2s;
#{{ t.id }}.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#{{ t.id }}.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
#{{ t.id }}.fbConvVario.xBeladenError := "DB_Inputs".Sensors["{{ ws.loaded_error_sensor.value }}"].xDirectSensor;{{ m.todo(ws.loaded_error_sensor) }}
{% set z = [] %}
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
{% set _ = z.append(("xInAutomatic := " ~ t.automatic.value, m.note(t.automatic, "Automatik des Schaltschranks"))) %}
{% set _ = z.append(('xInReset := "cTodoFalse"', "")) %}
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
{% set _ = z.append(("xInMotorProtection := " ~ t.motor_protection.value, m.note(t.motor_protection))) %}
{% set _ = z.append(('stInStationSensor := "DB_Inputs".Sensors["' ~ ws.station_sensor.value ~ '"]', m.note(ws.station_sensor, "Arbeitsplatz"))) %}
{% set _ = z.append(("xInReleaseStation := " ~ ws.station_release.value, m.note(ws.station_release, "Freigabe Arbeitsplatz"))) %}
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
{% set _ = z.append(("arInOutJamFinger := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
{% set _ = z.append(("stInOutJamFinger := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
{% set _ = z.append(('xOutStopper => "' ~ t.separators[0].stopper.value ~ '"', m.note(t.separators[0].stopper))) %}
{% set _ = z.append(('xOutVarioMotor => "' ~ t.vario_motor.value ~ '"', m.note(t.vario_motor))) %}
{{ m.call(t, z) }}
END_REGION
+140
View File
@@ -0,0 +1,140 @@
{#-
_macros.scl.j2 - gemeinsame Bausteine der FB-Templates (doc/SCL_Templates.md §5.2)
Jede Zeile ist eine der Klassen aus §6: L (Layout, aus dem Kontext),
N (Name, heute Platzhalter + TODO), R (Referenzwert aus der realen Aufrufstelle),
E (E-Planung, Platzhalter + TODO), C (Bedingung).
Makros liefern ihre Zeilen OHNE abschliessenden Zeilenumbruch; der Aufrufer
setzt ihn. Globale Variable: TODO (Marker "TODO(E-Planung)").
-#}
{#- TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist -#}
{% macro todo(name, abstand=" ") -%}
{% if name.todo %}{{ abstand }}// {{ TODO }}{% endif %}
{%- endmacro %}
{#- TODO an einen schon vorhandenen Kommentar anhaengen ("// MT in Pos - TODO(E-Planung)") -#}
{% macro todo_mehr(name) -%}
{% if name.todo %} - {{ TODO }}{% endif %}
{%- endmacro %}
{#- Kommentartext fuer eine Parameterzeile des Aufrufs -#}
{% macro note(name, text="") -%}
{% if name.todo %}{{ TODO }}{% if text %} - {{ text }}{% endif %}{% else %}{{ text }}{% endif %}
{%- endmacro %}
{#- Kommentarkopf der REGION: Lage und Nachbarn im Layout (L) -#}
{% macro kopf(t) %}
{% if t.position %}
// Anlagenkoordinate x/y: {{ "%.0f"|format(t.position[0]) }} / {{ "%.0f"|format(t.position[1]) }} mm
{% endif %}
// Vorgaenger: {{ t.predecessors|join(", ") or "-" }}
// Nachfolger: {{ t.successors|join(", ") or "-" }}
{% if t.confidence %}
// Einstufung: {{ t.confidence }}
{% endif %}
// Aufbau nach Template {{ t.fb }}.scl.j2 (Referenz: doc/TRO_Katalog/scl_templates)
{%- endmacro %}
{#- stInSeparator<n>: Zeiten R aus der Referenz (TRO107), Nummer N, Sensor E -#}
{% macro separator(t, s) %}
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ todo(s.no) }}
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#100ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#50ms; // {{ TODO }}: pruefen
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ todo(s.sensor) }}
{%- endmacro %}
{#- stInSenJam<n>: ein Stausensor je Ausgang (E) -#}
{% macro jam_sensors(t) %}
{% for j in t.jam_exits %}
#{{ t.id }}.stInSenJam{{ j.n }} := "DB_Inputs".Sensors["{{ j.sensor.value }}"];{{ todo(j.sensor) }}
{% endfor %}
{%- endmacro %}
{#- stInSwitch<n>: Nummer N; nTo1Destinations L, aber Bedeutung noch zu klaeren -#}
{% macro switch(t, w) %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}";{{ todo(w.no) }}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen3 := FALSE;
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen4 := FALSE;
{% if w.to1_destinations is not none %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := {{ w.to1_destinations }}; // Abgaenge laut Topologie - {{ TODO }}: pruefen (Info fuer HMI; Referenz TRO107 hat 3 bei 2 Abgaengen)
{% else %}
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := _; // {{ TODO }}: {{ t.successors|length }} Abgaenge auf {{ t.switches|length }} Weichen - Aufteilung festlegen
{% endif %}
{%- endmacro %}
{#- Zielliste je Weichenabgang (L, nur Kommentar) -#}
{% macro ziele(t) %}
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
{% for e in t.switch_exits %}
// ueber {{ e.via }}: {{ e.targets|join(", ") or "-" }}
{% endfor %}
{%- endmacro %}
{#-
Instanzaufruf aus einer Liste von (Code, Kommentar). Das Komma wird hier
gesetzt und steht damit VOR dem Kommentar - sonst waere es Teil des
Kommentars und die Parameterliste uebersetzt nicht.
-#}
{% macro call(t, zeilen) %}
{% set einzug = " " * (6 + t.id|length) %}
{% for code, kommentar in zeilen %}
{{ (" #" ~ t.id ~ "(") if loop.first else einzug }}{{ code }}{{ ");" if loop.last else "," }}{% if kommentar %} // {{ kommentar }}{% endif %}
{% endfor %}
{%- endmacro %}
{#- Cognex-Kamera: Eingaenge der Schnittstelle (E). ziel = "#arCognexInterface[...]" oder eine eigene Struktur -#}
{% macro cognex_in(ziel, sc, einzug=" ") %}
{{ einzug }}{{ ziel }}.xInCamTriggerReady := "{{ sc.tag.value }}-TriggerReady";{{ todo(sc.tag) }}
{{ einzug }}{{ ziel }}.xInCamTriggerAck := "{{ sc.tag.value }}-TriggerAck";{{ todo(sc.tag) }}
{{ einzug }}{{ ziel }}.xInCamResultsAvailable := "{{ sc.tag.value }}-Results Available";{{ todo(sc.tag) }}
{{ einzug }}{{ ziel }}.xInCamGeneralFault := "{{ sc.tag.value }}-General Fault";{{ todo(sc.tag) }}
{{ einzug }}{{ ziel }}.hwInCamInspectionResults := "{{ sc.hw_results.value }}";{{ todo(sc.hw_results) }}
{%- endmacro %}
{#- Cognex-Kamera: Ausgaenge zurueck auf die Hardware-Symbole (E) -#}
{% macro cognex_out(ziel, sc, einzug=" ") %}
{{ einzug }}"{{ sc.tag.value }}-TriggerEnable" := {{ ziel }}.xOutCamTriggerEnable;{{ todo(sc.tag) }}
{{ einzug }}"{{ sc.tag.value }}-Trigger" := {{ ziel }}.xOutCamTrigger;{{ todo(sc.tag) }}
{{ einzug }}"{{ sc.tag.value }}-ResultsAck" := {{ ziel }}.xOutCamResultsAck;{{ todo(sc.tag) }}
{%- endmacro %}
{#- Schluessel einer Kamera in #arCognexInterface -#}
{% macro cognex(sc) -%}
#arCognexInterface["{{ sc.interface.value }}"]
{%- endmacro %}
{#- Traegerkorrektur nach dem Scan (FC_CARR_GLOB_CORRECTION); Traeger am Eingang -#}
{% macro correction(t, titel, einzug=" ") %}
{{ einzug }}REGION {{ titel }}
{{ einzug }} "FC_CARR_GLOB_CORRECTION"(xInStart := #{{ t.id }}.xOutCorrection,
{{ einzug }} nInBarcodeScan := #{{ t.id }}.nBarcodeResult,
{{ einzug }} nInStation := {{ t.correction_station.value }},{{ todo(t.correction_station) }}
{{ einzug }} arOutEventLogInfo => #arDummyEventCorrectionInfo,
{{ einzug }} InOutCarrierExpect := {{ t.jam_in.carriers.value }}[1]);{{ todo(t.jam_in.carriers) }}
{{ einzug }}END_REGION
{%- endmacro %}
{#- REGION Direction: je Weiche ein FC_Direction-Aufruf -#}
{% macro direction(t) %}
REGION Direction
{% for w in t.switches %}
"FC_Direction"(nInSwitchNo := "{{ w.no.value }}",{{ todo(w.no) }}
nOutDirection => {{ w.direction_target }},
InOutCarrier := {{ w.jam_db.value }}.arCarrier[1]);{{ todo(w.jam_db) }}
{% if not loop.last %}
{% endif %}
{% endfor %}
END_REGION
{%- endmacro %}
+197 -106
View File
@@ -12,6 +12,12 @@ TROs und schreibt eine **Kopie** der Zeichnung mit zusaetzlicher Beschriftung:
* optional Pfeile fuer den gerichteten Materialfluss zwischen den TROs
* optional eine Legende
Die Zeichnung ist eine **Ansicht** zur Sichtkontrolle, keine Datenquelle: die
TRO-JSON fuer scl_skeleton.py schreibt tro_flow.py --json direkt aus dem CSV.
tro_annotate leitet die TROs mit genau demselben Aufruf ab wie tro_flow, damit
Zeichnung und JSON uebereinstimmen. (tro_extract.py, der Rueckweg Zeichnung ->
JSON, ist geparkt.)
Die Originalzeichnung wird nie veraendert. Alle Ergaenzungen liegen auf eigenen
Layern (Vorgabe: Praefix "TRO_") und tragen XDATA der Anwendungskennung
SPS_SKEL_TRO. Ein erneuter Lauf entfernt genau diese Objekte und baut sie neu -
@@ -101,8 +107,19 @@ 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 model import Analysis
from parameter import get_float
from tro_flow import (
BOUNDARY_ENTRY,
BOUNDARY_EXIT,
STATION_FARBE,
_station_positionen,
analyse_tros,
find_sibling_raw_json,
load_sepliste_counts,
resolve_ids_file,
)
from tro_ids import TroIds
# ---------------------------------------------------------------------------
# Konstanten
@@ -115,11 +132,11 @@ BLOCK_PREFIX = "TRO_SYM_"
# SSG_BLOCKEDIT in dxfmakros ihn ueber "CONNECTION_*" erkennt.
CONNECTION_BLOCK = "CONNECTION_ARROW"
# Zeichnungsgroessen in mm, wenn nicht anders angegeben
DEFAULT_MARKER = 300.0
DEFAULT_TEXT = 220.0
# Restfehler, ab dem der Lagebezug abgelehnt wird
DEFAULT_MAX_RESIDUAL = 600.0
# Zeichnungsgroessen und Restfehler-Grenze des Lagebezugs aus cfg/parameter.ini,
# Abschnitt [tro_annotate] (lib/parameter.py)
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
def hex_to_rgb(value: str) -> tuple[int, int, int]:
@@ -312,10 +329,13 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
doc.layers.add(name, dxfattribs={
"true_color": ezdxf.rgb2int(hex_to_rgb(style.stroke)),
})
# openpoint: im Review gemeldete Stellen, die noch KEIN TRO sind. Kraeftiges
# Rot, damit sie sich von den abgeleiteten Markern klar abheben.
# Kantenfarben wie im TRO-Fluss-SVG (tro_flow.render_dot): Uebergabe blau,
# Weiche orange, Materialeingang gruen, Materialausgang rot.
for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
("legend", "#808080"), ("openpoint", "#e00000")):
("weiche", "#cc8800"),
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
("legend", "#808080")):
name = f"{prefix}{key.upper()}"
names[key] = name
if name not in doc.layers:
@@ -334,6 +354,40 @@ def _polygon(sides: int, radius: float, start_deg: float) -> list[tuple[float, f
]
def shape_outline(symbol: str, r: float) -> list[tuple[float, float]]:
"""Aeussere Kontur eines Symbols als Polygon - Rand der Fuellflaeche
(Kreise als 48-Eck angenaehert)."""
if symbol in (TroSymbol.CIRCLE, TroSymbol.DOUBLE_CIRCLE):
return _polygon(48, r, 0)
if symbol == TroSymbol.TRIANGLE:
return _polygon(3, r, 90)
if symbol in (TroSymbol.TRIANGLE_DOWN, TroSymbol.DOUBLE_TRIANGLE_DOWN):
return _polygon(3, r, 270)
if symbol == TroSymbol.SQUARE:
return _polygon(4, r, 45)
if symbol == TroSymbol.DIAMOND:
return _polygon(4, r, 90)
if symbol == TroSymbol.PENTAGON:
return _polygon(5, r, 90)
if symbol == TroSymbol.HEXAGON:
return _polygon(6, r, 0)
if symbol == TroSymbol.RECT:
w, h = r * 1.45, r * 0.72
return [(-w, -h), (w, -h), (w, h), (-w, h)]
if symbol == TroSymbol.ARROW:
return [(-r * 0.55, -r), (r, 0), (-r * 0.55, r), (-r * 0.55, r * 0.35),
(-r, r * 0.35), (-r, -r * 0.35), (-r * 0.55, -r * 0.35)]
return _polygon(48, r, 0)
def draw_fill(blk, symbol: str, r: float, fill_hex: str) -> None:
"""Typ-eigene Fuellfarbe (wie im TRO-Fluss-SVG) als Solid-Hatch unter der
Kontur. Feste Farbe statt BYLAYER: der Layer traegt die Randfarbe der
Gruppe, die Fuellung unterscheidet den einzelnen Typ."""
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(fill_hex))})
hatch.paths.add_polyline_path(shape_outline(symbol, r), is_closed=True)
def draw_shape(blk, symbol: str, r: float) -> None:
"""
Symbolgeometrie in eine Blockdefinition zeichnen.
@@ -399,7 +453,10 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
blk = doc.blocks.new(name=name)
definition = get_tro(type_name)
draw_shape(blk, definition.get_symbol() if definition else "", marker)
symbol = definition.get_symbol() if definition else ""
if definition:
draw_fill(blk, symbol, marker, definition.get_fill())
draw_shape(blk, symbol, marker)
gap = marker * 1.5
rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)]
@@ -481,45 +538,25 @@ def place_markers(
return placed, internal
def draw_open_points(
msp, analysis: Analysis, transform: Transform, layers: dict[str, str],
marker: float, text: float,
) -> int:
"""Offene Punkte aus cfg/tro_overrides.ini als Fragezeichen-Marker setzen.
Das sind im Review gemeldete Stellen, die noch kein TRO sind. Sie werden
bewusst NICHT als TRO-Symbol gezeichnet, sondern als durchgestrichener
Kreis mit Klartext daneben - damit in der Zeichnung sofort erkennbar ist,
dass hier eine Entscheidung aussteht und nichts abgeleitet wurde.
"""
drawn = 0
radius = marker * 1.35
for point in analysis.overrides.open_points:
x, y = transform.to_dxf(point.x, point.y)
ring = msp.add_circle((x, y), radius,
dxfattribs={"layer": layers["openpoint"]})
tag(ring, f"OFFEN:{point.name}")
diag = radius * 0.7071
for (x1, y1), (x2, y2) in (((-diag, -diag), (diag, diag)),
((-diag, diag), (diag, -diag))):
line = msp.add_line((x + x1, y + y1), (x + x2, y + y2),
dxfattribs={"layer": layers["openpoint"]})
tag(line, f"OFFEN:{point.name}")
lines = [f"OFFEN - noch kein TRO: {point.name}"]
if point.expect:
lines.append(f"erwartet: {point.expect}")
if point.belongs_to:
lines.append(f"heute Teil von: {point.belongs_to}")
lines.append(f"Anlage x/y: {point.describe_position()}")
label = msp.add_mtext("\\P".join(lines), dxfattribs={
"layer": layers["openpoint"],
"char_height": text,
})
label.set_location((x + radius * 1.3, y + radius))
tag(label, f"OFFEN:{point.name}")
drawn += 1
return drawn
def place_stations(
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
layers: dict[str, str], text: float,
) -> dict[str, tuple[float, float]]:
"""Be-/Entladestationen wie im TRO-Fluss-SVG: kein Symbol, nur der Name
in Gruen (Eingang) bzw. Rot (Ausgang) am Ende des Zu-/Ablaufpfeils.
Lage wie dort (tro_flow._station_positionen): feste Pfeillaenge ab dem
angehaengten TRO, Richtung von dessen Kanten weg."""
positionen = _station_positionen(analysis, placed)
for station in analysis.boundaries:
if station.tro_id not in positionen:
continue
x, y = positionen[station.tro_id]
layer = layers["eingang" if station.type_name == BOUNDARY_ENTRY else "ausgang"]
label = msp.add_text(station.label, height=text,
dxfattribs={"layer": layer, "style": "Standard"})
label.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
tag(label, station.tro_id)
return positionen
def build_connection_symbol(doc, marker: float, text: float) -> str:
@@ -537,9 +574,9 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
UID unsichtbar - eindeutige interne Nummer, dauerhaft
FROM_UID unsichtbar - interne UID des Quell-TRO
TO_UID unsichtbar - interne UID des Ziel-TRO
FROM_TRO sichtbar - sprechender Name der Quelle (TRO01)
TO_TRO sichtbar - sprechender Name des Ziels (TRO09)
KIND unsichtbar - flow | colocated
FROM_TRO unsichtbar - sprechender Name der Quelle (TRO01)
TO_TRO unsichtbar - sprechender Name des Ziels (TRO09)
KIND unsichtbar - flow | weiche | colocated
"""
name = CONNECTION_BLOCK
if name in doc.blocks:
@@ -552,13 +589,17 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
dxfattribs={"color": 256})
blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256})
# FROM_TRO/TO_TRO unsichtbar: an jeder Kante sichtbar machten sie die
# Zeichnung unruhig, im TRO-Fluss-SVG stehen sie auch nicht (Review
# 2026-10-02). Die Daten bleiben am Block, CONNECTION_EDIT zeigt sie.
offset = size * 1.4
for tag_name in ("FROM_TRO", "TO_TRO"):
blk.add_attdef(
tag_name,
insert=(size * 1.3, offset),
height=text * 0.8,
dxfattribs={"style": "Standard", "color": 256, "text": ""},
dxfattribs={"style": "Standard", "color": 256, "text": "",
"invisible": 1},
)
offset -= text * 1.2
@@ -589,30 +630,57 @@ def draw_connections(
"""
head = marker * 0.9
block = build_connection_symbol(doc, marker, text)
stationen = {b.tro_id for b in analysis.boundaries}
drawn = 0
for src, dst in analysis.edges:
if src not in placed or dst not in placed:
continue
# Farbe/Layer wie im TRO-Fluss-SVG
if src in stationen:
layer, kind = layers["eingang"], "eingang"
elif dst in stationen:
layer, kind = layers["ausgang"], "ausgang"
elif analysis.edge_kinds.get((src, dst)) == "weiche":
layer, kind = layers["weiche"], "weiche"
else:
layer, kind = layers["flow"], "flow"
x1, y1 = placed[src]
x2, y2 = placed[dst]
length = math.dist((x1, y1), (x2, y2))
if length <= 2 * marker * 1.1:
if length < 1e-6:
continue
ux, uy = (x2 - x1) / length, (y2 - y1) / length
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht deckt
sx, sy = x1 + ux * marker * 1.1, y1 + uy * marker * 1.1
ex, ey = x2 - ux * marker * 1.1, y2 - uy * marker * 1.1
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
# deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
kurz_a = marker * (0.3 if src in stationen else 1.1)
kurz_b = marker * (0.3 if dst in stationen else 1.1)
# Zu kurz fuer einen gekuerzten Pfeil (Marker liegen dicht beieinander,
# z.B. HundM05 TRO03->TRO04 mit 500 mm): NICHT weglassen - sonst fehlt
# die Verbindung in tro_extract.py und damit in der SCL. Stattdessen
# Mitte-zu-Mitte ohne Kuerzung zeichnen.
kurz = length <= kurz_a + kurz_b + head
if kurz:
kurz_a = kurz_b = 0.0
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
dxfattribs={"layer": layers["flow"]})
dxfattribs={"layer": layer})
tag(shaft, f"{src}->{dst}")
# Pfeilspitze als gefuellte Flaeche
bx, by = ex - ux * head, ey - uy * head
# Pfeilspitze als gefuellte Flaeche (bei kurzer Verbindung in der Mitte,
# damit sie nicht im Zielmarker verschwindet)
hx, hy = ((x1 + x2) / 2 + ux * head / 2, (y1 + y2) / 2 + uy * head / 2) \
if kurz else (ex, ey)
bx, by = hx - ux * head, hy - uy * head
px, py = -uy * head * 0.38, ux * head * 0.38
point = msp.add_solid(
[(ex, ey), (bx + px, by + py), (bx - px, by - py)],
dxfattribs={"layer": layers["flow"]},
[(hx, hy), (bx + px, by + py), (bx - px, by - py)],
dxfattribs={"layer": layer},
)
tag(point, f"{src}->{dst}")
if kind in ("eingang", "ausgang"):
# Stationspfeile ohne Verbindungsblock: keine TRO-TRO-Beziehung
drawn += 1
continue
# Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils
ref = msp.add_blockref(
@@ -626,7 +694,7 @@ def draw_connections(
"TO_UID": internal.get(dst, ""),
"FROM_TRO": src,
"TO_TRO": dst,
"KIND": "flow",
"KIND": kind,
})
tag(ref, f"{src}->{dst}")
next_id += 1
@@ -653,28 +721,48 @@ def draw_legend(
title.set_placement((x0, y0 + row))
tag(title)
# Wie die Legende im TRO-Fluss-SVG: je vorkommendem Typ Fuellfarbe, Rand
# der Gruppe und Form, danach die Kantenfarben.
used = {t.type_name for t in analysis.tros}
groups = []
for style in STYLES:
types = sorted(n for n in used
if (get_tro(n).get_group() if get_tro(n) else "ext") == style.group)
if types:
groups.append((style, types))
zeilen: list[tuple[str, str | None, str | None]] = []
for definition in TRO_CATALOG:
if definition.get_name() in used:
zeilen.append((f"{definition.get_name()} - {definition.describe_symbol()}",
definition.get_fill(),
layers.get(definition.get_group(), layers["ext"])))
pfeile = [("eingang", "Materialeingang (Beladung / Eingang)"),
("ausgang", "Materialausgang (Entladung / Ausgang)"),
("flow", "Uebergabe / Ein-Ausschleusung"),
("weiche", "Weiche (Kreisel-Uebergang)")]
for index, (style, types) in enumerate(groups):
y = y0 - index * row
box = msp.add_solid(
[(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)],
dxfattribs={"layer": layers[style.group]},
)
y = y0
for beschriftung, fill, rand_layer in zeilen:
quadrat = [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)]
box = msp.add_hatch(dxfattribs={"layer": layers["legend"],
"true_color": ezdxf.rgb2int(hex_to_rgb(fill))})
box.paths.add_polyline_path(quadrat, is_closed=True)
tag(box)
label = msp.add_text(
f"{style.label}: {', '.join(types)}",
height=text,
dxfattribs={"layer": layers["legend"], "style": "Standard"},
)
rand = msp.add_lwpolyline(quadrat, close=True, dxfattribs={"layer": rand_layer})
tag(rand)
label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers["legend"], "style": "Standard"})
label.set_placement((x0 + text * 1.8, y - text))
tag(label)
y -= row
for key, beschriftung in pfeile:
mitte = y - text / 2
linie = msp.add_lwpolyline([(x0 - text * 0.5, mitte), (x0 + text * 0.9, mitte)],
dxfattribs={"layer": layers[key]})
tag(linie)
spitze = msp.add_solid([(x0 + text * 1.4, mitte), (x0 + text * 0.9, mitte + text * 0.3),
(x0 + text * 0.9, mitte - text * 0.3)],
dxfattribs={"layer": layers[key]})
tag(spitze)
label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers[key], "style": "Standard"})
label.set_placement((x0 + text * 1.8, y - text))
tag(label)
y -= row
# ---------------------------------------------------------------------------
@@ -705,13 +793,6 @@ def report(
if counts.get("first_id"):
print(f"Interne IDs ab = {format_internal_id(counts['first_id'])} "
f"(ID-Raum von SSG_LIB, siehe IDSCHECK)")
print(f"Offene Punkte = {counts['openpoints']} (Layer TRO_OPENPOINT, "
f"noch kein TRO)")
if analysis.overrides.applied:
print(f"Typkorrekturen = {len(analysis.overrides.applied)} aus "
f"{analysis.overrides.source}")
for entry in analysis.overrides.applied:
print(f" * {entry}")
print(f"Entfernte Objekte= {counts['removed']} (aus frueherem Lauf)")
print(f"Layer = {counts['layers']}, Bloecke: {counts['blocks']}")
print(f"DXF = {out_file if out_file else '- (--check)'}")
@@ -766,17 +847,14 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
metavar="MM", help="Markerradius in mm. Standard: %(default)s")
parser.add_argument("--text-height", type=float, default=DEFAULT_TEXT,
metavar="MM", help="Texthoehe in mm. Standard: %(default)s")
parser.add_argument("--layer-prefix", default=LAYER_PREFIX, metavar="P",
help="Praefix der erzeugten Layer. Standard: %(default)s")
parser.add_argument("--ids", metavar="DATEI",
help="ID-Datei der TROs wie bei tro_flow (nur gelesen). "
"Standard: <csv>_tro_ids.ini in %%SKEL_CFG%%.")
parser.add_argument("--offset", metavar="DX,DY",
help="Lagebezug von Hand setzen statt ermitteln.")
parser.add_argument("--re-register", action="store_true",
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
"einer abgelegt ist.")
parser.add_argument("--max-residual", type=float, default=DEFAULT_MAX_RESIDUAL,
metavar="MM",
help="Groesster zugelassener Restfehler des Lagebezugs. "
"Standard: %(default)s")
parser.add_argument("--check", action="store_true",
help="Nur pruefen und berichten, keine Datei schreiben.")
parser.add_argument("--emit-lisp", action="store_true",
@@ -834,8 +912,22 @@ 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)
analysis = analyse_tros(graph, elements, overrides)
# Derselbe Aufruf wie in tro_flow.main (ohne cfg/tro_overrides.ini, mit
# der Separator-Gegenprobe aus <name>_raw.json und der ID-Datei), damit die Zeichnung genau
# die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine Datenquelle.
raw_json_file = find_sibling_raw_json(csv_file)
sepliste_counts = (load_sepliste_counts(raw_json_file, read_warnings)
if raw_json_file is not None else {})
# Dieselbe ID-Datei wie tro_flow, aber nur lesen: neue TROs bekommen
# dieselbe vorlaeufige ID, die tro_flow anhaengen wuerde.
ids_file = resolve_ids_file(args.ids, csv_file)
try:
tro_ids = TroIds.load(ids_file) if ids_file.is_file() else None
except (OSError, ValueError) as exc:
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
return 1
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
tro_ids=tro_ids)
if not analysis.tros:
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
return 1
@@ -855,16 +947,15 @@ def main(argv: list[str] | None = None) -> int:
except RegistrationError as exc:
print(f"FEHLER: Lagebezug nicht bestimmbar: {exc}", file=sys.stderr)
return 4
if transform.residual_max > args.max_residual:
if transform.residual_max > DEFAULT_MAX_RESIDUAL:
print(f"FEHLER: Restfehler des Lagebezugs {transform.residual_max:.1f} mm "
f"ueber der Grenze von {args.max_residual:.1f} mm.", file=sys.stderr)
f"ueber der Grenze von {DEFAULT_MAX_RESIDUAL:.1f} mm.", file=sys.stderr)
print(f" {transform.describe()}", file=sys.stderr)
print(" Mit --offset von Hand setzen oder --max-residual anheben.",
file=sys.stderr)
print(" Mit --offset von Hand setzen.", file=sys.stderr)
return 4
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
"openpoints": 0, "first_id": 0}
"first_id": 0}
if args.check:
counts["marker"] = sum(1 for t in analysis.tros if t.position)
@@ -873,7 +964,7 @@ def main(argv: list[str] | None = None) -> int:
ensure_appid(doc)
counts["removed"] = remove_previous(msp)
layers = build_layers(doc, args.layer_prefix)
layers = build_layers(doc, LAYER_PREFIX)
counts["layers"] = len({v for v in layers.values()})
# Interne IDs erst NACH remove_previous suchen: die Marker des letzten Laufs
@@ -887,13 +978,13 @@ def main(argv: list[str] | None = None) -> int:
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
counts["first_id"] = next_id
counts["openpoints"] = draw_open_points(msp, analysis, transform, layers,
args.marker_size, args.text_height)
if args.flow:
# Stationen nur mit --flow: sie bestehen aus ihrem Zu-/Ablaufpfeil
stationen = place_stations(msp, analysis, placed, layers, args.text_height)
counts["flow"] = draw_connections(
msp, doc, analysis, placed, internal, layers, args.marker_size,
args.text_height, next_id=next_id + len(placed),
msp, doc, analysis, {**placed, **stationen}, internal, layers,
args.marker_size, args.text_height, next_id=next_id + len(placed),
)
if args.legend:
draw_legend(msp, analysis, placed, layers, args.marker_size,
+65 -16
View File
@@ -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,18 @@ 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"),
# ACHTUNG - NICHT aus SCL belegt, wie 2Sep2Swi. Aufgenommen am 2026-10-14
# fuer die Testanlage data/export.csv: Weichenknoten, an dem die Umlenkung
# eines Kreisels zwei andere beruehrt (Kreisel 5 an 4 und 6) - drei
# Separatoren laufen in Laufrichtung auf die zwei Weichen zu (siehe
# lib/kreisel_junctions.py). Scanner je Separator analog zu 2Sep2Swi.
# FB_ILS_MTRO_3Sep2Swi existiert noch nicht in der ILSLib.
TroDefinition("3Sep2Swi", "FB_ILS_MTRO_3Sep2Swi", {
TroItem.SEPARATOR: 3,
TroItem.SWITCH: 2,
TroItem.SCANNER: 3,
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#d9822b"),
)
_BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG}
@@ -610,5 +657,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_<Name>)."""
return ([style.mermaid_classdef() for style in STYLES]
+ [definition.mermaid_classdef() for definition in TRO_CATALOG])
+2178 -492
View File
File diff suppressed because it is too large. Load diff
-338
View File
@@ -1,338 +0,0 @@
# -*- coding: utf-8 -*-
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
"""
tro_geometry.py - Separator-Host und Weichen-Fanout direkt aus der Geometrie.
Ergaenzt tro_flow.py::derive_tros() um einen zweiten Weg, zwei seiner Eingaben
zu bestimmen:
Zuordnung (Host des Separators) -> resolve_host_geometric
Nachbarn/graph.edges (Fanout) -> count_geometric_exits
Beide Werte kommen normalerweise aus den CSV-Spalten "Zuordnung"/"Nachbarn"
(material_flow.py::build_graph). Dieses Modul sucht sie stattdessen direkt in
den Weltkoordinaten der Bauteile: ein shapely-STRtree ueber die AS/ES-Enden
(K1/K2) jeder Strecke/Gefaellestrecke sowie die beiden Kreisel-Bahnenden
liefert per .nearest() den naechstgelegenen Host zu einem Separator und per
.query(..., predicate="dwithin") alle anderen Ketten, die sich an einem
gegebenen Punkt tatsaechlich treffen - unabhaengig davon, ob die betroffene
Zeile im CSV eine vollstaendige "Nachbarn"-Liste hat.
Hintergrund: hundmM05_1.csv zeigt fuer mehrere "ILS 2.0 Strecke"-Zeilen den
Fehler "nur ein Partner" (Spalte Fehler) - die von dxfmakros berechnete
Nachbarschaft ist dort unvollstaendig. Eine fruehere, gleichartige Datei
(hundm_export.csv) zeigte den Ausfall dieser Abhaengigkeit in voller Wucht:
tro_flow.py leitete 5 TROs ab, alle vom Typ Vario, 0 Verbindungen, alle
isoliert (siehe results/hundm_export_tro_doc.md) - Zuordnung/Nachbarn liessen
sich fuer diesen Export nicht ausreichend aufloesen. Dieses Modul macht die
TRO-Ableitung fuer genau diesen Fall unabhaengig von der Qualitaet dieser
beiden Spalten.
Nutzung: `tro_flow.py --geometry` (siehe dort, `make_geometric_resolvers`).
Dieses Modul kennt tro_flow.py nicht - nur material_flow.py::Element-Listen.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from shapely import STRtree
from shapely.geometry import LineString, Point
from material_flow import (
LANE_LEFT,
LANE_RIGHT,
UNASSIGNED,
Element,
_as_float,
_merkmal,
)
# Kreisel-Durchmesser (mm), falls kein Merkmal mit dem Durchmesser vorliegt -
# Default wie dxfmakros' cfg/export.cfg [Kreisel] durchmesser_mm.
DEFAULT_KREISEL_DURCHMESSER_MM = 800.0
# Merkmal-Namen, unter denen der Achsabstand der beiden Kreiselstationen
# (AN8/SP8) im CSV-Export auftaucht - in Metern, siehe kreisel_capsule().
ABSTAND_MERKMALE = (
"Abstand (Kreiselachse A - Kreiselachse) in Meter",
"Abstand_m",
"ABSTAND",
)
# Default-Toleranz (mm), innerhalb derer zwei Anschlusspunkte als "derselbe
# Uebergabepunkt" gelten. Bewusst zwischen dxfmakros' omniflo_ks_toleranz_mm
# (10mm, enge KOS-Deckung von Omniflo-Steckverbindern) und vf_linienzug.lsp's
# *vfl-kette-tol-weit* (100mm, "sieht verbunden aus, ist es aber vielleicht
# nicht") - zwei Kettenenden an einem echten mechanischen Uebergang treffen
# sich nicht auf den zehntel Millimeter, muessen aber auch nicht 10cm
# auseinanderliegen duerfen, um noch als verbunden zu gelten.
DEFAULT_TOLERANCE_MM = 50.0
@dataclass
class _EndpointEntry:
"""Ein indizierter Anschlusspunkt: welches Bauteil, welches Ende."""
teile_id: str
end: str # "K1"/"K2" (AS/ES) oder "a"/"b" (Kreisel-Bahnende)
def chain_endpoints(
element: Element,
) -> tuple[tuple[float, float] | None, tuple[float, float] | None]:
"""AS-(K1) und ES-(K2) Weltpunkt einer Strecke/Gefaellestrecke.
Fehlen K1/K2 (aeltere Exporte ohne AS/ES-Punkte), Ersatz durch die Mitten
der kurzen Stirnseiten der Bounding-Box entlang ihrer laengeren Ausdehnung
- gleiche Idee wie dxfmakros' export_neighbors.py::_strecke_ends, nur ohne
dessen "Anfang liegt naeher am Einfuegepunkt"-Zuordnung: fuer die reine
Fanout-Zaehlung an einem Punkt (count_geometric_exits) ist unerheblich,
welches Ende AS und welches ES ist.
"""
by_label = dict(element.kpoints)
k1, k2 = by_label.get("K1"), by_label.get("K2")
if k1 is not None and k2 is not None:
return k1, k2
if element.x is None or element.y is None or element.bbox is None:
return k1, k2
cx, cy = element.x, element.y
dx, dy = element.bbox_x or 0.0, element.bbox_y or 0.0
if dx >= dy:
bbox_ends = ((cx - dx / 2.0, cy), (cx + dx / 2.0, cy))
else:
bbox_ends = ((cx, cy - dy / 2.0), (cx, cy + dy / 2.0))
return k1 or bbox_ends[0], k2 or bbox_ends[1]
def kreisel_durchmesser_mm(element: Element) -> float:
"""Kreisel-Durchmesser (mm) aus einem Merkmal, sonst DEFAULT_*."""
for key in ("Durchmesser_mm", "Durchmesser (mm)", "Durchmesser"):
value = _as_float(_merkmal(element.merkmale, key))
if value:
return value
return DEFAULT_KREISEL_DURCHMESSER_MM
def kreisel_capsule(element: Element, durchmesser_mm: float | None = None) -> dict | None:
"""Achsen-Segment (Kapsel) eines Kreisels in Weltkoordinaten.
dxfmakros' eigenes export_neighbors.py::_kreisel_capsule setzt die
Antriebsstation (AN8) lokal bei (radius, 0) und die Spannstation (SP8) bei
(radius+abstand, 0) - relativ zum BLOCK-Einfuegepunkt (lokaler Ursprung
des KREISEL_n-Wrappers, siehe KreiselInsert.lsp::draw-module). Die CSV-
Spalte "Position" fuehrt bei einem Kreisel jedoch nachweislich den
BBOX-MITTELPUNKT, nicht diesen Einfuegepunkt: die exportierte Boundingbox
ist exakt symmetrisch um Position (halbe Breite = (abstand+durchmesser)/2,
siehe hundmM05_1.csv Zeile 6/7 - AN8-lokal-(radius,0) waere das nicht).
AN8/SP8 werden daher symmetrisch um Position gelegt: AN8 bei
(-abstand/2, 0), SP8 bei (+abstand/2, 0) - beide auf der lokalen X-Achse,
gedreht um das Merkmal "Drehung" (Grad, CCW positiv).
Rueckgabe {"a": (x,y), "b": (x,y), "r": radius} (a = AN8-, b = SP8-Mitte),
oder None ohne Position.
"""
if element.x is None or element.y is None:
return None
abstand_m = None
for key in ABSTAND_MERKMALE:
abstand_m = _as_float(_merkmal(element.merkmale, key))
if abstand_m is not None:
break
abstand_mm = (abstand_m or 0.0) * 1000.0
durchmesser = durchmesser_mm or kreisel_durchmesser_mm(element)
r = durchmesser / 2.0
drehung = _as_float(_merkmal(element.merkmale, "Drehung")) or 0.0
rad = math.radians(drehung)
cos_r, sin_r = math.cos(rad), math.sin(rad)
def world(lx: float, ly: float) -> tuple[float, float]:
return (element.x + lx * cos_r - ly * sin_r,
element.y + lx * sin_r + ly * cos_r)
half = abstand_mm / 2.0
return {"a": world(-half, 0.0), "b": world(half, 0.0), "r": r}
def kreisel_lane_for_point(capsule: dict, point: tuple[float, float]) -> str:
"""Welche Bahn (L/R) liegt ein Punkt bezogen auf die Kreiselachse a->b.
Kreuzprodukt Achse x (Punkt - a): positiv = links der Achsrichtung
(mathematisch positive/CCW-Drehung von der Achse aus), konsistent mit
dxfmakros' "links" = lokal Y in [0, +radius] (siehe kreisel_half_bboxes).
"""
ax, ay = capsule["a"]
bx, by = capsule["b"]
px, py = point
cross = (bx - ax) * (py - ay) - (by - ay) * (px - ax)
return LANE_LEFT if cross >= 0 else LANE_RIGHT
class GeometryIndex:
"""STRtree-Index der Bauteil-Geometrie fuer die geometrische TRO-Ableitung.
Zwei getrennte Baeume:
_host_tree eine LineString je Gefaellestrecke/Strecke (K1->K2-Achse)
bzw. Kreisel (AN8->SP8-Achse), abgefragt mit .nearest() -
"welcher Bauteil-ACHSE liegt dieser Separator am
naechsten" (resolve_host_geometric). Bewusst die Achse,
nicht die lose Bounding-Box: ein Kreisel deckt mit
seiner Box leicht 20+ Meter ab (Antriebs- bis
Spannstation), ein Separator auf einer nahen VF-Kette
waere nach reiner Box-Distanz faelschlich "im Kreisel",
obwohl er auf der Kette sitzt (siehe Modul-Historie:
genau dieser Fehler zeigte sich beim ersten Testlauf
gegen hundmM05_1.csv).
_endpoint_tree ein Punkt je AS/ES-Ende einer Strecke/Gefaellestrecke
plus je Kreisel-Bahnende, abgefragt mit .query(...,
predicate="dwithin") - "wie viele andere Ketten treffen
sich an diesem Punkt" (count_geometric_exits).
"""
def __init__(self, elements: list[Element], durchmesser_mm: float | None = None):
self.durchmesser_mm = durchmesser_mm
self._host_ids: list[str] = []
host_axes = []
# Kreisel-Kapseln nach TeileId, um am Treffer die Bahn (-L/-R) zu
# bestimmen - graph.nodes kennt Kreisel nur unter "<id>-L"/"<id>-R",
# nie unter der blanken TeileId (siehe material_flow.py::_build_nodes).
self._kreisel_capsules: dict[str, dict] = {}
self._endpoints: list[_EndpointEntry] = []
endpoint_points = []
for element in elements:
if element.kind in ("Gefaellestrecke", "Strecke"):
k1, k2 = chain_endpoints(element)
if k1 is None or k2 is None:
continue
self._host_ids.append(element.teile_id)
host_axes.append(LineString([k1, k2]))
for label, point in (("K1", k1), ("K2", k2)):
self._endpoints.append(_EndpointEntry(element.teile_id, label))
endpoint_points.append(Point(point))
elif element.kind == "Kreisel":
if element.x is None or element.y is None:
continue
capsule = kreisel_capsule(element, durchmesser_mm)
if capsule is None:
continue
self._host_ids.append(element.teile_id)
host_axes.append(LineString([capsule["a"], capsule["b"]]))
self._kreisel_capsules[element.teile_id] = capsule
for end in ("a", "b"):
self._endpoints.append(_EndpointEntry(element.teile_id, end))
endpoint_points.append(Point(capsule[end]))
self._host_tree = STRtree(host_axes) if host_axes else None
self._endpoint_tree = STRtree(endpoint_points) if endpoint_points else None
def nearest_host(self, x: float, y: float) -> str | None:
"""Naechster Host-Knoten nach Achsabstand (Gefaellestrecke/Strecke-
TeileId, oder "<Kreisel-TeileId>-L"/"-R" - dieselbe Schreibweise wie
graph.nodes)."""
if self._host_tree is None:
return None
index = self._host_tree.nearest(Point(x, y))
if index is None:
return None
teile_id = self._host_ids[int(index)]
capsule = self._kreisel_capsules.get(teile_id)
if capsule is None:
return teile_id
lane = kreisel_lane_for_point(capsule, (x, y))
return f"{teile_id}-{lane}"
def exits_near(self, x: float, y: float, tol_mm: float, exclude_ids: set[str]) -> set[str]:
"""Andere Bauteil-Ids, deren Anschlusspunkt innerhalb tol_mm von (x,y) liegt."""
if self._endpoint_tree is None:
return set()
hits = self._endpoint_tree.query(Point(x, y), predicate="dwithin", distance=tol_mm)
return {self._endpoints[int(i)].teile_id for i in hits} - exclude_ids
def resolve_host_geometric(separator: Element, index: GeometryIndex) -> str | None:
"""Naechster Host zum Separator rein nach Weltkoordinate."""
if separator.x is None or separator.y is None:
return None
return index.nearest_host(separator.x, separator.y)
def _strip_lane(host_id: str) -> str:
""""0006-L" -> "0006" (Kreisel-Bahnsuffix entfernen; alles andere unveraendert)."""
if host_id.endswith(("-L", "-R")):
return host_id[:-2]
return host_id
def count_geometric_exits(
hosts: list[str],
elements_by_id: dict[str, Element],
index: GeometryIndex,
tol_mm: float = DEFAULT_TOLERANCE_MM,
) -> list[str]:
"""Andere Ketten, die geometrisch an einem Ende von `hosts` zusammentreffen.
Entspricht tro_flow.py::_switch_exits, aber unabhaengig von graph.edges:
beide Enden JEDES Hosts (K1 und K2 bzw. beide Kreisel-Bahnenden) werden
abgefragt, die Treffer aller Enden vereinigt - wie eine aus beiden Enden
zusammengefuehrte "Nachbarn"-Spalte, nur direkt aus der Geometrie statt aus
der (fuer diese Bauteile teils unvollstaendigen) CSV-Spalte.
`hosts` kann Kreisel-Bahnen als "<TeileId>-L"/"-R" enthalten (siehe
GeometryIndex.nearest_host) - die Endpunkt-Eintraege im Index fuehren dazu
nur die blanke TeileId, daher wird der Suffix hier wie dort entfernt.
"""
exclude = {_strip_lane(h) for h in hosts}
found: set[str] = set()
for host_id in hosts:
element = elements_by_id.get(_strip_lane(host_id))
if element is None:
continue
if element.kind == "Kreisel":
capsule = kreisel_capsule(element, index.durchmesser_mm)
points = (capsule["a"], capsule["b"]) if capsule else ()
else:
points = tuple(p for p in chain_endpoints(element) if p is not None)
for x, y in points:
found |= index.exits_near(x, y, tol_mm, exclude)
return sorted(found)
def make_geometric_resolvers(
elements: list[Element],
tol_mm: float = DEFAULT_TOLERANCE_MM,
durchmesser_mm: float | None = None,
):
"""resolve_host/count_exits-Paar fuer tro_flow.py::derive_tros(...).
Ersetzt Zuordnung (Spalte) und Nachbarn/graph.edges durch eine direkte
STRtree-Nachbarsuche ueber die Weltkoordinaten der Bauteile. Weicht der
geometrisch gefundene Host von der CSV-Spalte "Zuordnung" ab, wird das als
Hinweis protokolliert statt stillschweigend ignoriert - das deckt zugleich
auf, wo die CSV-Spalte fehlerhaft ist.
"""
index = GeometryIndex(elements, durchmesser_mm)
by_id = {e.teile_id: e for e in elements}
def resolve_host(sep: Element, findings: list[str]) -> str | None:
host = resolve_host_geometric(sep, index)
if host is None:
findings.append(
f"Separator {sep.teile_id} '{sep.name}': geometrisch kein Host "
f"gefunden (CSV-Zuordnung '{sep.assignment or '-'}') - keinem "
f"TRO zugeordnet"
)
return None
if sep.assignment and sep.assignment != UNASSIGNED and sep.assignment != host:
findings.append(
f"Separator {sep.teile_id} '{sep.name}': geometrischer Host "
f"'{host}' weicht von CSV-Zuordnung '{sep.assignment}' ab"
)
return host
def count_exits(hosts: list[str]) -> list[str]:
return count_geometric_exits(hosts, by_id, index, tol_mm)
return resolve_host, count_exits
+310
View File
@@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
"""
tro_ids.py - stabile TRO-IDs ueber eine ID-Datei je Anlage.
Die TRO-IDs vergeben die SPS-Programmierer; eine ID muss nur eindeutig sein.
Sobald SCL-Code steht, darf sich eine ID nicht mehr aendern, nur weil das
Layout neu exportiert wurde. Die Layout-TeileId taugt dafuer nicht (sie
wechselt zwischen zwei Exporten derselben Anlage), stabil ist nur die Lage.
Siehe doc/Konzept_Layout_Hardware_SCL.md, "Festlegungen" -> "TRO-IDs".
Die ID-Datei (`%SKEL_CFG%/<csv>_tro_ids.ini`, versioniert, von Hand
umbenennbar) haelt je TRO die ID und die Lage seines Anker-Separators:
[tro]
TRO01 = 1234.5, -567.8 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L
K0102_1 = 2210.0, 410.0 ; von Hand umbenannt
Ablauf (assign_ids):
- ohne ID-Datei bzw. mit renumber: alle TROs in der uebergebenen Reihenfolge
(entlang des Materialflusses) neu durchnummerieren, TRO01, TRO02, ...
- mit ID-Datei: jeder Eintrag findet den TRO, dessen Separator seiner Lage am
naechsten liegt (innerhalb der Toleranz, je TRO hoechstens ein Eintrag).
Nicht gefundene TROs bekommen die naechste freie Nummer, nicht gefundene
Eintraege bleiben in der Datei stehen und ihre ID wird nicht wiederverwendet.
Das Modul liest und schreibt nur die Datei; Lage und Reihenfolge der TROs
liefert tro_flow.py.
"""
from __future__ import annotations
import configparser
import math
import re
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from parameter import get_float
__all__ = [
"SECTION",
"DEFAULT_TOLERANCE",
"IdEntry",
"IdAssignment",
"TroIds",
"ids_path",
"load_ids",
"assign_ids",
"render_ids",
"render_new_entries",
"insert_entries",
"other_sections",
]
SECTION = "tro"
ID_FILE_SUFFIX = "_tro_ids.ini"
PREFIX = "TRO"
# Abstand (mm) zwischen Eintrag und Separator, bis zu dem ein TRO als
# wiedergefunden gilt - aus cfg/parameter.ini [tro_ids] (lib/parameter.py).
DEFAULT_TOLERANCE = get_float("tro_ids", "default_tolerance")
# Eine ID wird zum SCL-Bezeichner (Instanz #<ID>, "DB_TRO_TEST".<ID>).
_ID_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
# SYSn sind die Systemgrenzen (tro_flow.derive_boundaries), keine TROs.
_RESERVED = re.compile(r"^SYS\d+$")
_NUMBERED = re.compile(rf"^{PREFIX}(\d+)$")
@dataclass
class IdEntry:
"""Ein Eintrag der ID-Datei: ID und Lage des Anker-Separators (mm)."""
tro_id: str
x: float
y: float
@dataclass
class IdAssignment:
"""Ergebnis von assign_ids, fuer den Bericht."""
renumbered: bool = False
# (ID, Abstand in mm) der wiedergefundenen TROs
kept: list[tuple[str, float]] = field(default_factory=list)
# IDs, die in diesem Lauf neu vergeben wurden (ohne renumbered)
new: list[str] = field(default_factory=list)
# Eintraege der Datei ohne passenden TRO
missing: list[IdEntry] = field(default_factory=list)
# weggefallene ID -> TRO, dessen Separator dort liegt, der aber schon eine
# naehere ID traegt (z. B. zwei TROs zu einem 2Sep2Swi zusammengelegt)
merged_into: dict[str, str] = field(default_factory=dict)
@dataclass
class TroIds:
"""ID-Datei einer Anlage plus Einstellungen fuer einen Lauf.
entries ist leer, wenn die Datei (noch) nicht existiert. result fuellt
tro_flow.derive_tros ueber assign_ids.
"""
path: Path
entries: list[IdEntry] = field(default_factory=list)
exists: bool = False
tolerance: float = DEFAULT_TOLERANCE
renumber: bool = False
result: IdAssignment | None = None
@classmethod
def load(cls, path: Path, *, tolerance: float = DEFAULT_TOLERANCE,
renumber: bool = False) -> "TroIds":
exists = path.is_file()
entries = load_ids(path) if exists else []
return cls(path=path, entries=entries, exists=exists,
tolerance=tolerance, renumber=renumber)
def ids_path(csv_stem: str, cfg_dir: Path) -> Path:
"""Standardpfad der ID-Datei zu einer CSV: <cfg>/<stem>_tro_ids.ini."""
return cfg_dir / f"{csv_stem}{ID_FILE_SUFFIX}"
def _parser() -> configparser.ConfigParser:
parser = configparser.ConfigParser(
inline_comment_prefixes=(";", "#"),
interpolation=None,
strict=True,
)
parser.optionxform = str # IDs nicht in Kleinbuchstaben wandeln
return parser
def load_ids(path: Path) -> list[IdEntry]:
"""ID-Datei lesen. ValueError bei doppelter ID, falscher Lage oder ID."""
parser = _parser()
try:
parser.read(path, encoding="utf-8")
except configparser.Error as exc:
raise ValueError(f"{path}: {exc}") from exc
if not parser.has_section(SECTION):
raise ValueError(f"{path}: Abschnitt [{SECTION}] fehlt")
entries: list[IdEntry] = []
for tro_id, value in parser.items(SECTION):
if not _ID_PATTERN.match(tro_id):
raise ValueError(f"{path}: '{tro_id}' ist kein gueltiger SCL-Bezeichner")
if _RESERVED.match(tro_id):
raise ValueError(f"{path}: '{tro_id}' ist fuer Systemgrenzen reserviert")
parts = [p.strip() for p in value.split(",")]
try:
x, y = (float(p) for p in parts)
except ValueError:
raise ValueError(f"{path}: {tro_id} = '{value}' - erwartet 'x, y' in mm") \
from None
entries.append(IdEntry(tro_id, x, y))
return entries
def _distance(tro, entry: IdEntry, anchors: dict[str, tuple[float, float]]) -> float:
"""Kleinster Abstand eines Separators des TRO zum Eintrag (inf ohne Lage)."""
best = math.inf
for sep in tro.separators:
pos = anchors.get(sep)
if pos is not None:
best = min(best, math.hypot(pos[0] - entry.x, pos[1] - entry.y))
return best
def _width(count: int) -> int:
return max(2, len(str(count)))
def assign_ids(
tros: list,
anchors: dict[str, tuple[float, float]],
entries: list[IdEntry],
*,
tolerance: float = DEFAULT_TOLERANCE,
renumber: bool = False,
) -> IdAssignment:
"""tro.tro_id fuer alle TROs setzen (Reihenfolge = Reihenfolge der Liste).
anchors: Separator-TeileId -> (x, y) in mm.
"""
result = IdAssignment(renumbered=renumber or not entries)
if result.renumbered:
width = _width(len(tros))
for index, tro in enumerate(tros, start=1):
tro.tro_id = f"{PREFIX}{index:0{width}d}"
return result
# Paare nach Abstand; jeder TRO und jeder Eintrag hoechstens einmal
pairs = sorted(
(d, i, j)
for i, tro in enumerate(tros)
for j, entry in enumerate(entries)
if (d := _distance(tro, entry, anchors)) <= tolerance
)
tro_taken: dict[int, tuple[int, float]] = {}
entry_taken: set[int] = set()
for d, i, j in pairs:
if i in tro_taken or j in entry_taken:
continue
tro_taken[i] = (j, d)
entry_taken.add(j)
used = {e.tro_id for e in entries}
numbers = [int(m.group(1)) for e in entries if (m := _NUMBERED.match(e.tro_id))]
widths = [len(m.group(1)) for e in entries if (m := _NUMBERED.match(e.tro_id))]
width = max(widths + [_width(len(tros))])
next_number = max(numbers, default=0) + 1
for i, tro in enumerate(tros):
if i in tro_taken:
j, d = tro_taken[i]
tro.tro_id = entries[j].tro_id
result.kept.append((tro.tro_id, d))
continue
while f"{PREFIX}{next_number:0{width}d}" in used:
next_number += 1
tro.tro_id = f"{PREFIX}{next_number:0{width}d}"
used.add(tro.tro_id)
next_number += 1
result.new.append(tro.tro_id)
result.missing = [e for j, e in enumerate(entries) if j not in entry_taken]
for d, i, j in pairs:
if j not in entry_taken:
result.merged_into.setdefault(entries[j].tro_id, tros[i].tro_id)
return result
def _anchor(tro, anchors: dict[str, tuple[float, float]]) -> tuple[float, float] | None:
for sep in tro.separators:
if sep in anchors:
return anchors[sep]
return None
def _entry_lines(tros: list, anchors: dict[str, tuple[float, float]]) -> list[str]:
lines = []
for tro in tros:
pos = _anchor(tro, anchors)
if pos is None:
continue
seps = ", ".join(tro.separators)
lines.append(f"{tro.tro_id} = {pos[0]:.1f}, {pos[1]:.1f}"
f" ; {tro.type_name} | {tro.label} | Sep {seps}")
return lines
def render_ids(tros: list, anchors: dict[str, tuple[float, float]], source: str) -> str:
"""Komplette ID-Datei (erster Lauf oder renumber)."""
head = [
"; ============================================================",
f"; TRO-IDs zu {source} - erzeugt von lib/tro_flow.py "
f"am {datetime.now():%Y-%m-%d}",
";",
"; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.",
"; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);",
"; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden",
"; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.",
"; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).",
"; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.",
"; ============================================================",
"",
f"[{SECTION}]",
]
return "\n".join(head + _entry_lines(tros, anchors)) + "\n"
def render_new_entries(tros: list, anchors: dict[str, tuple[float, float]],
new_ids: list[str], source: str) -> str:
"""Anhang fuer neu gefundene TROs an eine bestehende ID-Datei."""
neu = [t for t in tros if t.tro_id in set(new_ids)]
head = [f"; neu aus {source} am {datetime.now():%Y-%m-%d}"]
return "\n".join(head + _entry_lines(neu, anchors)) + "\n"
def other_sections(path: Path) -> str:
"""Alle Abschnitte ausser [tro] als Text (bleiben bei renumber erhalten)."""
if not path.is_file():
return ""
header = re.compile(r"^\s*\[(.+)\]\s*$")
keep: list[str] = []
current = None
for line in path.read_text(encoding="utf-8").splitlines():
if m := header.match(line):
current = m.group(1).strip()
if current is not None and current != SECTION:
keep.append(line)
return "\n".join(keep).strip("\n")
def insert_entries(path: Path, block: str) -> None:
"""block am Ende des Abschnitts [tro] einfuegen, Rest der Datei unveraendert."""
lines = path.read_text(encoding="utf-8").splitlines()
header = re.compile(r"^\s*\[(.+)\]\s*$")
start = next(i for i, line in enumerate(lines)
if (m := header.match(line)) and m.group(1).strip() == SECTION)
end = next((i for i in range(start + 1, len(lines)) if header.match(lines[i])),
len(lines))
# vor Leerzeilen am Abschnittsende einfuegen
while end > start + 1 and not lines[end - 1].strip():
end -= 1
lines[end:end] = block.rstrip("\n").splitlines()
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
+49 -237
View File
@@ -10,10 +10,13 @@ eigener TRO erkannt wurde. Solche Erkenntnisse kommen aus der Begehung bzw.
aus dem Review der erzeugten Diagramme.
Damit sie den naechsten Lauf ueberleben, stehen sie in einer INI-Datei
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien. `tro_flow.py`
und `tro_annotate.py` lesen sie und wenden sie nach der Ableitung an; die Doku
weist jede angewandte Korrektur getrennt aus, damit abgeleitet und behauptet
unterscheidbar bleiben.
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien.
Seit 2026-10-05 nur noch von `material_flow.py --connect` gelesen (Abschnitt
`[<Anlage>.manual]` der connect.ini). `tro_flow.py` und `tro_annotate.py`
wenden keine Korrekturen mehr an: die Abschnitte hingen an laufenden
TRO-Nummern und veralteten mit jedem Export - Fehler der Ableitung werden
seitdem im Code behoben (lib/tro_flow.py), nicht in der INI.
Aufbau der INI (Sektionsnamen mit dem CSV-Dateinamen als Praefix, damit
mehrere Anlagen in einer Datei stehen koennen):
@@ -23,17 +26,19 @@ mehrere Anlagen in einer Datei stehen koennen):
reason = ... Begruendung (kommt in die Doku)
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO (model.TroCandidate)
x = 529.95 Anlagenkoordinate in mm
y = -3895.27
z = 1468
expect = 1Sep1Swi hier erwarteter TRO-Typ
expect = 1Sep1Swi hier erwarteter TRO-Typ (-> type_name)
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
note = ... Erlaeuterung
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**. Sie werden nur
in der Doku und in der annotierten Zeichnung markiert, damit sie in BricsCAD
geprueft werden koennen, bevor daraus ein TRO wird.
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
`model.Connection` bzw. `model.TroCandidate`.
"""
from __future__ import annotations
@@ -43,6 +48,7 @@ import os
from dataclasses import dataclass, field
from pathlib import Path
from model import Connection, TroCandidate
from tro_catalog import get_tro
OVERRIDE_FILE = "tro_overrides.ini"
@@ -62,16 +68,17 @@ class TroOverride:
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
"""
key: str
by_separator: bool = False
# genau eins von beiden ist gesetzt
tro_id: str = ""
separator: str = ""
type_name: str = ""
reason: str = ""
confidence: str = ""
def matches(self, tro) -> bool:
if self.by_separator:
return self.key in tro.separators
return self.key == tro.tro_id
if self.separator:
return self.separator in tro.separators
return self.tro_id == tro.tro_id
@dataclass
@@ -89,21 +96,12 @@ class SplitSpec:
type_name: str
separators: list[str] = field(default_factory=list)
scanners: list[str] = field(default_factory=list)
host: str = ""
hosts: list[str] = field(default_factory=list)
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class ConnectionSpec:
"""Eine vor Ort bestaetigte Verbindung zwischen zwei TROs."""
src: str
dst: str
kind: str = ""
@dataclass
class MergeSpec:
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
@@ -118,28 +116,12 @@ class MergeSpec:
name: str
type_name: str
separators: list[str] = field(default_factory=list)
host: str = ""
hosts: list[str] = field(default_factory=list)
label: str = ""
reason: str = ""
confidence: str = DEFAULT_CONFIDENCE
@dataclass
class OpenPoint:
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
name: str
x: float
y: float
z: float | None = None
expect: str = ""
belongs_to: str = ""
note: str = ""
def describe_position(self) -> str:
return f"{self.x:.0f}, {self.y:.0f}"
@dataclass
class Overrides:
"""Alle Korrekturen zu einer CSV-Datei."""
@@ -149,15 +131,15 @@ class Overrides:
splits: list[SplitSpec] = field(default_factory=list)
merges: list[MergeSpec] = field(default_factory=list)
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
connections: list[ConnectionSpec] = field(default_factory=list)
open_points: list[OpenPoint] = field(default_factory=list)
connections: list[Connection] = field(default_factory=list)
candidates: list[TroCandidate] = field(default_factory=list)
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
applied: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
def __bool__(self) -> bool:
return bool(self.tros or self.splits or self.merges or self.connections
or self.open_points)
or self.candidates)
def override_path(cfg_dir: Path | None = None) -> Path:
@@ -178,6 +160,10 @@ def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
return None
def _as_list(raw: str) -> list[str]:
return [s.strip() for s in raw.split(",") if s.strip()]
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
path = override_path(cfg_dir)
@@ -204,7 +190,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
)
continue
result.connections.append(
ConnectionSpec(src=src, dst=dst, kind=rest.strip())
Connection(src=src, dst=dst, kind=rest.strip())
)
continue
@@ -229,8 +215,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
key = name.upper()
by_sep = key.startswith("SEP")
result.tros[key] = TroOverride(
key=key[3:] if by_sep else key,
by_separator=by_sep,
tro_id="" if by_sep else key,
separator=key[3:] if by_sep else "",
type_name=type_name,
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -243,7 +229,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
f"- neuer TRO nicht angelegt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
seps = _as_list(values.get("separators", ""))
if not seps:
result.warnings.append(
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
@@ -254,9 +240,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
name=name,
type_name=type_name,
separators=seps,
scanners=[s.strip() for s in values.get("scanners", "").split(",")
if s.strip()],
host=values.get("host", "").strip(),
scanners=_as_list(values.get("scanners", "")),
hosts=_as_list(values.get("hosts", values.get("host", ""))),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -270,7 +255,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
f"- Zusammenfassung nicht durchgefuehrt"
)
continue
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
seps = _as_list(values.get("separators", ""))
if len(seps) < 2:
result.warnings.append(
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
@@ -281,7 +266,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
name=name,
type_name=type_name,
separators=seps,
host=values.get("host", "").strip(),
hosts=_as_list(values.get("hosts", values.get("host", ""))),
label=values.get("label", "").strip(),
reason=values.get("reason", "").strip(),
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
@@ -293,200 +278,27 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
if x is None or y is None:
continue
z_raw = values.get("z", "").strip()
expect = values.get("expect", "").strip()
if expect and get_tro(expect) is None:
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
type_name = values.get("expect", "").strip()
if type_name and get_tro(type_name) is None:
result.warnings.append(
f"[{section}]: erwarteter Typ '{expect}' steht nicht im "
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
f"TRO-Katalog"
)
result.open_points.append(
OpenPoint(
result.candidates.append(
TroCandidate(
name=name,
x=x,
y=y,
z=_as_float(z_raw, f"[{section}] z", result.warnings)
if z_raw
else None,
expect=expect,
type_name=type_name,
position=(x, y, z if z is not None else 0.0),
belongs_to=values.get("belongs_to", "").strip(),
note=values.get("note", "").strip(),
)
)
else:
result.warnings.append(
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro' oder "
f"'openpoint'"
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
f"'merge' oder 'openpoint'"
)
result.open_points.sort(key=lambda p: p.name)
result.candidates.sort(key=lambda p: p.name)
return result
def apply_splits(tros, overrides: Overrides, make_tro) -> list:
"""Neue TROs aus bestehenden herausloesen (vor der Nummernvergabe).
`make_tro(spec, hosts, label)` erzeugt das projektspezifische Tro-Objekt -
so bleibt dieses Modul frei von einem Import aus tro_flow (das umgekehrt
schon von hier importiert).
"""
for spec in overrides.splits:
quellen = [t for t in tros if set(spec.separators) & set(t.separators)]
if not quellen:
overrides.warnings.append(
f"Split '{spec.name}': keiner der Separatoren "
f"{', '.join(spec.separators)} kommt in diesem Lauf vor - "
f"nicht angelegt"
)
continue
hosts: list[str] = []
for quelle in quellen:
quelle.separators = [s for s in quelle.separators
if s not in spec.separators]
quelle.scanners = [s for s in quelle.scanners if s not in spec.scanners]
hosts.extend(quelle.hosts)
if not quelle.separators and quelle.type_name not in ("Vario",):
overrides.warnings.append(
f"Split '{spec.name}': {quelle.label} hat danach keinen "
f"Separator mehr - bitte pruefen"
)
ziel_hosts = [spec.host] if spec.host else sorted(set(hosts))[:1]
label = spec.label or (
f"Separator {', '.join(spec.separators)} "
f"(manuell aus {quellen[0].label} herausgeloest)"
)
tros.append(make_tro(spec, ziel_hosts, label))
overrides.applied.append(
f"neu {spec.name}: {spec.type_name} auf {', '.join(ziel_hosts)} "
f"mit Separator {', '.join(spec.separators)}, herausgeloest aus "
f"{', '.join(sorted({q.label for q in quellen}))} "
f"({spec.reason or 'manuelle Ergaenzung'})"
)
return tros
def apply_merges(tros, overrides: Overrides) -> list:
"""Mehrere TROs zu einem zusammenfassen (nach den Splits, vor der Nummer).
Der erste betroffene TRO bleibt bestehen und uebernimmt Bauteile, Hosts und
Typ; die uebrigen werden aus der Liste entfernt.
"""
for spec in overrides.merges:
betroffen = [t for t in tros if set(spec.separators) & set(t.separators)]
if len(betroffen) < 2:
gefunden = ", ".join(t.tro_id or t.label for t in betroffen) or "keiner"
overrides.warnings.append(
f"Merge '{spec.name}': nur {len(betroffen)} der Separatoren "
f"{', '.join(spec.separators)} gefunden ({gefunden}) - "
f"nicht zusammengefasst"
)
continue
ziel = betroffen[0]
quellen = betroffen[1:]
for quelle in quellen:
ziel.separators = sorted(set(ziel.separators) | set(quelle.separators))
ziel.scanners = sorted(set(ziel.scanners) | set(quelle.scanners))
ziel.hosts = sorted(set(ziel.hosts) | set(quelle.hosts))
tros.remove(quelle)
if spec.host:
# Ein Host bestimmt, wo der zusammengefasste TRO im Fluss haengt;
# die uebrigen Bahnen bleiben als weitere Hosts erhalten, damit die
# Verknuepfung beide Seiten des Uebergangs sieht.
ziel.hosts = [spec.host] + [h for h in ziel.hosts if h != spec.host]
ziel.type_name = spec.type_name
ziel.confidence = spec.confidence
ziel.label = spec.label or ziel.label
grund = spec.reason or "manuell zusammengefasst"
ziel.reason = f"{ziel.reason}; manuell zusammengefasst ({grund})"
overrides.applied.append(
f"merge {spec.name}: {len(betroffen)} TROs -> ein {spec.type_name} "
f"mit Separatoren {', '.join(ziel.separators)} ({grund})"
)
return tros
def apply_connections(tros, overrides: Overrides):
"""Gesetzte Topologie an die Stelle der abgeleiteten Kanten setzen.
Ersetzt vollstaendig, nicht ergaenzend: wenn die Verbindungen vor Ort
aufgenommen wurden, sind die abgeleiteten damit erledigt - sonst blieben
genau die Kanten stehen, die korrigiert werden sollten. Was dabei wegfaellt
bzw. hinzukommt, steht anschliessend in overrides.applied.
Rueckgabe: (edges, edge_kinds) wie von link_tros.
"""
bekannt = {t.tro_id for t in tros}
edges: list[tuple[str, str]] = []
kinds: dict[tuple[str, str], str] = {}
for spec in overrides.connections:
for seite, wert in (("von", spec.src), ("nach", spec.dst)):
if wert not in bekannt:
overrides.warnings.append(
f"Verbindung {spec.src} -> {spec.dst}: {seite} '{wert}' ist "
f"kein TRO dieses Laufs - Verbindung ignoriert"
)
break
else:
paar = (spec.src, spec.dst)
if paar in kinds:
continue
edges.append(paar)
kinds[paar] = spec.kind or "gesetzt"
for tro in tros:
tro.successors = sorted(d for s, d in edges if s == tro.tro_id)
tro.predecessors = sorted(s for s, d in edges if d == tro.tro_id)
tro.colocated = []
ohne = [t.tro_id for t in tros if not t.successors and not t.predecessors]
if ohne:
overrides.warnings.append(
"ohne Verbindung in der gesetzten Topologie: " + ", ".join(sorted(ohne))
)
overrides.applied.append(
f"Topologie gesetzt: {len(edges)} Verbindungen aus der Override-Datei "
f"ersetzen die abgeleiteten"
)
return sorted(edges), kinds
def apply_overrides(tros, overrides: Overrides) -> None:
"""Typkorrekturen auf die abgeleitete TRO-Liste anwenden.
Wird aufgerufen, nachdem die TRO-IDs vergeben sind (die Korrekturen sind
ueber die ID adressiert) und bevor FB-Baustein und Bauteilliste aus dem
Katalog gezogen werden - der Typwechsel zieht beides mit.
"""
if not overrides.tros:
return
for section_key, override in sorted(overrides.tros.items()):
treffer = [t for t in tros if override.matches(t)]
if not treffer:
wie = "Separator" if override.by_separator else "TRO-ID"
overrides.warnings.append(
f"[{section_key}]: kein TRO mit {wie} '{override.key}' in diesem "
f"Lauf - Korrektur nicht angewandt"
+ ("" if override.by_separator else
" (Nummerierung geaendert? Besser ueber SEP<Nummer> adressieren)")
)
continue
if len(treffer) > 1:
overrides.warnings.append(
f"[{section_key}]: trifft {len(treffer)} TROs "
f"({', '.join(t.tro_id for t in treffer)}) - auf alle angewandt"
)
for tro in treffer:
if not override.type_name or override.type_name == tro.type_name:
continue
vorher = tro.type_name
tro.type_name = override.type_name
tro.confidence = override.confidence
grund = override.reason or "manuelle Korrektur"
tro.reason = (f"{tro.reason}; manuell auf {override.type_name} "
f"gesetzt ({grund})")
overrides.applied.append(
f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})"
)
+5 -6
View File
@@ -3,15 +3,14 @@
pydantic >= 2.0.0
# SCL-Templates (scl_skeleton --templates, lib/scl_templates/*.scl.j2)
jinja2 >= 3.1
# DXF lesen und schreiben (tro_annotate). DWG wird nicht unterstuetzt.
ezdxf >= 1.4.0
# STRtree fuer die geometrische TRO-Ableitung (tro_flow.py --geometry, siehe
# lib/tro_geometry.py) - nur zur Laufzeit von --geometry benoetigt.
shapely >= 2.1
# Nur fuer Vorschaubilder eines DXF (ezdxf.addons.drawing) - optional:
# matplotlib >= 3.8
# Noch nicht benoetigt:
# pytest >= 9.0.0
# Tests (tests/, Aufruf: python -m pytest tests/)
pytest >= 9.0.0
+9
View File
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""lib/ importierbar machen, auch ohne vorher bin/setenv aufzurufen."""
import sys
from pathlib import Path
LIB = Path(__file__).resolve().parent.parent / "lib"
if str(LIB) not in sys.path:
sys.path.insert(0, str(LIB))
+967
View File
@@ -0,0 +1,967 @@
// ============================================================
// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py
// Quelle : HundM05_tro.json
// Erzeugt: <maskiert>
//
// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum
// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem
// Anlagenlayout und der vor Ort aufgenommenen Topologie.
//
// Jede Stelle, die aus der E-Planung kommen muss, ist mit
// 'TODO(E-Planung)' markiert - danach suchen, dann ist nichts vergessen.
// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),
// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche
// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.
//
// Reihenfolge der REGIONs = Materialfluss.
// Anfang bei TRO20: Materialeingang ueber SYS1, SYS2.
// ============================================================
FUNCTION_BLOCK "FB_Main"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR
TRO20 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0007
TRO01 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0008
TRO02 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0025
TRO06 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0010
TRO03 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0026
TRO07 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0031
TRO09 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0012
TRO04 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0027
TRO08 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0032
TRO10 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0035
TRO12 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0014
TRO05 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0028
TRO11 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0036
TRO13 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0039
TRO15 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0016
TRO14 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0040
TRO16 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0018
TRO17 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0043
TRO19 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0020
TRO18 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0044
END_VAR
BEGIN
REGION TRO20 (1Sep) Sep 0007
// Anlagenkoordinate x/y: 2415 / 365 mm
// Vorgaenger: SYS1, SYS2, TRO19
// Nachfolger: TRO01
// Einstufung: mittel
// Separator 1 = Bauteil 0007 aus dem Layout
#TRO20.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO20.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO20.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO20.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO20.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO20.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO20.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO20.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO20.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO20.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO20.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO20.stInPriorityManager.xReleaseOk := TRUE;
#TRO20(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO20,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0007 von SYS1 (Station, kein TRO), SYS2 (Station, kein TRO), TRO19
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 vor TRO01
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO20,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO01 (1Sep1Swi) Sep 0008
// Anlagenkoordinate x/y: 4076 / 985 mm
// Vorgaenger: TRO20
// Nachfolger: TRO02, TRO06
// Einstufung: mittel
// Separator 1 = Bauteil 0008 aus dem Layout
#TRO01.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO01.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO01.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO01.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO01.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO01.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO01.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO01.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO01.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO01.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO01.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO02: SYS3, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
// ueber TRO06: SYS3, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
#TRO01.stInPriorityManager.xReleaseOk := TRUE;
#TRO01(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO01,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 von TRO20
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 vor TRO02
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO01,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO01.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO02 (Vario) Sep 0025
// Anlagenkoordinate x/y: 5070 / 2530 mm
// Vorgaenger: TRO01
// Nachfolger: TRO03
// Einstufung: hoch
// Separator 1 = Bauteil 0025 aus dem Layout
#TRO02.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO02.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO02.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO02.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO02.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO02.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO02.stInVario.Settings.xContinousEmptying := FALSE;
#TRO02.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO02.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO02.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO02.stInVario.Settings.tWaitForStop := t#3s;
#TRO02.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO02.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO02.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO02.stInVario.Settings.tJamSenDelay := T#2s;
#TRO02.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInPriorityManager.xReleaseOk := TRUE;
#TRO02(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO02,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 von TRO01
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 vor TRO03
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO02,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO06 (1Sep1Swi) Sep 0010
// Anlagenkoordinate x/y: 8577 / 989 mm
// Vorgaenger: TRO01, TRO05
// Nachfolger: TRO07, TRO09
// Einstufung: mittel
// Separator 1 = Bauteil 0010 aus dem Layout
#TRO06.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO06.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO06.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO06.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO06.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO06.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO06.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO06.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO06.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO06.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO06.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO06.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO07: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
// ueber TRO09: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
#TRO06.stInPriorityManager.xReleaseOk := TRUE;
#TRO06(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO06,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 von TRO01, TRO05
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 vor TRO07
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO06,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO06.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO03 (1Sep) Sep 0026
// Anlagenkoordinate x/y: 7223 / 6180 mm
// Vorgaenger: TRO02
// Nachfolger: TRO04
// Einstufung: mittel
// Separator 1 = Bauteil 0026 aus dem Layout
#TRO03.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO03.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO03.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO03.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO03.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO03.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO03.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO03.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO03.stInPriorityManager.xReleaseOk := TRUE;
#TRO03(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO03,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 von TRO02
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 vor TRO04
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO03,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO07 (Vario) Sep 0031
// Anlagenkoordinate x/y: 9571 / 5933 mm
// Vorgaenger: TRO06
// Nachfolger: TRO08
// Einstufung: hoch
// Separator 1 = Bauteil 0031 aus dem Layout
#TRO07.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO07.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO07.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO07.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO07.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO07.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO07.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO07.stInVario.Settings.xContinousEmptying := FALSE;
#TRO07.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO07.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO07.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO07.stInVario.Settings.tWaitForStop := t#3s;
#TRO07.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO07.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO07.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO07.stInVario.Settings.tJamSenDelay := T#2s;
#TRO07.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInPriorityManager.xReleaseOk := TRUE;
#TRO07(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO07,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 von TRO06
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 vor TRO08
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO07,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO09 (1Sep1Swi) Sep 0012
// Anlagenkoordinate x/y: 12650 / 987 mm
// Vorgaenger: TRO06, TRO08
// Nachfolger: TRO10, TRO12
// Einstufung: mittel
// Separator 1 = Bauteil 0012 aus dem Layout
#TRO09.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO09.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO09.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO09.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO09.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO09.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO09.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO09.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO09.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO09.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO09.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO09.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO10: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
// ueber TRO12: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
#TRO09.stInPriorityManager.xReleaseOk := TRUE;
#TRO09(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO09,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO06, TRO08
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 vor TRO10
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO09,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO09.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO04 (Vario) Sep 0027
// Anlagenkoordinate x/y: 7223 / 5680 mm
// Vorgaenger: TRO03
// Nachfolger: TRO05
// Einstufung: hoch
// Separator 1 = Bauteil 0027 aus dem Layout
#TRO04.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO04.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO04.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO04.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO04.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO04.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO04.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO04.stInVario.Settings.xContinousEmptying := FALSE;
#TRO04.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO04.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO04.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO04.stInVario.Settings.tWaitForStop := t#3s;
#TRO04.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO04.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO04.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO04.stInVario.Settings.tJamSenDelay := T#2s;
#TRO04.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInPriorityManager.xReleaseOk := TRUE;
#TRO04(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO04,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 von TRO03
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 vor TRO05
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO04,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO08 (1Sep) Sep 0032
// Anlagenkoordinate x/y: 10841 / 1724 mm
// Vorgaenger: TRO07
// Nachfolger: TRO09
// Einstufung: mittel
// Separator 1 = Bauteil 0032 aus dem Layout
#TRO08.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO08.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO08.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO08.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO08.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO08.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO08.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO08.stInPriorityManager.xReleaseOk := TRUE;
#TRO08(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO08,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 von TRO07
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO08,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO10 (Vario) Sep 0035
// Anlagenkoordinate x/y: 13648 / 5933 mm
// Vorgaenger: TRO09
// Nachfolger: TRO11
// Einstufung: hoch
// Separator 1 = Bauteil 0035 aus dem Layout
#TRO10.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO10.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO10.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO10.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO10.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO10.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO10.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO10.stInVario.Settings.xContinousEmptying := FALSE;
#TRO10.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO10.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO10.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO10.stInVario.Settings.tWaitForStop := t#3s;
#TRO10.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO10.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO10.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO10.stInVario.Settings.tJamSenDelay := T#2s;
#TRO10.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInPriorityManager.xReleaseOk := TRUE;
#TRO10(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO10,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 von TRO09
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 vor TRO11
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO10,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO12 (1Sep1Swi) Sep 0014
// Anlagenkoordinate x/y: 16820 / 991 mm
// Vorgaenger: TRO09, TRO11
// Nachfolger: TRO13, TRO15
// Einstufung: mittel
// Separator 1 = Bauteil 0014 aus dem Layout
#TRO12.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO12.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO12.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO12.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO12.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO12.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO12.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO12.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO12.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO12.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO12.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO12.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO13: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
// ueber TRO15: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
#TRO12.stInPriorityManager.xReleaseOk := TRUE;
#TRO12(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO12,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 von TRO09, TRO11
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 vor TRO13
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO12,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO12.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO05 (1Sep) Sep 0028
// Anlagenkoordinate x/y: 6274 / 1756 mm
// Vorgaenger: TRO04
// Nachfolger: TRO06
// Einstufung: mittel
// Separator 1 = Bauteil 0028 aus dem Layout
#TRO05.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO05.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO05.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO05.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO05.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO05.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO05.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO05.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO05.stInPriorityManager.xReleaseOk := TRUE;
#TRO05(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO05,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 von TRO04
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO05,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO11 (1Sep) Sep 0036
// Anlagenkoordinate x/y: 14918 / 1725 mm
// Vorgaenger: TRO10
// Nachfolger: TRO12
// Einstufung: mittel
// Separator 1 = Bauteil 0036 aus dem Layout
#TRO11.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO11.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO11.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO11.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO11.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO11.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO11.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO11.stInPriorityManager.xReleaseOk := TRUE;
#TRO11(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO11,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von TRO10
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO11,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO13 (Vario) Sep 0039
// Anlagenkoordinate x/y: 17816 / 5934 mm
// Vorgaenger: TRO12
// Nachfolger: TRO14
// Einstufung: hoch
// Separator 1 = Bauteil 0039 aus dem Layout
#TRO13.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO13.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO13.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO13.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO13.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO13.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO13.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO13.stInVario.Settings.xContinousEmptying := FALSE;
#TRO13.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO13.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO13.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO13.stInVario.Settings.tWaitForStop := t#3s;
#TRO13.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO13.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO13.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO13.stInVario.Settings.tJamSenDelay := T#2s;
#TRO13.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInPriorityManager.xReleaseOk := TRUE;
#TRO13(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO13,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 von TRO12
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 vor TRO14
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO13,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO15 (1Sep1Swi) Sep 0016
// Anlagenkoordinate x/y: 20692 / 985 mm
// Vorgaenger: TRO12, TRO14
// Nachfolger: TRO16, TRO17
// Einstufung: mittel
// Separator 1 = Bauteil 0016 aus dem Layout
#TRO15.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO15.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO15.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO15.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO15.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO15.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO15.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO15.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO15.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO15.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO15.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO15.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO16: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO16, TRO19, TRO20
// ueber TRO17: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO16, TRO17, TRO18, TRO19, TRO20
#TRO15.stInPriorityManager.xReleaseOk := TRUE;
#TRO15(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO15,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 von TRO12, TRO14
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO17
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO15,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO15.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO14 (1Sep) Sep 0040
// Anlagenkoordinate x/y: 19086 / 1725 mm
// Vorgaenger: TRO13
// Nachfolger: TRO15
// Einstufung: mittel
// Separator 1 = Bauteil 0040 aus dem Layout
#TRO14.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO14.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO14.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO14.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO14.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO14.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO14.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO14.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO14.stInPriorityManager.xReleaseOk := TRUE;
#TRO14(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO14,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO13
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO14,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO16 (1Sep) Sep 0018
// Anlagenkoordinate x/y: 23729 / 990 mm
// Vorgaenger: TRO15, TRO18
// Nachfolger: TRO19
// Einstufung: mittel
// Separator 1 = Bauteil 0018 aus dem Layout
#TRO16.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO16.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO16.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO16.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO16.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO16.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO16.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO16.stInPriorityManager.xReleaseOk := TRUE;
#TRO16(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO16,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 von TRO15, TRO18
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 vor TRO19
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO16,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO17 (Vario) Sep 0043
// Anlagenkoordinate x/y: 21590 / 5933 mm
// Vorgaenger: TRO15
// Nachfolger: TRO18
// Einstufung: hoch
// Separator 1 = Bauteil 0043 aus dem Layout
#TRO17.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO17.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO17.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO17.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO17.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO17.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO17.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO17.stInVario.Settings.xContinousEmptying := FALSE;
#TRO17.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO17.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO17.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO17.stInVario.Settings.tWaitForStop := t#3s;
#TRO17.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO17.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO17.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO17.stInVario.Settings.tJamSenDelay := T#2s;
#TRO17.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInPriorityManager.xReleaseOk := TRUE;
#TRO17(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO17,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO15
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO18
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO17,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO19 (1Sep) Sep 0020
// Anlagenkoordinate x/y: 24570 / 1167 mm
// Vorgaenger: TRO16
// Nachfolger: SYS3, TRO20
// Einstufung: mittel
// Separator 1 = Bauteil 0020 aus dem Layout
#TRO19.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO19.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO19.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO19.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO19.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO19.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO19.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO19.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO19.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO19.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO19.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO19.stInPriorityManager.xReleaseOk := TRUE;
#TRO19(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO19,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 von TRO16
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO)
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0007 vor TRO20
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO19,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO18 (1Sep) Sep 0044
// Anlagenkoordinate x/y: 22860 / 1724 mm
// Vorgaenger: TRO17
// Nachfolger: TRO16
// Einstufung: mittel
// Separator 1 = Bauteil 0044 aus dem Layout
#TRO18.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO18.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO18.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO18.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO18.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO18.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO18.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO18.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
#TRO18(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO18,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO17
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO18,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
END_FUNCTION_BLOCK
File diff suppressed because it is too large. Load diff
+932
View File
@@ -0,0 +1,932 @@
{
"schema": "sps_skel-tro-v3",
"source": "HundM05.csv",
"created": "2026-10-09 11:30",
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
"tros": [
{
"id": "TRO01",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0008"
],
"predecessors": [
"TRO20"
],
"successors": [
"TRO02",
"TRO06"
],
"position": {
"x": 4075.94,
"y": 985.0,
"z": 2154.75
},
"label": "Separator 0008 an Ausschleuselement 0024 (Strecke 0001, Anfang)",
"hosts": [
"0001"
],
"scanners": [
"0009"
],
"reason": "1014mm von Ausschleuselement 0024 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO02",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0025"
],
"predecessors": [
"TRO01"
],
"successors": [
"TRO03"
],
"position": {
"x": 5070.1,
"y": 2530.06,
"z": 2098.34
},
"label": "VF_1",
"hosts": [
"0001"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,horizontal,Auf,horizontal)",
"colocated": [
"TRO04",
"TRO05"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO03",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0026"
],
"predecessors": [
"TRO02"
],
"successors": [
"TRO04"
],
"position": {
"x": 7222.54,
"y": 6180.03,
"z": 2019.94
},
"label": "VF_1",
"hosts": [
"0001"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (3. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [
"TRO05"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO04",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0027"
],
"predecessors": [
"TRO03"
],
"successors": [
"TRO05"
],
"position": {
"x": 7222.54,
"y": 5679.62,
"z": 1993.71
},
"label": "VF_1",
"hosts": [
"0001"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,horizontal,Auf,horizontal)",
"colocated": [
"TRO02"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO05",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0028"
],
"predecessors": [
"TRO04"
],
"successors": [
"TRO06"
],
"position": {
"x": 6273.83,
"y": 1756.17,
"z": 2305.73
},
"label": "VF_1",
"hosts": [
"0001"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [
"TRO02",
"TRO03"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO06",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0010"
],
"predecessors": [
"TRO01",
"TRO05"
],
"successors": [
"TRO07",
"TRO09"
],
"position": {
"x": 8577.09,
"y": 988.6,
"z": 2154.75
},
"label": "Separator 0010 an Ausschleuselement 0030 (Strecke 0002, Anfang)",
"hosts": [
"0002"
],
"scanners": [
"0011"
],
"reason": "1015mm von Ausschleuselement 0030 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO07",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0031"
],
"predecessors": [
"TRO06"
],
"successors": [
"TRO08"
],
"position": {
"x": 9570.9,
"y": 5932.96,
"z": 1918.34
},
"label": "VF_2",
"hosts": [
"0002"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO08",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0032"
],
"predecessors": [
"TRO07"
],
"successors": [
"TRO09"
],
"position": {
"x": 10840.84,
"y": 1724.4,
"z": 2305.73
},
"label": "VF_2",
"hosts": [
"0002"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_2 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO09",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0012"
],
"predecessors": [
"TRO06",
"TRO08"
],
"successors": [
"TRO10",
"TRO12"
],
"position": {
"x": 12650.4,
"y": 987.3,
"z": 2154.75
},
"label": "Separator 0012 an Ausschleuselement 0034 (Strecke 0003, Anfang)",
"hosts": [
"0003"
],
"scanners": [
"0013"
],
"reason": "1016mm von Ausschleuselement 0034 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO10",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0035"
],
"predecessors": [
"TRO09"
],
"successors": [
"TRO11"
],
"position": {
"x": 13648.0,
"y": 5933.46,
"z": 1918.34
},
"label": "VF_3",
"hosts": [
"0003"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO11",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0036"
],
"predecessors": [
"TRO10"
],
"successors": [
"TRO12"
],
"position": {
"x": 14917.94,
"y": 1724.9,
"z": 2305.73
},
"label": "VF_3",
"hosts": [
"0003"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_3 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO12",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0014"
],
"predecessors": [
"TRO09",
"TRO11"
],
"successors": [
"TRO13",
"TRO15"
],
"position": {
"x": 16819.62,
"y": 990.75,
"z": 2154.75
},
"label": "Separator 0014 an Ausschleuselement 0038 (Strecke 0004, Anfang)",
"hosts": [
"0004"
],
"scanners": [
"0015"
],
"reason": "1016mm von Ausschleuselement 0038 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO13",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0039"
],
"predecessors": [
"TRO12"
],
"successors": [
"TRO14"
],
"position": {
"x": 17816.0,
"y": 5933.96,
"z": 1918.34
},
"label": "VF_4",
"hosts": [
"0004"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO14",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0040"
],
"predecessors": [
"TRO13"
],
"successors": [
"TRO15"
],
"position": {
"x": 19085.94,
"y": 1725.4,
"z": 2305.73
},
"label": "VF_4",
"hosts": [
"0004"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_4 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO15",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0016"
],
"predecessors": [
"TRO12",
"TRO14"
],
"successors": [
"TRO16",
"TRO17"
],
"position": {
"x": 20692.43,
"y": 985.25,
"z": 2154.75
},
"label": "Separator 0016 an Ausschleuselement 0042 (Strecke 0005, Anfang)",
"hosts": [
"0005"
],
"scanners": [
"0017"
],
"reason": "1013mm von Ausschleuselement 0042 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO16",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0018"
],
"predecessors": [
"TRO15",
"TRO18"
],
"successors": [
"TRO19"
],
"position": {
"x": 23729.42,
"y": 989.7,
"z": 2154.75
},
"label": "VF_5",
"hosts": [
"0005"
],
"scanners": [
"0019"
],
"reason": "Separator 0018: Zuordnung zeigt auf VF_5, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
"colocated": [
"TRO17"
],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO17",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0043"
],
"predecessors": [
"TRO15"
],
"successors": [
"TRO18"
],
"position": {
"x": 21590.2,
"y": 5932.86,
"z": 1918.34
},
"label": "VF_5",
"hosts": [
"0005"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
"colocated": [
"TRO16"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO18",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0044"
],
"predecessors": [
"TRO17"
],
"successors": [
"TRO16"
],
"position": {
"x": 22860.14,
"y": 1724.3,
"z": 2305.73
},
"label": "VF_5",
"hosts": [
"0005"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_5 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO19",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0020"
],
"predecessors": [
"TRO16"
],
"successors": [
"SYS3",
"TRO20"
],
"position": {
"x": 24570.15,
"y": 1167.2,
"z": 2155.5
},
"label": "Separator 0020 auf Kreisel1 Bahn L",
"hosts": [
"0006-L"
],
"scanners": [],
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO20",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0007"
],
"predecessors": [
"SYS1",
"SYS2",
"TRO19"
],
"successors": [
"TRO01"
],
"position": {
"x": 2414.95,
"y": 365.0,
"z": 2155.5
},
"label": "Separator 0007 auf Kreisel1 Bahn R",
"hosts": [
"0006-R"
],
"scanners": [],
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
}
],
"boundaries": [
{
"id": "SYS1",
"type": "Materialeingang",
"confidence": "hoch",
"separators": [],
"predecessors": [],
"successors": [
"TRO20"
],
"position": {
"x": 5532.67,
"y": 265.14,
"z": 2109.31
},
"label": "BTMT Beladung :2",
"hosts": [
"0021"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
},
{
"id": "SYS2",
"type": "Materialeingang",
"confidence": "hoch",
"separators": [],
"predecessors": [],
"successors": [
"TRO20"
],
"position": {
"x": 4503.25,
"y": 269.77,
"z": 2109.31
},
"label": "BTMT Beladung :1",
"hosts": [
"0022"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
},
{
"id": "SYS3",
"type": "Materialausgang",
"confidence": "hoch",
"separators": [],
"predecessors": [
"TRO19"
],
"successors": [],
"position": {
"x": 25014.37,
"y": 1545.51,
"z": 2001.09
},
"label": "BTMT Entladung :1",
"hosts": [
"0023"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
}
],
"connections": [
{
"from": "SYS1",
"to": "TRO20",
"kind": "normal"
},
{
"from": "SYS2",
"to": "TRO20",
"kind": "normal"
},
{
"from": "TRO01",
"to": "TRO02",
"kind": "weiche"
},
{
"from": "TRO01",
"to": "TRO06",
"kind": "bahn"
},
{
"from": "TRO02",
"to": "TRO03",
"kind": "bahn"
},
{
"from": "TRO03",
"to": "TRO04",
"kind": "bahn"
},
{
"from": "TRO04",
"to": "TRO05",
"kind": "bahn"
},
{
"from": "TRO05",
"to": "TRO06",
"kind": "bahn"
},
{
"from": "TRO06",
"to": "TRO07",
"kind": "weiche"
},
{
"from": "TRO06",
"to": "TRO09",
"kind": "bahn"
},
{
"from": "TRO07",
"to": "TRO08",
"kind": "bahn"
},
{
"from": "TRO08",
"to": "TRO09",
"kind": "bahn"
},
{
"from": "TRO09",
"to": "TRO10",
"kind": "weiche"
},
{
"from": "TRO09",
"to": "TRO12",
"kind": "bahn"
},
{
"from": "TRO10",
"to": "TRO11",
"kind": "bahn"
},
{
"from": "TRO11",
"to": "TRO12",
"kind": "bahn"
},
{
"from": "TRO12",
"to": "TRO13",
"kind": "weiche"
},
{
"from": "TRO12",
"to": "TRO15",
"kind": "bahn"
},
{
"from": "TRO13",
"to": "TRO14",
"kind": "bahn"
},
{
"from": "TRO14",
"to": "TRO15",
"kind": "bahn"
},
{
"from": "TRO15",
"to": "TRO16",
"kind": "bahn"
},
{
"from": "TRO15",
"to": "TRO17",
"kind": "weiche"
},
{
"from": "TRO16",
"to": "TRO19",
"kind": "bahn"
},
{
"from": "TRO17",
"to": "TRO18",
"kind": "bahn"
},
{
"from": "TRO18",
"to": "TRO16",
"kind": "bahn"
},
{
"from": "TRO19",
"to": "SYS3",
"kind": "normal"
},
{
"from": "TRO19",
"to": "TRO20",
"kind": "bahn"
},
{
"from": "TRO20",
"to": "TRO01",
"kind": "bahn"
}
],
"candidates": [],
"findings": [
"Scanner 0019 'Scanner :1': montiert an 0006-L, liest aber an Separator 0018 auf 0005",
"TRO16 (VF_5): Scanner 0019 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
"VF_1 Separator 0026: alternierendes Muster (3. von hinten) - als 1Sep statt Vario eingestuft",
"VF_1 Separator 0028: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_2 Separator 0032: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_3 Separator 0036: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_4 Separator 0040: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_5 Separator 0018: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
"VF_5 Separator 0044: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft"
]
}
+851
View File
@@ -0,0 +1,851 @@
// ============================================================
// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py
// Quelle : mubea_tro.json
// Erzeugt: <maskiert>
//
// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum
// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem
// Anlagenlayout und der vor Ort aufgenommenen Topologie.
//
// Jede Stelle, die aus der E-Planung kommen muss, ist mit
// 'TODO(E-Planung)' markiert - danach suchen, dann ist nichts vergessen.
// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),
// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche
// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.
//
// Reihenfolge der REGIONs = Materialfluss.
// Anfang bei TRO03: Materialeingang ueber SYS1.
// ============================================================
FUNCTION_BLOCK "FB_Main"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR
TRO03 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0042
TRO05 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0044
TRO18 : "FB_ILS_MTRO_PinStore_Auto"; // PinStore_Auto, Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
TRO07 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0045
TRO10 : "FB_ILS_MTRO_2Sep2Swi"; // 2Sep2Swi, Sep 0005, 0036
TRO04 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0043
TRO01 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0040
TRO02 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0012
TRO11 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0058
TRO13 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0011
TRO12 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0059
TRO16 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0062
TRO17 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0063
TRO15 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0038
TRO06 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0003
TRO14 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0037
TRO08 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0054
TRO09 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0055
END_VAR
BEGIN
REGION TRO03 (1Sep) Sep 0042
// Anlagenkoordinate x/y: 10490 / -525 mm
// Vorgaenger: SYS1, TRO04
// Nachfolger: TRO05
// Einstufung: mittel
// Separator 1 = Bauteil 0042 aus dem Layout
#TRO03.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO03.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO03.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO03.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO03.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO03.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO03.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO03.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO03.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO03.stInPriorityManager.xReleaseOk := TRUE;
#TRO03(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO03,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 von SYS1 (Station, kein TRO), TRO04
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO05
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO03,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO05 (1Sep) Sep 0044
// Anlagenkoordinate x/y: 10310 / -5013 mm
// Vorgaenger: TRO03
// Nachfolger: TRO18
// Einstufung: mittel
// Separator 1 = Bauteil 0044 aus dem Layout
#TRO05.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO05.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO05.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO05.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO05.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO05.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO05.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO05.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO05.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO05.stInPriorityManager.xReleaseOk := TRUE;
#TRO05(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO05,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO03
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO05,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
// Anlagenkoordinate x/y: 958 / 4700 mm
// Vorgaenger: TRO05, TRO12
// Nachfolger: TRO07, TRO10
// Einstufung: hoch
// Separator 1 = Bauteil 0066 aus dem Layout
#TRO18.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO18.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO18.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO18.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO18.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO18.stInSeparator1.Settings.xPluggedScanner := TRUE;
#TRO18.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO18.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO18.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO18.stInBarcodeReader.Settings.xActivateCheckASCI := TRUE;
#TRO18.stInBarcodeReader.Settings.tTimeout := T#3000ms;
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
// TODO(E-Planung): Linienspeicher mit 19 Linien - Parametrierung je Linie
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
#TRO18(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO18,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 von TRO05, TRO12 (Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator)
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO18,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO07 (1Sep) Sep 0045
// Anlagenkoordinate x/y: 9510 / 14217 mm
// Vorgaenger: TRO09, TRO18
// Nachfolger: TRO04
// Einstufung: mittel
// Separator 1 = Bauteil 0045 aus dem Layout
#TRO07.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO07.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO07.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO07.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO07.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO07.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO07.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO07.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO07.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO07.stInPriorityManager.xReleaseOk := TRUE;
#TRO07(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO07,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 von TRO09, TRO18
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO04
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO07,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO10 (2Sep2Swi) Sep 0005, 0036
// Anlagenkoordinate x/y: 1012 / -4650 mm
// Vorgaenger: TRO01, TRO06, TRO18
// Nachfolger: TRO01, TRO02, TRO11
// Einstufung: hoch
// Separator 1 = Bauteil 0005 aus dem Layout
#TRO10.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO10.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO10.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO10.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO10.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO10.stInSeparator1.Settings.xPluggedScanner := TRUE;
#TRO10.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
// Separator 2 = Bauteil 0036 aus dem Layout
#TRO10.stInSeparator2.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO10.stInSeparator2.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO10.stInSeparator2.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator2.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator2.Settings.tJam := "TimeForSepJam";
#TRO10.stInSeparator2.Settings.xPluggedJam := TRUE;
#TRO10.stInSeparator2.Settings.xPluggedPart := FALSE;
#TRO10.stInSeparator2.Settings.xPluggedScanner := TRUE;
#TRO10.stInSeparator2.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO10.stInSeparator2.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO10.stInSeparator2.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO10.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO10.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO10.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO10.stInSwitch1.Settings.nTo1Destinations := _; // TODO(E-Planung): dieser TRO hat 3 Abgaenge auf 2 Weichen - Aufteilung festlegen
#TRO10.stInSwitch2.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO10.stInSwitch2.Settings.xPluggedExSen3 := FALSE;
#TRO10.stInSwitch2.Settings.xPluggedExSen4 := FALSE;
#TRO10.stInSwitch2.Settings.nTo1Destinations := _; // TODO(E-Planung): dieser TRO hat 3 Abgaenge auf 2 Weichen - Aufteilung festlegen
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO01: TRO01
// ueber TRO02: SYS2, SYS3, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
// ueber TRO11: TRO03, TRO04, TRO05, TRO07, TRO11, TRO12, TRO18
#TRO10.stInBarcodeReader1.Settings.xActivateCheckASCI := TRUE;
#TRO10.stInBarcodeReader1.Settings.tTimeout := T#3000ms;
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
#TRO10.stInBarcodeReader2.Settings.xActivateCheckASCI := TRUE;
#TRO10.stInBarcodeReader2.Settings.tTimeout := T#3000ms;
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
#TRO10.stInPriorityManager.xReleaseOk := TRUE;
#TRO10(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO10,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 von TRO01, TRO06, TRO18 (Aufteilung der Vorgaenger auf die Eingaenge offen)
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von - (Aufteilung der Vorgaenger auf die Eingaenge offen)
stInOutJamEntr2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 vor TRO01
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO02
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0058 vor TRO11
stInOutJamExit4 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutStopper2 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
xOutSw2ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO10,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO10.nInDirection1,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO10.nInDirection2,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO04 (1Sep) Sep 0043
// Anlagenkoordinate x/y: 10310 / 160 mm
// Vorgaenger: TRO07
// Nachfolger: TRO03
// Einstufung: mittel
// Separator 1 = Bauteil 0043 aus dem Layout
#TRO04.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO04.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO04.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO04.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO04.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO04.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO04.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO04.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO04.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO04.stInPriorityManager.xReleaseOk := TRUE;
#TRO04(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO04,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO07
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 vor TRO03
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO04,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO01 (1Sep) Sep 0040
// Anlagenkoordinate x/y: 5355 / -5878 mm
// Vorgaenger: TRO10
// Nachfolger: TRO10
// Einstufung: mittel
// Separator 1 = Bauteil 0040 aus dem Layout
#TRO01.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO01.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO01.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO01.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO01.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO01.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO01.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO01.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO01.stInPriorityManager.xReleaseOk := TRUE;
#TRO01(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO01,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO10
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO01,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO02 (1Sep) Sep 0012
// Anlagenkoordinate x/y: -220 / -3472 mm
// Vorgaenger: TRO10
// Nachfolger: SYS2, SYS3, TRO13
// Einstufung: mittel
// Separator 1 = Bauteil 0012 aus dem Layout
#TRO02.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO02.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO02.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO02.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO02.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO02.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO02.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO02.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO02.stInPriorityManager.xReleaseOk := TRUE;
#TRO02(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO02,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO10
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS2 (Station, kein TRO)
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO)
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 vor TRO13
stInOutJamExit4 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO02,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO11 (Vario) Sep 0058
// Anlagenkoordinate x/y: 3300 / -4743 mm
// Vorgaenger: TRO10
// Nachfolger: TRO12
// Einstufung: hoch
// Separator 1 = Bauteil 0058 aus dem Layout
#TRO11.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO11.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO11.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO11.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO11.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO11.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO11.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO11.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO11.stInVario.Settings.xContinousEmptying := FALSE;
#TRO11.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO11.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO11.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO11.stInVario.Settings.tWaitForStop := t#3s;
#TRO11.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO11.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO11.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO11.stInVario.Settings.tJamSenDelay := T#2s;
#TRO11.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO11.stInPriorityManager.xReleaseOk := TRUE;
#TRO11(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO11,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0058 von TRO10
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 vor TRO12
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO11,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO13 (1Sep1Swi) Sep 0011
// Anlagenkoordinate x/y: -220 / -481 mm
// Vorgaenger: TRO02
// Nachfolger: TRO16, TRO17
// Einstufung: mittel
// Separator 1 = Bauteil 0011 aus dem Layout
#TRO13.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO13.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO13.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO13.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO13.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO13.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO13.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO13.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO13.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO13.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO13.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO13.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO13.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO16: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO14, TRO15, TRO16, TRO17, TRO18
// ueber TRO17: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO17, TRO18
#TRO13.stInPriorityManager.xReleaseOk := TRUE;
#TRO13(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO13,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 von TRO02
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 vor TRO16
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO13,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO13.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO12 (1Sep) Sep 0059
// Anlagenkoordinate x/y: 9412 / -4150 mm
// Vorgaenger: TRO11
// Nachfolger: TRO18
// Einstufung: mittel
// Separator 1 = Bauteil 0059 aus dem Layout
#TRO12.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO12.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO12.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO12.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO12.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO12.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO12.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO12.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO12.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO12.stInPriorityManager.xReleaseOk := TRUE;
#TRO12(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO12,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 von TRO11
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO12,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO16 (Vario) Sep 0062
// Anlagenkoordinate x/y: -899 / 1135 mm
// Vorgaenger: TRO13
// Nachfolger: TRO15
// Einstufung: hoch
// Separator 1 = Bauteil 0062 aus dem Layout
#TRO16.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO16.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO16.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO16.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO16.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO16.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO16.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO16.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO16.stInVario.Settings.xContinousEmptying := FALSE;
#TRO16.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO16.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO16.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO16.stInVario.Settings.tWaitForStop := t#3s;
#TRO16.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO16.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO16.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO16.stInVario.Settings.tJamSenDelay := T#2s;
#TRO16.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO16.stInPriorityManager.xReleaseOk := TRUE;
#TRO16(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO16,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 von TRO13
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 vor TRO15
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO16,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO17 (1Sep) Sep 0063
// Anlagenkoordinate x/y: -703 / 12296 mm
// Vorgaenger: TRO13, TRO14
// Nachfolger: TRO06
// Einstufung: mittel
// Separator 1 = Bauteil 0063 aus dem Layout
#TRO17.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO17.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO17.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO17.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO17.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO17.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO17.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO17.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO17.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO17.stInPriorityManager.xReleaseOk := TRUE;
#TRO17(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO17,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 von TRO13, TRO14
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 vor TRO06
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO17,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO15 (1Sep) Sep 0038
// Anlagenkoordinate x/y: -1206 / 5330 mm
// Vorgaenger: TRO16
// Nachfolger: TRO14
// Einstufung: mittel
// Separator 1 = Bauteil 0038 aus dem Layout
#TRO15.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO15.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO15.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO15.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO15.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO15.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO15.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO15.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO15.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO15.stInPriorityManager.xReleaseOk := TRUE;
#TRO15(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO15,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 von TRO16
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 vor TRO14
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO15,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO06 (1Sep1Swi) Sep 0003
// Anlagenkoordinate x/y: -220 / 13960 mm
// Vorgaenger: TRO17
// Nachfolger: TRO08, TRO10
// Einstufung: mittel
// Separator 1 = Bauteil 0003 aus dem Layout
#TRO06.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO06.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO06.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO06.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO06.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO06.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO06.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO06.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO06.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO06.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
#TRO06.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
#TRO06.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
#TRO06.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
// ueber TRO08: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
// ueber TRO10: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
#TRO06.stInPriorityManager.xReleaseOk := TRUE;
#TRO06(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO06,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 von TRO17
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 vor TRO08
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO06,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
REGION Direction
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
nOutDirection => #TRO06.nInDirection,
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
END_REGION
END_REGION
REGION TRO14 (1Sep) Sep 0037
// Anlagenkoordinate x/y: -1026 / 10723 mm
// Vorgaenger: TRO15
// Nachfolger: TRO17
// Einstufung: mittel
// Separator 1 = Bauteil 0037 aus dem Layout
#TRO14.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO14.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO14.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO14.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO14.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO14.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO14.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO14.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO14.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO14.stInPriorityManager.xReleaseOk := TRUE;
#TRO14(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO14,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 von TRO15
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO14,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO08 (Vario) Sep 0054
// Anlagenkoordinate x/y: 3300 / 13510 mm
// Vorgaenger: TRO06
// Nachfolger: TRO09
// Einstufung: hoch
// Separator 1 = Bauteil 0054 aus dem Layout
#TRO08.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO08.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO08.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO08.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO08.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO08.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO08.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO08.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO08.stInVario.Settings.xContinousEmptying := FALSE;
#TRO08.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
#TRO08.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
#TRO08.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
#TRO08.stInVario.Settings.tWaitForStop := t#3s;
#TRO08.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
#TRO08.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
#TRO08.stInVario.Settings.xJamSenDelayActive := TRUE;
#TRO08.stInVario.Settings.tJamSenDelay := T#2s;
#TRO08.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO08.stInPriorityManager.xReleaseOk := TRUE;
#TRO08(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO08,
xInMotorProtection := "FC____", // TODO(E-Planung)
xInAutomatic := #t_automatik,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 von TRO06
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 vor TRO09
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
xOutVarioMotor => "MA____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO08,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
REGION TRO09 (1Sep) Sep 0055
// Anlagenkoordinate x/y: 8953 / 13510 mm
// Vorgaenger: TRO08
// Nachfolger: TRO07
// Einstufung: mittel
// Separator 1 = Bauteil 0055 aus dem Layout
#TRO09.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
#TRO09.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
#TRO09.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tJam := "TimeForSepJam";
#TRO09.stInSeparator1.Settings.xPluggedJam := TRUE;
#TRO09.stInSeparator1.Settings.xPluggedPart := FALSE;
#TRO09.stInSeparator1.Settings.xPluggedScanner := FALSE;
#TRO09.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
#TRO09.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
#TRO09.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
#TRO09.stInPriorityManager.xReleaseOk := TRUE;
#TRO09(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
xInAllRdyToStart := TRUE,
xInRelease := "DB_TRO_TEST".TRO09,
stInOutMachineState := #stInOutControlUnitCabinet,
stInOutCognexInterface := #stCognexInterfaceDummy,
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 von TRO08
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
xOutStopper1 => "MB____", // TODO(E-Planung)
stInOutHMI := "DB_Interface_HMI".stTRO.TRO09,
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
END_REGION
END_FUNCTION_BLOCK
File diff suppressed because it is too large. Load diff
+907
View File
@@ -0,0 +1,907 @@
{
"schema": "sps_skel-tro-v3",
"source": "mubea.csv",
"created": "2026-10-09 11:38",
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
"tros": [
{
"id": "TRO01",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0040"
],
"predecessors": [
"TRO10"
],
"successors": [
"TRO10"
],
"position": {
"x": 5355.05,
"y": -5878.0,
"z": 1555.5
},
"label": "Separator 0040 auf Kreisel1 Bahn R",
"hosts": [
"0001-R"
],
"scanners": [],
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO02",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0012"
],
"predecessors": [
"TRO10"
],
"successors": [
"SYS2",
"SYS3",
"TRO13"
],
"position": {
"x": -220.0,
"y": -3472.47,
"z": 1554.75
},
"label": "Separator 0012 auf Kreisel2 Bahn L",
"hosts": [
"0002-L"
],
"scanners": [
"0007"
],
"reason": "Separator mit Scanner auf angetriebener Kreisel-Bahn",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO03",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0042"
],
"predecessors": [
"SYS1",
"TRO04"
],
"successors": [
"TRO05"
],
"position": {
"x": 10490.0,
"y": -525.02,
"z": 2028.75
},
"label": "Separator 0042 auf Kreisel3 Bahn R",
"hosts": [
"0010-R"
],
"scanners": [
"0008"
],
"reason": "Separator mit Scanner auf angetriebener Kreisel-Bahn",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO04",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0043"
],
"predecessors": [
"TRO07"
],
"successors": [
"TRO03"
],
"position": {
"x": 10310.0,
"y": 159.95,
"z": 2029.5
},
"label": "Separator 0043 auf Kreisel3 Bahn R",
"hosts": [
"0010-R"
],
"scanners": [],
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
"colocated": [
"TRO05"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO05",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0044"
],
"predecessors": [
"TRO03"
],
"successors": [
"TRO18"
],
"position": {
"x": 10310.0,
"y": -5013.05,
"z": 2029.5
},
"label": "Separator 0044 auf Kreisel3 Bahn R",
"hosts": [
"0010-R"
],
"scanners": [],
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
"colocated": [
"TRO04"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO06",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0003"
],
"predecessors": [
"TRO17"
],
"successors": [
"TRO08",
"TRO10"
],
"position": {
"x": -220.0,
"y": 13960.02,
"z": 1554.75
},
"label": "Separator 0003 an Ausschleuselement 0053 (Strecke 0013, Anfang)",
"hosts": [
"0013"
],
"scanners": [
"0004"
],
"reason": "899mm von Ausschleuselement 0053 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO07",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0045"
],
"predecessors": [
"TRO09",
"TRO18"
],
"successors": [
"TRO04"
],
"position": {
"x": 9510.0,
"y": 14217.05,
"z": 2029.5
},
"label": "VF_1",
"hosts": [
"0013"
],
"scanners": [],
"reason": "Separator 0045: Zuordnung zeigt auf VF_1, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
"colocated": [
"TRO08"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO08",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0054"
],
"predecessors": [
"TRO06"
],
"successors": [
"TRO09"
],
"position": {
"x": 3299.55,
"y": 13509.62,
"z": 1416.12
},
"label": "VF_1",
"hosts": [
"0013"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
"colocated": [
"TRO07"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO09",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0055"
],
"predecessors": [
"TRO08"
],
"successors": [
"TRO07"
],
"position": {
"x": 8953.1,
"y": 13509.62,
"z": 2179.73
},
"label": "VF_1",
"hosts": [
"0013"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO10",
"type": "2Sep2Swi",
"confidence": "hoch",
"separators": [
"0005",
"0036"
],
"predecessors": [
"TRO01",
"TRO06",
"TRO18"
],
"successors": [
"TRO01",
"TRO02",
"TRO11"
],
"position": {
"x": 1012.49,
"y": -4649.76,
"z": 1554.75
},
"label": "Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125",
"hosts": [
"0014"
],
"scanners": [
"0006",
"0041"
],
"reason": "Ausschleuselement 0057 (Separator 0005) und ILS Weiche 0125 (Separator 0036) liegen in der TrackIds-Kette von Kreisel2 direkt hintereinander - eine Uebergabestelle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 4,
"fbType": "FB_ILS_MTRO_2Sep2Swi",
"items": {
"Separator": 2,
"Switch": 2,
"Scanner": 2
}
},
{
"id": "TRO11",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0058"
],
"predecessors": [
"TRO10"
],
"successors": [
"TRO12"
],
"position": {
"x": 3299.55,
"y": -4743.38,
"z": 1416.12
},
"label": "VF_2",
"hosts": [
"0014"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO12",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0059"
],
"predecessors": [
"TRO11"
],
"successors": [
"TRO18"
],
"position": {
"x": 9411.55,
"y": -4150.19,
"z": 2103.86
},
"label": "VF_2",
"hosts": [
"0014"
],
"scanners": [
"0046"
],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_2 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO13",
"type": "1Sep1Swi",
"confidence": "mittel",
"separators": [
"0011"
],
"predecessors": [
"TRO02"
],
"successors": [
"TRO16",
"TRO17"
],
"position": {
"x": -220.0,
"y": -480.98,
"z": 1554.75
},
"label": "Separator 0011 an Ausschleuselement 0061 (Strecke 0015, Anfang)",
"hosts": [
"0015"
],
"scanners": [
"0009"
],
"reason": "832mm von Ausschleuselement 0061 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep1Swi",
"items": {
"Separator": 1,
"Switch": 1
}
},
{
"id": "TRO14",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0037"
],
"predecessors": [
"TRO15"
],
"successors": [
"TRO17"
],
"position": {
"x": -1026.0,
"y": 10723.05,
"z": 1634.5
},
"label": "VF_3",
"hosts": [
"0015"
],
"scanners": [],
"reason": "Separator 0037: Zuordnung zeigt auf VF_3, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
"colocated": [
"TRO16"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO15",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0038"
],
"predecessors": [
"TRO16"
],
"successors": [
"TRO14"
],
"position": {
"x": -1206.0,
"y": 5330.02,
"z": 1477.75
},
"label": "VF_3",
"hosts": [
"0015"
],
"scanners": [
"0039"
],
"reason": "Separator 0038: Zuordnung zeigt auf VF_3, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
"colocated": [
"TRO17"
],
"positionFrom": "Bauteile",
"positionPoints": 2,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO16",
"type": "Vario",
"confidence": "hoch",
"separators": [
"0062"
],
"predecessors": [
"TRO13"
],
"successors": [
"TRO15"
],
"position": {
"x": -898.87,
"y": 1134.6,
"z": 1534.33
},
"label": "VF_3",
"hosts": [
"0015"
],
"scanners": [],
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
"colocated": [
"TRO14",
"TRO17"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_Vario",
"items": {
"Separator": 1,
"VarioDrive": 1
}
},
{
"id": "TRO17",
"type": "1Sep",
"confidence": "mittel",
"separators": [
"0063"
],
"predecessors": [
"TRO13",
"TRO14"
],
"successors": [
"TRO06"
],
"position": {
"x": -702.98,
"y": 12296.45,
"z": 1751.35
},
"label": "VF_3",
"hosts": [
"0015"
],
"scanners": [],
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_3 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
"colocated": [
"TRO15",
"TRO16"
],
"positionFrom": "Bauteile",
"positionPoints": 1,
"fbType": "FB_ILS_MTRO_1Sep",
"items": {
"Separator": 1
}
},
{
"id": "TRO18",
"type": "PinStore_Auto",
"confidence": "hoch",
"separators": [
"0066",
"0069",
"0072",
"0075",
"0078",
"0081",
"0084",
"0087",
"0090",
"0093",
"0096",
"0099",
"0102",
"0105",
"0108",
"0111",
"0114",
"0117",
"0120",
"0123"
],
"predecessors": [
"TRO05",
"TRO12"
],
"successors": [
"TRO07",
"TRO10"
],
"position": {
"x": 958.0,
"y": 4700.0,
"z": 1697.6
},
"label": "Speicher (20 Schwerkraftlinien)",
"hosts": [
"0016",
"0017",
"0018",
"0019",
"0020",
"0021",
"0022",
"0023",
"0024",
"0025",
"0026",
"0027",
"0028",
"0029",
"0030",
"0031",
"0032",
"0033",
"0034",
"0035"
],
"scanners": [],
"reason": "20 Gefaellestrecken mit je eigenem Separator, gemeinsamer Zu-/Ablauf - Muster eines Linienspeichers",
"colocated": [],
"positionFrom": "Bauteile",
"positionPoints": 20,
"fbType": "FB_ILS_MTRO_PinStore_Auto",
"items": {
"Separator": 1,
"StorageLine": 19,
"Scanner": 1,
"Encoder": 1
}
}
],
"boundaries": [
{
"id": "SYS1",
"type": "Materialeingang",
"confidence": "hoch",
"separators": [],
"predecessors": [],
"successors": [
"TRO03"
],
"position": {
"x": 10410.95,
"y": 3.52,
"z": 1983.31
},
"label": "BTMT Beladung :1",
"hosts": [
"0047"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
},
{
"id": "SYS2",
"type": "Materialausgang",
"confidence": "hoch",
"separators": [],
"predecessors": [
"TRO02"
],
"successors": [],
"position": {
"x": -778.79,
"y": -1226.1,
"z": 1401.09
},
"label": "BTMT Entladung :2",
"hosts": [
"0048"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
},
{
"id": "SYS3",
"type": "Materialausgang",
"confidence": "hoch",
"separators": [],
"predecessors": [
"TRO02"
],
"successors": [],
"position": {
"x": -775.22,
"y": -2466.46,
"z": 1401.09
},
"label": "BTMT Entladung :1",
"hosts": [
"0049"
],
"scanners": [],
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
"colocated": [],
"positionFrom": "Host-Objekte",
"positionPoints": 1,
"fbType": "",
"items": {}
}
],
"connections": [
{
"from": "SYS1",
"to": "TRO03",
"kind": "normal"
},
{
"from": "TRO01",
"to": "TRO10",
"kind": "normal"
},
{
"from": "TRO02",
"to": "SYS2",
"kind": "normal"
},
{
"from": "TRO02",
"to": "SYS3",
"kind": "normal"
},
{
"from": "TRO02",
"to": "TRO13",
"kind": "transfer"
},
{
"from": "TRO03",
"to": "TRO05",
"kind": "bahn"
},
{
"from": "TRO04",
"to": "TRO03",
"kind": "bahn"
},
{
"from": "TRO05",
"to": "TRO18",
"kind": "normal"
},
{
"from": "TRO06",
"to": "TRO08",
"kind": "weiche"
},
{
"from": "TRO06",
"to": "TRO10",
"kind": "bahn"
},
{
"from": "TRO07",
"to": "TRO04",
"kind": "bahn"
},
{
"from": "TRO08",
"to": "TRO09",
"kind": "bahn"
},
{
"from": "TRO09",
"to": "TRO07",
"kind": "bahn"
},
{
"from": "TRO10",
"to": "TRO01",
"kind": "bahn"
},
{
"from": "TRO10",
"to": "TRO02",
"kind": "bahn"
},
{
"from": "TRO10",
"to": "TRO11",
"kind": "weiche"
},
{
"from": "TRO11",
"to": "TRO12",
"kind": "bahn"
},
{
"from": "TRO12",
"to": "TRO18",
"kind": "normal"
},
{
"from": "TRO13",
"to": "TRO16",
"kind": "weiche"
},
{
"from": "TRO13",
"to": "TRO17",
"kind": "bahn"
},
{
"from": "TRO14",
"to": "TRO17",
"kind": "bahn"
},
{
"from": "TRO15",
"to": "TRO14",
"kind": "bahn"
},
{
"from": "TRO16",
"to": "TRO15",
"kind": "bahn"
},
{
"from": "TRO17",
"to": "TRO06",
"kind": "normal"
},
{
"from": "TRO18",
"to": "TRO07",
"kind": "normal"
},
{
"from": "TRO18",
"to": "TRO10",
"kind": "normal"
}
],
"candidates": [],
"findings": [
"Kreisel2: Ausschleuselement 0057 und ILS Weiche 0125 benachbart - als 2Sep2Swi zusammengefasst (FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib)",
"Scanner 0008 'Scanner :5': montiert an 0010, liest aber an Separator 0042 auf 0010-R",
"Scanner 0046 'Scanner :1': montiert an 0035, liest aber an Separator 0059 auf 0014",
"TRO02 (Separator 0012 auf Kreisel2 Bahn L): Scanner 0007 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
"TRO03 (Separator 0042 auf Kreisel3 Bahn R): Scanner 0008 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
"TRO12 (VF_2): Scanner 0046 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
"TRO15 (VF_3): Scanner 0039 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
"VF_1 Separator 0045: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
"VF_1 Separator 0055: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_2 Separator 0059: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
"VF_3 Separator 0037: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
"VF_3 Separator 0038: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
"VF_3 Separator 0063: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft"
]
}
+30
View File
@@ -0,0 +1,30 @@
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
1;"ILS 2.0 Kreisel";"0001";"Kreisel1";"A/7-B/8";1;"-2011.74, 994.84, 2000.00";"3100.00, 800.00, 1.00";"yab0Dy4YHoSAAAAAAAAAAAAA";"";"";"";"";"0009-L";"0023, 0021";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel1", "Geruest fuer Einzelmodul": true}
2;"ILS 2.0 Kreisel";"0002";"Kreisel2";"E/3-E/6";1;"4938.26, -4953.25, 2000.00";"800.00, 5300.00, 1.00";"S1oUi/vOHoSAAAAAAAAAWoJ4";"";"";"";"";"0007-L, 0007-R, 0003-L";"0015, 0029, 0014, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.5", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel2", "Geruest fuer Einzelmodul": true}
3;"ILS 2.0 Kreisel";"0003";"Kreisel3";"E/1-E/4";1;"5738.26, -9850.80, 2000.00";"800.00, 6095.71, 1.00";"V48UgAABHoSAAAAAAAAAWoJ4";"";"";"";"";"0004-L, 0002-R";"0029, 0028";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "5.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel3", "Geruest fuer Einzelmodul": true}
4;"ILS 2.0 Kreisel";"0004";"Kreisel4";"E/3-F/7";1;"6538.26, -3258.73, 2000.00";"800.00, 8689.42, 1.00";"Y8QUi/sQHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-R, 0003-R";"0016, 0028, 0017, 0027";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "7.89", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel4", "Geruest fuer Einzelmodul": true}
5;"ILS 2.0 Kreisel";"0005";"Kreisel5";"E/7-K/8";1;"11430.05, 1485.99, 2000.00";"10583.58, 800.00, 1.00";"XamUFqymHoSAAAAAAAAAAAAA";"";"";"";"";"0006-L, 0006-R, 0004-L, 0004-R";"0018, 0026, 0027, 0020";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "9.78", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 0, "Name": "Kreisel5", "Geruest fuer Einzelmodul": true}
6;"ILS 2.0 Kreisel";"0006";"Kreisel6";"E/8-F/10";1;"6538.26, 4145.31, 2000.00";"800.00, 4518.63, 1.00";"Y8QUHMcmHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-L";"0019, 0026";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.72", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel6", "Geruest fuer Einzelmodul": true}
7;"ILS 2.0 Kreisel";"0007";"Kreisel7";"E/6-E/8";1;"4938.26, -453.25, 2000.00";"800.00, 3700.00, 1.00";"S1oU3NruHoSAAAAAAAAAWoJ4";"";"";"";"";"0008-L, 0008-R, 0002-L, 0002-R";"0024, 0013, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.9", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel7", "Geruest fuer Einzelmodul": true}
8;"ILS 2.0 Kreisel";"0008";"Kreisel8";"B/7-E/7";1;"2438.26, 505.79, 2000.00";"4200.00, 800.00, 1.00";"BSlUB7e+HoSAAAAAAAAAAAAA";"";"";"";"";"0009-R, 0007-L";"0024, 0011, 0022, 0012";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.4", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel8", "Geruest fuer Einzelmodul": true}
9;"ILS 2.0 Kreisel";"0009";"Kreisel9";"B/6-B/9";1;"-61.74, 1425.92, 2000.00";"800.00, 4800.00, 1.00";"/w7U8SMAHoSAAAAAAAAAWoJ4";"";"";"";"";"0001-L, 0001-R, 0008-L, 0008-R";"0010, 0023, 0022";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel9", "Geruest fuer Einzelmodul": true}
10;"ILS 2.0 Separator";"0010";"Separator :10";"B/8";1;"-461.74, 1526.13, 2001.50";"150.00, 209.90, 3.00";"+PRUFl9EHoSAAAAAAAAAWoJ4";"/+33ADlFAE4g";"";"";"";"0022, 0023";"";"";"";{"Zuordnung": "0009-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
11;"ILS 2.0 Separator";"0011";"Separator :9";"C/7";1;"1241.80, 105.79, 1.50";"209.90, 150.00, 3.00";"EgiaAZ0+AAAAAAAAAAAAAAAA";"AC4qAAQhAAAA";"";"";"";"0024, 0022";"";"";"";{"Zuordnung": "0008-R", "ARTINR": "200000077", "Höhe": "0", "Drehung": 0, "Typ": ""}
12;"ILS 2.0 Separator";"0012";"Separator :8";"D/7";1;"3696.84, 905.79, 2001.50";"209.90, 150.00, 3.00";"OVL2DdI+HoSAAAAAAAAAf///";"AJK/ACNhAE4g";"";"";"";"0022, 0024";"";"";"";{"Zuordnung": "0008-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 180, "Typ": ""}
13;"ILS 2.0 Separator";"0013";"Separator :7";"E/6";1;"4538.26, -1493.47, 2001.50";"150.00, 209.90, 3.00";"RT+U6Ev0HoSAAAAAAAAAWoJ4";"ALFG/8NSAE4g";"";"";"";"0024, 0025";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
14;"ILS 2.0 Separator";"0014";"Separator :6";"E/5";1;"4538.26, -3169.65, 2001.50";"150.00, 209.90, 3.00";"RT+U0Iy8HoSAAAAAAAAApX2I";"ALFG/4aHAE4g";"";"";"";"0029, 0025";"";"";"";{"Zuordnung": "0002-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
15;"ILS 2.0 Separator";"0015";"Separator :5";"E/4";1;"5338.26, -6534.21, 2001.50";"150.00, 209.90, 3.00";"UXSUm2GQHoSAAAAAAAAAWoJ4";"ANCG/v5rAE4g";"";"";"";"0025, 0029";"";"";"";{"Zuordnung": "0002-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
16;"ILS 2.0 Separator";"0016";"Separator :4";"F/4";1;"6938.26, -6618.23, 2001.50";"150.00, 209.90, 3.00";"ad6UmhlcHoSAAAAAAAAAWoJ4";"AQ8G/vsjAE4g";"";"";"";"0027, 0028";"";"";"";{"Zuordnung": "0004-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
17;"ILS 2.0 Separator";"0017";"Separator :3";"E/7";1;"6138.26, 302.58, 2001.50";"150.00, 209.90, 3.00";"XamUBYgiHoSAAAAAAAAApX2I";"AO/GAA4pAE4g";"";"";"";"0028, 0027";"";"";"";{"Zuordnung": "0004-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
18;"ILS 2.0 Separator";"0018";"Separator :2";"F/8";1;"7153.84, 1885.99, 2001.50";"209.90, 150.00, 3.00";"bD6CHMcmHoSAAAAAAAAAAAAA";"ARUaAEmrAE4g";"";"";"";"0020, 0026";"";"";"";{"Zuordnung": "0005-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 0, "Typ": ""}
19;"ILS 2.0 Separator";"0019";"Separator :1";"F/8";1;"6938.26, 2908.49, 2001.50";"150.00, 209.90, 3.00";"ad6UK3ccHoSAAAAAAAAAWoJ4";"AQ8GAG9FAE4g";"";"";"";"0026, 0026";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
20;"ILS 2.0 BTMT Beladung";"0020";"BTMT Beladung :1";"I/7-J/8";1;"14378.91, 1186.94, 1955.31";"645.71, 397.50, 316.02";"f///EJImHoSAAAAAAAAAAAAA";"AiYwADU7AE6r";"Aj4eACprAE4g";"";"";"0027, 0018";"";"";"";{"ARTINR": "829662012", "Höhe": "2000", "Drehung": 0}
21;"ILS 2.0 BTMT Entladung";"0021";"BTMT Entladung :1";"A/7";1;"-2501.71, 212.89, 1847.09";"1468.14, 950.36, 467.01";"3bjiCN/+HoSAAAAAAAAAf/+r";"/7UPABabAE4f";"/4Hu//aMAD/1";"";"";"0023, 0023";"";"";"";{"ARTINR": "400100156", "Höhe": "2000", "Drehung": 179}
22;"ILS Weiche";"0022";"ILS Weiche :1";"B/7";1;"338.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0011, 0012";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0008", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
23;"ILS Weiche";"0023";"ILS Weiche :2";"B/7";1;"-461.74, 994.84, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0021, 0021";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0001", "Achse von Kreisel": "0001", "Beruehrung": "Kreis/Tangente"}
24;"ILS Weiche";"0024";"ILS Weiche :3";"E/7";1;"4538.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0025, 0013";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0008", "Kreisel B": "0007", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
25;"ILS Weiche";"0025";"ILS Weiche :4";"E/6";1;"4938.26, -2303.25, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0014, 0015";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0007", "Kreisel B": "0002", "Achse von Kreisel": "0007", "Beruehrung": "Kreis/Kreis"}
26;"ILS Weiche";"0026";"ILS Weiche :5";"F/8";1;"6538.26, 1885.99, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0018, 0027";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0006", "Kreisel B": "0005", "Achse von Kreisel": "0006", "Beruehrung": "Kreis/Kreis"}
27;"ILS Weiche";"0027";"ILS Weiche :6";"F/7";1;"6538.26, 1085.78, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0017, 0016";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0005", "Kreisel B": "0004", "Achse von Kreisel": "0005", "Beruehrung": "Kreis/Kreis"}
28;"ILS Weiche";"0028";"ILS Weiche :7";"E/3";1;"6138.26, -7203.05, 2000.00";"100.25, 250.10, 30.00";"";"";"";"";"";"0029, 0029";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0004", "Kreisel B": "0003", "Achse von Kreisel": "0004", "Beruehrung": "Kreis/Kreis"}
29;"ILS Weiche";"0029";"ILS Weiche :8";"E/3";1;"5338.26, -7202.95, 2000.00";"100.19, 250.07, 30.00";"";"";"";"";"";"0015, 0014";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0003", "Kreisel B": "0002", "Achse von Kreisel": "0003", "Beruehrung": "Kreis/Kreis"}
Can't render this file because it contains an unexpected character in line 2 and column 171.
+31
View File
@@ -0,0 +1,31 @@
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
1;"ILS 2.0 Kreisel";"0001";"Kreisel37";"A/7-B/8";1;"-2011.74, 994.84, 2000.00";"3100.00, 800.00, 1.00";"yab0Dy4YHoSAAAAAAAAAAAAA";"";"";"";"";"0009-L";"0024, 0021";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel37", "Geruest fuer Einzelmodul": true}
2;"ILS 2.0 Kreisel";"0002";"Kreisel38";"E/3-E/6";1;"4940.03, -4953.25, 2000.00";"800.00, 5300.00, 1.00";"S2D+i/vOHoSAAAAAAAAAWoJ4";"";"";"";"";"0007-L, 0007-R, 0003-L";"0026, 0015, 0030, 0014";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.5", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel38", "Geruest fuer Einzelmodul": true}
3;"ILS 2.0 Kreisel";"0003";"Kreisel39";"E/1-E/4";1;"5739.13, -9850.80, 2000.00";"800.00, 6095.71, 1.00";"V5J6gAABHoSAAAAAAAAAWoJ4";"";"";"";"";"0004-L, 0002-R";"0030, 0029";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "5.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel39", "Geruest fuer Einzelmodul": true}
4;"ILS 2.0 Kreisel";"0004";"Kreisel40";"E/3-F/7";1;"6533.60, -3258.73, 2000.00";"800.00, 8689.42, 1.00";"Y7Hgi/sQHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-R, 0003-R";"0016, 0029, 0017, 0028";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "7.89", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel40", "Geruest fuer Einzelmodul": true}
5;"ILS 2.0 Kreisel";"0005";"Kreisel41";"E/7-K/8";1;"11425.39, 1485.99, 2000.00";"10583.58, 800.00, 1.00";"XZdgFqymHoSAAAAAAAAAAAAA";"";"";"";"";"0006-L, 0006-R, 0004-L, 0004-R";"0018, 0027, 0028, 0020";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "9.78", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 0, "Name": "Kreisel41", "Geruest fuer Einzelmodul": true}
6;"ILS 2.0 Kreisel";"0006";"Kreisel42";"E/8-F/10";1;"6533.60, 4145.31, 2000.00";"800.00, 4518.63, 1.00";"Y7HgHMcmHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-L";"0019, 0027";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.72", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel42", "Geruest fuer Einzelmodul": true}
7;"ILS 2.0 Kreisel";"0007";"Kreisel43";"E/6-E/8";1;"4940.59, 96.75, 2000.00";"800.00, 4800.00, 1.00";"S2Mu3NruHoSAAAAAAAAAWoJ4";"";"";"";"";"0008-L, 0008-R, 0002-L, 0002-R";"0022, 0025, 0013, 0026";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel43", "Geruest fuer Einzelmodul": true}
8;"ILS 2.0 Kreisel";"0008";"Kreisel44";"B/7-E/7";1;"2440.85, 505.79, 2000.00";"4200.00, 800.00, 1.00";"BTNyB7e+HoSAAAAAAAAAAAAA";"";"";"";"";"0009-R, 0007-L";"0025, 0011, 0023, 0012";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.4", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel44", "Geruest fuer Einzelmodul": true}
9;"ILS 2.0 Kreisel";"0009";"Kreisel45";"B/6-B/9";1;"-61.76, 1425.92, 2000.00";"800.00, 4800.00, 1.00";"/w7A8SMAHoSAAAAAAAAAWoJ4";"";"";"";"";"0001-L, 0001-R, 0008-L, 0008-R";"0010, 0024, 0023";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel45", "Geruest fuer Einzelmodul": true}
10;"ILS 2.0 Separator";"0010";"Separator :11";"B/8";1;"-461.76, 1526.13, 2001.50";"150.00, 209.90, 3.00";"+PRAFl9EHoSAAAAAAAAAWoJ4";"/+33ADlFAE4g";"";"";"";"0023, 0024";"";"";"";{"Zuordnung": "0009-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
11;"ILS 2.0 Separator";"0011";"Separator :10";"C/7";1;"1244.39, 105.79, 1.50";"209.90, 150.00, 3.00";"EhK4AZ0+AAAAAAAAAAAAAAAA";"AC5EAAQhAAAA";"";"";"";"0025, 0023";"";"";"";{"Zuordnung": "0008-R", "ARTINR": "200000077", "Höhe": "0", "Drehung": 0, "Typ": ""}
12;"ILS 2.0 Separator";"0012";"Separator :9";"D/7";1;"3699.43, 905.79, 2001.50";"209.90, 150.00, 3.00";"OV0UDdI+HoSAAAAAAAAAf///";"AJLZACNhAE4g";"";"";"";"0023, 0025";"";"";"";{"Zuordnung": "0008-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 180, "Typ": ""}
13;"ILS 2.0 Separator";"0013";"Separator :8";"E/6";1;"4540.59, -1493.47, 2001.50";"150.00, 209.90, 3.00";"RUiu6Ev0HoSAAAAAAAAAWoJ4";"ALFd/8NSAE4g";"";"";"";"0025, 0026";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
14;"ILS 2.0 Separator";"0014";"Separator :7";"E/5";1;"4540.03, -3169.65, 2001.50";"150.00, 209.90, 3.00";"RUZ+0Iy8HoSAAAAAAAAApX2I";"ALFY/4aHAE4g";"";"";"";"0030, 0026";"";"";"";{"Zuordnung": "0002-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
15;"ILS 2.0 Separator";"0015";"Separator :6";"E/4";1;"5340.03, -6534.21, 2001.50";"150.00, 209.90, 3.00";"UXt+m2GQHoSAAAAAAAAAWoJ4";"ANCY/v5rAE4g";"";"";"";"0026, 0030";"";"";"";{"Zuordnung": "0002-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
16;"ILS 2.0 Separator";"0016";"Separator :5";"F/4";1;"6933.60, -6618.23, 2001.50";"150.00, 209.90, 3.00";"acxgmhlcHoSAAAAAAAAAWoJ4";"AQ7Y/vsjAE4g";"";"";"";"0028, 0029";"";"";"";{"Zuordnung": "0004-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
17;"ILS 2.0 Separator";"0017";"Separator :4";"E/7";1;"6133.60, 302.58, 2001.50";"150.00, 209.90, 3.00";"XZdgBYgiHoSAAAAAAAAApX2I";"AO+YAA4pAE4g";"";"";"";"0029, 0028";"";"";"";{"Zuordnung": "0004-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
18;"ILS 2.0 Separator";"0018";"Separator :3";"F/8";1;"7149.18, 1885.99, 2001.50";"209.90, 150.00, 3.00";"bCxOHMcmHoSAAAAAAAAAAAAA";"ARTsAEmrAE4g";"";"";"";"0020, 0027";"";"";"";{"Zuordnung": "0005-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 0, "Typ": ""}
19;"ILS 2.0 Separator";"0019";"Separator :2";"F/8";1;"6933.60, 2908.49, 2001.50";"150.00, 209.90, 3.00";"acxgK3ccHoSAAAAAAAAAWoJ4";"AQ7YAG9FAE4g";"";"";"";"0027, 0027";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
20;"ILS 2.0 BTMT Beladung";"0020";"BTMT Beladung :1";"I/7-J/8";1;"14374.25, 1186.94, 1955.31";"645.71, 397.50, 316.02";"f///EJImHoSAAAAAAAAAAAAA";"AiYBADU7AE6r";"Aj3vACprAE4g";"";"";"0028, 0018";"";"";"";{"ARTINR": "829662012", "Höhe": "2000", "Drehung": 0}
21;"ILS 2.0 BTMT Entladung";"0021";"BTMT Entladung :1";"A/7";1;"-2501.71, 212.89, 1847.09";"1468.14, 950.36, 467.01";"3bjiCN/+HoSAAAAAAAAAf/+r";"/7UPABabAE4f";"/4Hu//aMAD/1";"";"";"0024, 0024";"";"";"";{"ARTINR": "400100156", "Höhe": "2000", "Drehung": 179}
22;"ILS 2.0 Separator";"0022";"Separator :1";"E/8";1;"4540.59, 1258.01, 2001.50";"150.00, 209.90, 3.00";"RUiuEkfsHoSAAAAAAAAAWoJ4";"ALFdAC7MAE4g";"";"";"";"0026, 0025";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
23;"ILS Weiche";"0023";"ILS Weiche :1";"B/7";1;"338.24, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0011, 0012";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0008", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
24;"ILS Weiche";"0024";"ILS Weiche :2";"B/7";1;"-461.76, 994.84, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0021, 0021";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0001", "Achse von Kreisel": "0001", "Beruehrung": "Kreis/Tangente"}
25;"ILS Weiche";"0025";"ILS Weiche :3";"E/7";1;"4540.59, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0022, 0013";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0008", "Kreisel B": "0007", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
26;"ILS Weiche";"0026";"ILS Weiche :4";"E/6";1;"4940.31, -2303.25, 2000.00";"250.07, 100.17, 30.00";"";"";"";"";"";"0014, 0015";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0007", "Kreisel B": "0002", "Achse von Kreisel": "0007", "Beruehrung": "Kreis/Kreis"}
27;"ILS Weiche";"0027";"ILS Weiche :5";"F/8";1;"6533.60, 1885.99, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0018, 0028";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0006", "Kreisel B": "0005", "Achse von Kreisel": "0006", "Beruehrung": "Kreis/Kreis"}
28;"ILS Weiche";"0028";"ILS Weiche :6";"F/7";1;"6533.60, 1085.78, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0017, 0016";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0005", "Kreisel B": "0004", "Achse von Kreisel": "0005", "Beruehrung": "Kreis/Kreis"}
29;"ILS Weiche";"0029";"ILS Weiche :7";"E/3";1;"6136.36, -7203.05, 2000.00";"100.25, 250.10, 30.00";"";"";"";"";"";"0030, 0030";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0004", "Kreisel B": "0003", "Achse von Kreisel": "0004", "Beruehrung": "Kreis/Kreis"}
30;"ILS Weiche";"0030";"ILS Weiche :8";"E/3";1;"5339.58, -7202.95, 2000.00";"100.19, 250.07, 30.00";"";"";"";"";"";"0015, 0014";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0003", "Kreisel B": "0002", "Achse von Kreisel": "0003", "Beruehrung": "Kreis/Kreis"}
Can't render this file because it contains an unexpected character in line 2 and column 172.
+282
View File
@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/e_planning.py - Zuordnung Layout <-> E-Planung.
Die Geometrie ist synthetisch und nachgebaut nach den Befunden an Mubea
(doc/E-Planung_Bruecke.md): Speicherlinien im 800-mm-Raster, Sensor auf dem
Separator, Ventil ~270 mm daneben, Stausensor konstant ~1390 mm vom
Ausschleuselement. Der Lagebezug ist hier die Identitaet.
"""
import json
import shutil
from pathlib import Path
import pytest
import scl_skeleton
from dxf_registration import Transform
from e_planning import (
EDevice,
EplanDocument,
LayoutPoint,
Rule,
assign,
load_positionsdraw,
load_rules,
pair_drives,
)
from scl_context import build_context
from scl_skeleton import load_document
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
RULES = Path(__file__).resolve().parent.parent / "cfg" / "e_planning.ini"
IDENT = Transform(dx=0.0, dy=0.0)
def _dev(bmk: str, verw: str, x: float, y: float, **extra) -> EDevice:
return EDevice(bmk=bmk, verw=verw, pos=(x, y),
kennzeichnung="=A1+UH01-KF107", **extra)
def _rolle(assignments, layout_id: str, role: str):
return next((a for a in assignments
if a.layout_id == layout_id and a.role == role), None)
# ---------------------------------------------------------------------------
# Einlesen
# ---------------------------------------------------------------------------
def test_positionsdraw_einlesen(tmp_path):
datei = tmp_path / "x_positionsdraw.json"
datei.write_text(json.dumps({
"sensors": {
"BG0642@UNKNOWN": {"B": "BG0642", "VERW": "IN SEP",
"BEZEICHNUNG": "TEIL IN SEPARATOR STR. 20",
"KENNZEICHNUNG": "=A1+UH01-KF107", "pos": [1.0, 2.0]},
},
"schaltschrank_elemente": {
# Derselbe Motorschutz zweimal: einmal mit, einmal ohne Attribute
"FC0200@": {"IO": "FC0200", "VERW": "MOTORSCHUTZ",
"BEZEICHNUNG": "MOTORSCHUTZ CONVEYER #100", "pos": [5, 5]},
"FC0200": {"B": "FC0200", "pos": [6, 6]},
},
"double_ids": {"BG0516": [[0, 0], [1, 1]]},
}), encoding="utf-8")
warnungen: list[str] = []
geraete = {d.bmk: d for d in load_positionsdraw(datei, warnungen)}
assert set(geraete) == {"BG0642", "FC0200"}
bg = geraete["BG0642"]
assert (bg.verw, bg.pos, bg.cabinet) == ("IN SEP", (1.0, 2.0), False)
assert (bg.controller, bg.station, bg.prefix, bg.number) == ("UH01", "KF107", "BG", 642)
assert geraete["FC0200"].verw == "MOTORSCHUTZ"
assert geraete["FC0200"].cabinet
assert any("BG0516" in w for w in warnungen)
def test_regeln_aus_der_projektkonfiguration():
regeln, anker = load_rules(RULES)
rollen = [r.role for r in regeln]
assert anker == ["IN SEP"]
# Reihenfolge = Dateireihenfolge; der Sensor muss vor dem Ventil kommen
assert rollen.index("Separator.sensor") < rollen.index("Separator.stopper")
stopper = next(r for r in regeln if r.role == "Separator.stopper")
assert (stopper.layout, stopper.number_from, stopper.number_offset) == (
["Separator"], "Separator.sensor", 1)
def test_nummer_von_muss_eine_regel_sein(tmp_path):
ini = tmp_path / "r.ini"
ini.write_text("[Separator.stopper]\nverw = SEP\nnummer_von = Separator.gibtsnicht\n",
encoding="utf-8")
with pytest.raises(ValueError, match="gibtsnicht"):
load_rules(ini)
# ---------------------------------------------------------------------------
# Zuordnung
# ---------------------------------------------------------------------------
SENSOR = Rule(role="Separator.sensor", layout=["Separator"], verw=["IN SEP"],
prefix="BG", max_mm=300)
STOPPER = Rule(role="Separator.stopper", layout=["Separator"], verw=["SEP"],
prefix="MB", number_from="Separator.sensor", number_offset=1,
max_mm=600, target_mm=270)
def _speicher():
"""Drei Speicherlinien im 800-mm-Raster.
Das Ventil jeder Linie liegt 530 mm von ihrem Separator - und nur 270 mm
vom Separator der Nachbarlinie. Raeumlich waere das falsch; die
Nummernregel (MB = BG + 1) muss gewinnen.
"""
punkte = [LayoutPoint(f"00{i}", "Separator", 0.0, 800.0 * i) for i in (1, 2, 3)]
geraete = []
for i, bg in ((1, 642), (2, 644), (3, 646)):
geraete.append(_dev(f"BG0{bg}", "IN SEP", 0.0, 800.0 * i))
geraete.append(_dev(f"MB0{bg + 1}", "SEP", 0.0, 800.0 * i + 530.0))
return punkte, geraete
def test_ventil_ueber_die_nummer_nicht_ueber_den_abstand():
punkte, geraete = _speicher()
ergebnis = assign(punkte, geraete, [SENSOR, STOPPER], IDENT)
for sep, bg, mb in (("001", "BG0642", "MB0643"), ("002", "BG0644", "MB0645"),
("003", "BG0646", "MB0647")):
assert _rolle(ergebnis, sep, "Separator.sensor").bmk == bg
stopper = _rolle(ergebnis, sep, "Separator.stopper")
assert (stopper.bmk, stopper.rule) == (mb, "nummer")
def test_jedes_e_bauteil_nur_einmal():
"""Zwei Separatoren, ein Sensor dazwischen: nur der naehere bekommt ihn."""
punkte = [LayoutPoint("A", "Separator", 0, 0), LayoutPoint("B", "Separator", 0, 250)]
geraete = [_dev("BG0100", "IN SEP", 0, 100)]
ergebnis = assign(punkte, geraete, [SENSOR], IDENT)
assert [(a.layout_id, a.bmk) for a in ergebnis] == [("A", "BG0100")]
assert ergebnis[0].runner_up is None
def test_zu_weit_weg_bleibt_offen():
punkte = [LayoutPoint("A", "Separator", 0, 0)]
geraete = [_dev("BG0100", "IN SEP", 0, 400)]
assert assign(punkte, geraete, [SENSOR], IDENT) == []
def test_sollabstand_schlaegt_kleinsten_abstand():
"""Mubea 0080: ein fremder Stausensor liegt 269 mm nah, der eigene 1391 mm."""
stau = Rule(role="Ausschleus.stau", layout=["Ausschleus"], verw=["STAU*"],
prefix="BG", max_mm=1600, target_mm=1390)
punkte = [LayoutPoint("0080", "Ausschleus", 0, 0)]
geraete = [_dev("BG0525", "STAU_REF-LT", 269, 0),
_dev("BG0550", "STAU_REF-LT", -1391, 0)]
ergebnis = assign(punkte, geraete, [stau], IDENT)
assert [a.bmk for a in ergebnis] == ["BG0550"]
def test_lagebezug_wird_angewendet():
"""E-Koordinaten werden mit dem Lagebezug ins CSV gebracht."""
punkte = [LayoutPoint("A", "Separator", 1000, 2000)]
geraete = [_dev("BG0100", "IN SEP", 0, 0)]
ergebnis = assign(punkte, geraete, [SENSOR], Transform(dx=1000, dy=2000))
assert ergebnis[0].bmk == "BG0100" and ergebnis[0].distance == 0
def test_schrankelemente_werden_nicht_raeumlich_zugeordnet():
punkte = [LayoutPoint("A", "Separator", 0, 0)]
geraete = [_dev("BG0100", "IN SEP", 0, 0, cabinet=True)]
assert assign(punkte, geraete, [SENSOR], IDENT) == []
def test_motor_und_motorschutz_ueber_die_nummer():
geraete = [_dev("MA0200", "MOT_0,37kW", 0, 0, bezeichnung="PIN CONVEYOR#100"),
EDevice(bmk="FC0200", verw="MOTORSCHUTZ", cabinet=True),
_dev("MA0300", "MOT", 0, 0)]
antriebe = {d.motor: d.protection for d in pair_drives(geraete)}
assert antriebe == {"MA0200": "FC0200", "MA0300": ""}
# ---------------------------------------------------------------------------
# Bis in die SCL
# ---------------------------------------------------------------------------
def _eplan_fuer_mubea(csv_file: str = "mubea.csv") -> EplanDocument:
"""Zuordnung fuer Separator 0012 (TRO02 der Fixture) - nur Sensor und Ventil."""
punkte = [LayoutPoint("0012", "Separator", 0, 0)]
geraete = [_dev("BG0712", "IN SEP", 0, 0), _dev("MB0713", "SEP", 0, 300)]
return EplanDocument(csv_file=csv_file, eplan_file="Mubea_positionsdraw.json",
created="2026-01-01 00:00",
assignments=assign(punkte, geraete, [SENSOR, STOPPER], IDENT))
def test_kontext_setzt_namen_ein_und_loescht_den_merker():
document = load_document(FIXTURES / "mubea_tro.json")
kontext = build_context(document, "mubea_tro.json", eplan=_eplan_fuer_mubea())
tro02 = next(t for t in kontext.tros if t.id == "TRO02")
sep = tro02.separators[0]
assert (sep.layout, sep.sensor.value, sep.sensor.todo) == ("0012", "cInBG0712", False)
assert (sep.stopper.value, sep.stopper.todo) == ("MB0713", False)
# Was die E-Planung nicht liefert, bleibt offen
assert sep.no.todo
tro01 = next(t for t in kontext.tros if t.id == "TRO01")
assert tro01.separators[0].sensor.todo
assert kontext.eplan_source.startswith("Mubea_positionsdraw.json")
def test_kontext_setzt_namen_auch_in_speicherlinien_ein():
"""PinStore_Auto: die Linien sind Separatoren im Layout - ihre E-Planung-Namen
kommen in store.lines an (und damit ins PinStore-Template), nicht nur in den
Separatorblock."""
from model import Analysis, DerivedTro, TroDocument
from scl_skeleton import TEMPLATE_DIR, SclTemplates
linien = ["0066", "0069"]
store = DerivedTro(tro_id="TRO04", type_name="PinStore_Auto", label="Speicher",
hosts=["0016", "0017"], separators=linien, scanners=[],
reason="Test", predecessors=["TRO03"], successors=["TRO05"])
punkte = [LayoutPoint("0066", "Separator", 0, 0), LayoutPoint("0069", "Separator", 1000, 0)]
geraete = [_dev("BG0766", "IN SEP", 0, 0), _dev("MB0767", "SEP", 0, 300),
_dev("BG0769", "IN SEP", 1000, 0), _dev("MB0770", "SEP", 1000, 300)]
eplan = EplanDocument(csv_file="mubea.csv", eplan_file="Mubea_positionsdraw.json",
assignments=assign(punkte, geraete, [SENSOR, STOPPER], IDENT))
document = TroDocument.from_analysis(Analysis(tros=[store]), source="mubea.csv")
kontext = build_context(document, "mubea_tro.json", eplan=eplan)
zeilen = kontext.tros[0].store.lines
assert [(z.layout, z.sensor.value, z.stopper.value) for z in zeilen] == [
("0066", "cInBG0766", "MB0767"), ("0069", "cInBG0769", "MB0770")]
assert not any(n.todo for z in zeilen for n in (z.sensor, z.stopper))
text = SclTemplates(TEMPLATE_DIR).render(kontext.tros[0])
assert '#TRO04.arInSep[#nActNo].stSenTrolley := "DB_Inputs".Sensors["cInBG0766"];\n' in text
assert '"MB0770" := #TRO04.arOutSeparatorStore[2];\n' in text
def _lauf(tmp_path, monkeypatch, eplan: EplanDocument,
*weitere: str) -> tuple[int, Path]:
shutil.copy(FIXTURES / "mubea_tro.json", tmp_path / "mubea_tro.json")
(tmp_path / "mubea_eplan.json").write_text(eplan.to_json(), encoding="utf-8")
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
rc = scl_skeleton.main(["--json", "mubea_tro.json", "--eplan", "mubea_eplan.json",
*weitere])
return rc, tmp_path / "mubea_FB_Main.scl"
def test_scl_skeleton_mit_eplan(tmp_path, monkeypatch):
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea())
text = scl.read_text(encoding="utf-8")
assert rc == 0
assert '#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0712"];\n' in text
assert 'xOutStopper1 => "MB0713",\n' in text
assert "// E-Planung: Mubea_positionsdraw.json" in text
def test_scl_skeleton_mit_eplan_und_templates(tmp_path, monkeypatch):
"""Die Namen aus der E-Planung kommen auch in den Templates an (TRO02 = 1Sep)."""
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea(), "--templates")
text = scl.read_text(encoding="utf-8")
assert rc == 0
region = text[text.index("REGION TRO02 "):]
region = region[:region.index("\nEND_REGION")]
assert "Aufbau nach Template FB_ILS_MTRO_1Sep.scl.j2" in region
assert '#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0712"];\n' in region
assert 'xOutStopper1 => "MB0713",\n' in region
def test_scl_skeleton_lehnt_fremde_zuordnung_ab(tmp_path, monkeypatch):
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea("anderes.csv"))
assert rc == 1
assert not scl.exists()
def test_eplan_json_rundlauf():
doc = _eplan_fuer_mubea()
zurueck = EplanDocument.from_json(doc.to_json())
assert zurueck == doc
assert json.loads(doc.to_json())["schema"] == "sps_skel-eplan-v1"
assert set(zurueck.for_layout("0012")) == {"sensor", "stopper"}
+279
View File
@@ -0,0 +1,279 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/hw_import.py - TIA-Import + Positionsdatei -> HardwareDocument.
Die TIA-Datei wird hier als kleine xlsx gebaut (zipfile, wie sie der
TIA-Export schreibt: Blatt "PLC Tags" und "Constants"); Namen, Adressen und
Klartexte nach dem Mubea-Datensatz (data/Mubea/mubea-1_TIA.xlsx).
"""
import json
import zipfile
from pathlib import Path
import pytest
import hw_import
from hw_import import (
HardwareDocument,
build_hardware,
conveyor_kind_of,
conveyor_of,
load_tia,
project_name,
read_xlsx,
)
_HEAD = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<{tag} xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">')
def _col(i: int) -> str:
out = ""
i += 1
while i:
i, r = divmod(i - 1, 26)
out = chr(65 + r) + out
return out
def write_xlsx(path: Path, sheets: dict[str, list[list]], shared: bool = True) -> Path:
"""Minimale xlsx: Texte als Shared Strings (oder inline), Zahlen als Werte."""
strings: list[str] = []
files = {}
for n, (name, rows) in enumerate(sheets.items(), start=1):
xml_rows = []
for r, row in enumerate(rows, start=1):
cells = []
for c, value in enumerate(row):
ref = f"{_col(c)}{r}"
if value is None:
continue
if isinstance(value, (int, float)):
cells.append(f'<c r="{ref}"><v>{value}</v></c>')
elif shared:
if value not in strings:
strings.append(value)
cells.append(f'<c r="{ref}" t="s"><v>{strings.index(value)}</v></c>')
else:
cells.append(f'<c r="{ref}" t="inlineStr"><is><t>{value}</t></is></c>')
xml_rows.append(f'<row r="{r}">{"".join(cells)}</row>')
files[f"xl/worksheets/sheet{n}.xml"] = (
_HEAD.format(tag="worksheet") + f'<sheetData>{"".join(xml_rows)}</sheetData></worksheet>')
sheet_xml = "".join(f'<sheet name="{name}" sheetId="{n}" r:id="rId{n}"/>'
for n, name in enumerate(sheets, start=1))
files["xl/workbook.xml"] = _HEAD.format(tag="workbook") + f"<sheets>{sheet_xml}</sheets></workbook>"
rels = "".join(f'<Relationship Id="rId{n}" Type="worksheet" Target="worksheets/sheet{n}.xml"/>'
for n in range(1, len(sheets) + 1))
files["xl/_rels/workbook.xml.rels"] = (
'<?xml version="1.0" encoding="UTF-8"?><Relationships '
'xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
f"{rels}</Relationships>")
if shared and strings:
si = "".join(f"<si><t>{s}</t></si>" for s in strings)
files["xl/sharedStrings.xml"] = _HEAD.format(tag="sst") + f"{si}</sst>"
with zipfile.ZipFile(path, "w") as z:
for name, text in files.items():
z.writestr(name, text)
return path
TAG_HEADER = ["Name", "Path", "Data Type", "Logical Address", "Comment", "Hmi Visible"]
CONST_HEADER = ["Name", "Path", "Data Type", "Value", "Comment"]
def tia_file(tmp_path: Path, extra_tags=(), extra_consts=()) -> Path:
tags = [
["BG0532", "Import", "Bool", "%E053.2", "SENSOR IN SEP KR-K0102", "True"],
["MB0533", "Import", "Bool", "%A053.3", "STOPPER KR-K0102", "True"],
["MA0202", "Import", "Bool", "%A020.2", "ANTRIEB KREISEL-K0102", "True"],
["FC0202", "Import", "Bool", "%E020.2", "MOTORSCHUTZ KREISEL-K0102", "True"],
["GF20000", "Import", "Bool", "%A2000.0", "INKREMENTALGEBER KREISEL K0102 (M0205)", "True"],
["SF0001", "Import", "Bool", "%E000.1", "ANLAGE EIN", "True"],
["BG0999", "Import", "Bool", "%E099.0", "NUR IM TIA", "True"],
*extra_tags,
]
consts = [
["cInBG0532", "Import", "Int", 0],
["cOutMB0533", "Import", "Int", 0],
["cOutMA0202", "Import", "Int", 1],
["cInFC0202", "Import", "Int", 1],
["cOutGF20000", "Import", "Int", 2],
["cInSF0001", "Import", "Int", 2],
["cInBG0999", "Import", "Int", 3],
*extra_consts,
]
return write_xlsx(tmp_path / "x_TIA.xlsx",
{"PLC Tags": [TAG_HEADER, *tags], "Constants": [CONST_HEADER, *consts]})
def positions_file(tmp_path: Path) -> Path:
data = {
"sensors": {
"BG0532@1": {"IO": "BG0532", "VERW": "IN SEP", "BEZEICHNUNG": "SENSOR IN SEP KR-K0102",
"KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 200.0]},
"MB0533@1": {"IO": "MB0533", "VERW": "SEP", "BEZEICHNUNG": "STOPPER KR-K0102",
"KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 470.0]},
"BG0514@1": {"IO": "BG0514", "VERW": "KETTENBRUCH",
"BEZEICHNUNG": "SENSOR KETTENBRUCH CONVEYOR #101", "KENNZEICHNUNG": "",
"pos": [5.0, 5.0]},
},
"schaltschrank_elemente": {
"MA0202@1": {"IO": "MA0202", "VERW": "MOT", "BEZEICHNUNG": "ANTRIEB KREISEL-K0102",
"KENNZEICHNUNG": "=A1+UH01-KF1DQ3"},
"FC0202@1": {"IO": "FC0202", "BEZEICHNUNG": "MOTORSCHUTZ KREISEL-K0102",
"KENNZEICHNUNG": "=A1+UH01-KF1DI2"},
"QA7000@A/B": {"IO": "QA7000", "VERW": "IL-YE-LED", "BEZEICHNUNG": "HAUPTSCHUETZE",
"KENNZEICHNUNG": "=A1+UH01-KF1FDQ1"},
"SF0001@1": {"IO": "SF0001", "BEZEICHNUNG": "ANLAGE EIN"},
"GF20000@1": {"IO": "GF20000",
"BEZEICHNUNG": "INKREMENTALGEBER KREISEL K0102 (M0205)"},
},
"warnings": {"mapping_warnings": {"BG0514@1": "Ungueltiger Pfad in Kennzeichnung: "}},
}
path = tmp_path / "Xproj_positionsconv.json"
path.write_text(json.dumps(data), encoding="utf-8")
return path
# ---------------------------------------------------------------------------
# xlsx und TIA-Import
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("shared", [True, False])
def test_xlsx_lesen(tmp_path, shared):
path = write_xlsx(tmp_path / "t.xlsx", {"A": [["Name", "Wert"], ["x", 3], [None, "y"]]},
shared=shared)
assert read_xlsx(path) == {"A": [["Name", "Wert"], ["x", "3"], ["", "y"]]}
def test_tia_blaetter_an_der_kopfzeile_erkannt(tmp_path):
"""Blattnamen sind egal (deutscher Export), gezaehlt wird die Kopfzeile."""
path = write_xlsx(tmp_path / "de_TIA.xlsx", {
"PLC-Variablen": [["Name", "Pfad", "Datentyp", "Logische Adresse", "Kommentar"],
["BG0532", "Import", "Bool", "%I53.2", "SENSOR"]],
"Konstanten": [["Name", "Pfad", "Datentyp", "Wert", "Kommentar"],
["cInBG0532", "Import", "Int", "7", ""]],
"Leer": [["irgendwas"]],
})
warnings: list[str] = []
tags, consts = load_tia(path, warnings)
assert [(t.name, t.address, t.comment) for t in tags] == [("BG0532", "%I53.2", "SENSOR")]
assert [(c.name, c.value) for c in consts] == [("cInBG0532", "7")]
assert any("'Leer'" in w for w in warnings)
def test_tia_ohne_tags_ist_ein_fehler(tmp_path):
path = write_xlsx(tmp_path / "x.xlsx", {"Constants": [CONST_HEADER, ["cInBG1", "", "Int", 0]]})
with pytest.raises(ValueError, match="kein Blatt mit Tags"):
load_tia(path)
# ---------------------------------------------------------------------------
# Klartext -> Foerderer
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("text, name, kind", [
("ANTRIEB KREISEL-K0102", "K0102", "Kreisel"),
("PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)", "K0102", "Kreisel"),
# dieselbe Strecke einmal als K0103, einmal als #103
("MOTORSCHUTZ CONVEYOR-K0103", "K0103", "Strecke"),
("MOTOR TEF LBT CONVEYOR-#103", "K0103", "Strecke"),
("PIN CONVEYOR #109(EXPRESS)", "K0109", "Strecke"),
# FC0204 im Text ist kein Foerderer
("MOTORSCHUTZ FC0204 CONVEYOR #110", "K0110", "Strecke"),
("SEPARATOR STR. 14", "", ""),
])
def test_foerderer_aus_klartext(text, name, kind):
assert conveyor_of(text) == name
assert conveyor_kind_of(text) == kind
# ---------------------------------------------------------------------------
# Zusammenfuehren
# ---------------------------------------------------------------------------
def test_beide_quellen_je_bmk_zusammengefuehrt(tmp_path):
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj")
dev = doc.by_bmk()
s = dev["BG0532"]
assert (s.direction, s.address, s.constant, s.constant_value) == ("in", "%E053.2", "cInBG0532", 0)
assert (s.verw, s.station, s.controller, s.pos) == ("IN SEP", "KF102", "UH01", (100.0, 200.0))
assert s.sources == ["tia", "positions"]
assert (s.conveyor, s.conveyor_kind) == ("K0102", "Kreisel")
assert dev["MB0533"].constant == "cOutMB0533" and dev["MB0533"].direction == "out"
# nur in einer Quelle
assert dev["QA7000"].sources == ["positions"] and dev["QA7000"].constant == ""
assert dev["BG0999"].sources == ["tia"] and dev["BG0999"].pos is None
assert [d.bmk for d in doc.field_devices()] == ["BG0514", "BG0532", "MB0533"]
assert doc.tia_sha256 and doc.positions_sha256
assert doc.project == "Xproj"
def test_antrieb_mit_motorschutz_und_geber(tmp_path):
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path))
[drive] = doc.drives
assert (drive.motor, drive.protection, drive.conveyor, drive.conveyor_kind, drive.encoder) == (
"MA0202", "FC0202", "K0102", "Kreisel", "GF20000")
def test_gegenpruefung(tmp_path):
doc = build_hardware(
tia_file(tmp_path,
extra_tags=[["BG0533", "Import", "Bool", "%E053.2", "GLEICHE ADRESSE", ""]],
extra_consts=[["cInBG4711", "Import", "Int", 9]]),
positions_file(tmp_path))
checks = "\n".join(doc.checks)
assert "nur in der Positionsdatei, nicht im TIA-Import (2): BG0514, QA7000" in checks
assert "Feldgeraet nur im TIA-Import, nicht in der Positionsdatei (2): BG0533, BG0999" in checks
assert "Signal ohne Index-Konstante cIn/cOut (1): BG0533" in checks
assert "Index-Konstante ohne Signal (1): cInBG4711" in checks
assert "Adresse mehrfach vergeben: %E053.2" in checks
assert "Feldgeraet ohne Steuerung in KENNZEICHNUNG (1): BG0514" in checks
assert any("BG0514" in w and "mapping_warnings" in w for w in doc.warnings)
assert [c.name for c in doc.constants] == ["cInBG4711"]
def test_nur_tia_ohne_positionsdatei(tmp_path):
doc = build_hardware(tia_file(tmp_path), None)
assert doc.positions_file == "" and doc.field_devices() == []
assert all(d.sources == ["tia"] for d in doc.devices)
def test_json_rundreise(tmp_path):
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj")
again = HardwareDocument.from_json(doc.to_json())
assert again == doc
assert json.loads(doc.to_json())["schema"] == "sps_skel-hw-v1"
def test_projektname():
assert project_name(None, Path("Mubea_positionsconv.json")) == "Mubea"
assert project_name(None, Path("Mubea_positionsdraw.json")) == "Mubea"
assert project_name(Path("mubea-1_TIA.xlsx"), None) == "mubea-1"
def test_cli_mit_unterordner_in_skel_data(tmp_path, monkeypatch, capsys):
"""--tia Mubea/x_TIA.xlsx wird ab SKEL_DATA gefunden (Befund der Nachpruefung)."""
data = tmp_path / "data" / "Xproj"
data.mkdir(parents=True)
tia_file(data)
positions_file(data)
monkeypatch.setenv("SKEL_DATA", str(tmp_path / "data"))
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path / "results"))
monkeypatch.chdir(tmp_path)
rc = hw_import.main(["--tia", "Xproj/x_TIA.xlsx", "--positions",
"Xproj/Xproj_positionsconv.json"])
assert rc == 0
written = HardwareDocument.from_json(
(tmp_path / "results" / "Xproj_hw.json").read_text(encoding="utf-8"))
assert written.project == "Xproj" and len(written.devices) == 9
assert (tmp_path / "results" / "Xproj_hw.md").is_file()
assert "HARDWAREPLANUNG EINLESEN" in capsys.readouterr().out
+151
View File
@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/kreisel_junctions.py - Weichenknoten zwischen Kreiseln.
Die Geometrie stammt aus der Testanlage data/export.csv (9 Kreisel, 10
Separatoren, 5 ILS Weichen; drei Beruehrpunkte fehlen im Export als Weiche).
"""
from __future__ import annotations
import math
import pytest
from kreisel_junctions import find_contacts, find_junctions, kreisel_loop
from material_flow import Element
def _el(teile_id: str, kind: str, pos: tuple[float, float],
bbox: tuple[float, float] | None = None, **merkmale: object) -> Element:
return Element(
number=teile_id, kind=kind, teile_art=kind, teile_id=teile_id,
name=f"{kind} {teile_id}", plan_square="", neighbours=[], track_ids=[],
merkmale=dict(merkmale), row=0, position=(pos[0], pos[1], 2000.0),
bbox=(bbox[0], bbox[1], 1.0) if bbox else None,
)
def _kreisel(teile_id: str, pos: tuple[float, float], bbox: tuple[float, float],
drehrichtung: str, drehung: float) -> Element:
return _el(teile_id, "Kreisel", pos, bbox, Drehrichtung=drehrichtung, Drehung=drehung)
# (Id, Position, Boundingbox, Drehrichtung, Drehung) aus data/export.csv
KREISEL = [
("0001", (-2011.74, 994.84), (3100.00, 800.00), "UZS", 0),
("0002", (4940.03, -4953.25), (800.00, 5300.00), "UZS", 90),
("0003", (5739.13, -9850.80), (800.00, 6095.71), "GUZ", 90),
("0004", (6533.60, -3258.73), (800.00, 8689.42), "UZS", 90),
("0005", (11425.39, 1485.99), (10583.58, 800.00), "GUZ", 0),
("0006", (6533.60, 4145.31), (800.00, 4518.63), "UZS", 90),
("0007", (4940.59, -453.25), (800.00, 3700.00), "GUZ", 90),
("0008", (2440.85, 505.79), (4200.00, 800.00), "UZS", 0),
("0009", (-61.76, 1425.92), (800.00, 4800.00), "GUZ", 90),
]
SEPARATOREN = [
("0010", (-461.76, 1526.13)), ("0011", (1244.39, 105.79)),
("0012", (3699.43, 905.79)), ("0013", (4540.59, -1493.47)),
("0014", (4540.03, -3169.65)), ("0015", (5340.03, -6534.21)),
("0016", (6933.60, -6618.23)), ("0017", (6133.60, 302.58)),
("0018", (7149.18, 1885.99)), ("0019", (6933.60, 2908.49)),
]
WEICHEN = [
("0022", (-461.76, 994.84)), ("0023", (4540.59, 505.79)),
("0024", (4940.31, -2303.25)), ("0025", (6533.60, 1885.99)),
("0026", (6533.60, 1085.78)),
]
@pytest.fixture
def testanlage() -> list[Element]:
return ([_kreisel(*k) for k in KREISEL]
+ [_el(i, "Separator", p) for i, p in SEPARATOREN]
+ [_el(i, "Weiche", p) for i, p in WEICHEN])
@pytest.mark.parametrize("drehrichtung", ["UZS", "GUZ"])
def test_loop_point_project_roundtrip(drehrichtung: str) -> None:
loop = kreisel_loop(_kreisel("K", (100.0, 200.0), (800.0, 3000.0), drehrichtung, 90))
assert loop is not None
for i in range(40):
s = i * loop.length / 40
x, y = loop.point(s)
s2, dist, _sec = loop.project(x, y)
assert dist < 1e-6
assert min(abs(s2 - s), loop.length - abs(s2 - s)) < 1e-6
def test_loop_direction_follows_drehrichtung() -> None:
# Vertikaler Kreisel (Drehung 90), Bahnen bei x = -400 und x = +400:
# UZS laeuft auf der linken (westlichen) Bahn nach Norden.
uzs = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "UZS", 90))
guz = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "GUZ", 90))
s_uzs, _d, _sec = uzs.project(-400.0, 0.0)
s_guz, _d, _sec = guz.project(-400.0, 0.0)
assert uzs.direction(s_uzs) == pytest.approx((0.0, 1.0), abs=1e-6)
assert guz.direction(s_guz) == pytest.approx((0.0, -1.0), abs=1e-6)
def test_contacts_match_exported_weichen(testanlage: list[Element]) -> None:
findings: list[str] = []
_loops, contacts = find_contacts(testanlage, findings)
by_pair = {c.kreisel: c for c in contacts}
assert sorted(by_pair) == [
("0001", "0009"), ("0002", "0003"), ("0002", "0007"), ("0003", "0004"),
("0004", "0005"), ("0005", "0006"), ("0007", "0008"), ("0008", "0009"),
]
assert by_pair[("0002", "0007")].weiche_id == "0024"
assert by_pair[("0005", "0006")].weiche_id == "0025"
# Im Export fehlende Weichen bekommen eine abgeleitete Id und ein Finding
assert by_pair[("0008", "0009")].weiche_id == "W0008_0009"
assert not by_pair[("0008", "0009")].exported
assert sum("ohne ILS Weiche im Export" in f for f in findings) == 3
def test_junction_types_testanlage(testanlage: list[Element]) -> None:
junctions = find_junctions(testanlage, [])
result = sorted((j.type_name, tuple(j.separators),
tuple(c.weiche_id for c in j.contacts)) for j in junctions)
assert result == [
("1Sep1Swi", ("0010",), ("0022",)),
("1Sep1Swi", ("0011",), ("W0008_0009",)),
("1Sep1Swi", ("0012",), ("0023",)),
("2Sep1Swi", ("0013", "0014"), ("0024",)),
("2Sep2Swi", ("0015", "0016"), ("W0002_0003", "W0003_0004")),
("3Sep2Swi", ("0017", "0018", "0019"), ("0025", "0026")),
]
def test_separator_behind_other_weiche_does_not_control(testanlage: list[Element]) -> None:
# 0010 liegt auf Kreisel 9 vor Weiche 0022; die Weiche 9/8 folgt erst
# danach - sie steuert 0011 von Kreisel 8 aus, nicht 0010.
junctions = find_junctions(testanlage, [])
j98 = next(j for j in junctions if j.contacts[0].weiche_id == "W0008_0009")
assert j98.separators == ["0011"]
assert j98.sep_lane["0011"] == "0008-R"
def test_separator_lane_is_physical_not_zuordnung(testanlage: list[Element]) -> None:
# 0015 ist im Export Kreisel 0003 zugeordnet, liegt aber auf Kreisel 0002
junctions = find_junctions(testanlage, [])
j = next(j for j in junctions if "0015" in j.separators)
assert j.sep_kreisel["0015"] == "0002"
assert j.sep_lane["0015"] == "0002-R"
def test_counter_rotating_contact_is_no_weiche() -> None:
# Zwei UZS-Kreisel Stirn an Stirn laufen am Beruehrpunkt gegeneinander
a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0)
b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "UZS", 0)
findings: list[str] = []
_loops, contacts = find_contacts([a, b], findings)
assert contacts == []
assert any("gegenlaeufig" in f for f in findings)
def test_co_rotating_end_contact_is_weiche() -> None:
a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0)
b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "GUZ", 0)
_loops, contacts = find_contacts([a, b], [])
assert len(contacts) == 1
assert math.hypot(contacts[0].x - 1500.0, contacts[0].y) < 5.0
+118
View File
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""Tests fuer den Kreisel-Umlauf im Materialflussgraphen (lib/material_flow.py).
Ein Kreisel ist ein geschlossener Umlauf: das Ende von L laeuft in den Anfang
von R und das Ende von R in den Anfang von L. Frueher bekam jeder Kreisel nur
EINE Bahnkante (aus der Drehrichtung) - jede Bahn ohne Gegenkante galt dann
als Sackgasse bzw. unerreichbar, obwohl die Anlage vollstaendig durchlaufbar
ist. Fixture: Testanlage test_system (dxfmakros results/export.csv, 9 Kreisel,
8 Kreisel-Kreisel-Weichen, 1 Beladung, 1 Entladung).
"""
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
import pytest
from material_flow import (
CIRCLE_RETURN_NOTE,
EDGE_CIRCLE,
EDGE_FLOW,
EDGE_UNRESOLVED,
Edge,
Element,
Node,
_reachability_warnings,
build_graph,
read_elements,
)
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
@pytest.fixture(scope="module")
def elements() -> list[Element]:
return read_elements(FIXTURE, [])
def _graph(elements: list[Element]):
warnings: list[str] = []
return build_graph(elements, FIXTURE.name, warnings), warnings
def test_jeder_kreisel_hat_beide_bahnkanten(elements):
graph, _ = _graph(elements)
for el in (e for e in elements if e.is_circle):
left, right = f"{el.teile_id}-L", f"{el.teile_id}-R"
circle = [e for e in graph.edges if e.kind == EDGE_CIRCLE
and {e.src, e.dst} == {left, right}]
assert {(e.src, e.dst) for e in circle} == {(left, right), (right, left)}
# genau eine Hauptkante mit dem Drehrichtungs-Kuerzel
assert sorted(e.note for e in circle) == sorted([el.rotation, CIRCLE_RETURN_NOTE])
def test_keine_sackgassen_in_durchlaufbarer_anlage(elements):
_, warnings = _graph(elements)
bahn = [w for w in warnings if w.startswith("Bahn ")]
assert bahn == []
def test_kreis_kreis_weiche_ist_gerichtet(elements):
"""Weiche 0006<->0005 ist "Kreis/Kreis" - liegt genau auf dem Uebergang
Endkappe/Gerade, die Geometrie allein entschied dort per Rundung auf
"Laengsseite, unbestimmt". Das Merkmal "Beruehrung" hat Vorrang."""
graph, _ = _graph(elements)
weiche = [e for e in graph.edges if e.note == "Weiche"
and {e.src.split("-")[0], e.dst.split("-")[0]} == {"0005", "0006"}]
assert weiche
assert all(e.kind == EDGE_FLOW for e in weiche)
def test_ohne_weichen_meldet_unerreichbare_bahnen(elements):
"""Gegenprobe: ohne die Kreisel-Kreisel-Weichen (und ohne die rohen
Kreisel-Nachbarn, die sonst als unbestimmte Kanten verbinden) ist nur der
Kreisel der Beladung erreichbar - die Pruefung muss das melden."""
ohne = [replace(e, neighbours=[]) if e.is_circle else e
for e in elements if e.kind != "Weiche"]
_, warnings = _graph(ohne)
assert any("von keiner Beladung" in w for w in warnings)
assert any("keine Entladung" in w for w in warnings)
def _el(teile_id: str, kind: str) -> Element:
return Element(
number=teile_id, kind=kind, teile_art=kind, teile_id=teile_id,
name=f"{kind} {teile_id}", plan_square="", neighbours=[], track_ids=[],
merkmale={}, row=0, position=(0.0, 0.0, 0.0), bbox=None,
)
def test_unbestimmte_kante_in_beide_richtungen_begehbar():
"""Eine Weiche mit unbestimmter Richtung (dir=both) darf nicht nur in
src->dst-Richtung zaehlen."""
k1, k2 = _el("0001", "Kreisel"), _el("0002", "Kreisel")
nodes = {
"0001-L": Node("0001-L", "Kreisel", k1, lane="L"),
"0001-R": Node("0001-R", "Kreisel", k1, lane="R"),
"0002-L": Node("0002-L", "Kreisel", k2, lane="L"),
"0002-R": Node("0002-R", "Kreisel", k2, lane="R"),
"B": Node("B", "Beladung", _el("B", "Beladung")),
"E": Node("E", "Entladung", _el("E", "Entladung")),
}
edges = [
Edge("0001-L", "0001-R", EDGE_CIRCLE, "GUZ"),
Edge("0001-R", "0001-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
Edge("0002-L", "0002-R", EDGE_CIRCLE, "GUZ"),
Edge("0002-R", "0002-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
# Beladung an Kreisel 2, Entladung an Kreisel 1, Weiche zeigt "falsch herum"
Edge("B", "0002-L", EDGE_FLOW),
Edge("0001-L", "E", EDGE_FLOW),
Edge("0001-R", "0002-R", EDGE_UNRESOLVED, "Weiche"),
]
assert _reachability_warnings(nodes, edges) == []
# gerichtet waere Kreisel 1 von der Beladung aus nicht erreichbar
edges[-1] = Edge("0001-R", "0002-R", EDGE_FLOW, "Weiche")
assert any("0001-L" in w and "von keiner Beladung" in w
for w in _reachability_warnings(nodes, edges))
+318
View File
@@ -0,0 +1,318 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/merge_hw_layout.py - TRO-JSON + Hardware-JSON -> Anlagen-JSON.
Synthetische Anlage nach den Mubea-Befunden (doc/E-Planung_Bruecke.md):
Separator-Sensor auf dem Separator, Stopperventil ~270 mm daneben mit
Nummer = Sensor + 1, Stausensor ~1390 mm vom Ausschleuselement, Kreisel mit
ueberlappender Speicherlinie (Gefaellestrecke), Vario-Strecke mit Antrieb.
"""
import json
from pathlib import Path
import pytest
import merge_hw_layout
from dxf_registration import Transform, load_transform, save_transform
from merge_hw_layout import (
build_anlage,
load_overrides,
load_rules,
)
from hw_import import HardwareDocument, HwDevice, conveyor_kind_of, conveyor_of, pair_drives
from model import AnlageDocument, Analysis, DerivedTro, LayoutPart, TroDocument, derive_slots
RULES = Path(__file__).resolve().parent.parent / "cfg" / "e_planning.ini"
IDENT = Transform(dx=0.0, dy=0.0)
SEPS = {f"000{n}": ((n - 1) * 800.0, 0.0) for n in range(1, 7)}
def _part(layout_id, kind, x, y, bbox=None, name=""):
return LayoutPart(layout_id=layout_id, kind=kind, name=name or f"{kind}{layout_id}",
position=(x, y, 0.0), bbox=bbox)
def _tro(tro_id, type_name, seps, hosts=()):
return DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id, hosts=list(hosts),
separators=list(seps), scanners=[], reason="Test")
def layout() -> TroDocument:
parts = [_part(i, "Separator", x, y) for i, (x, y) in SEPS.items()]
parts += [
_part("0030", "Ausschleus", 0.0, 2000.0),
_part("0010", "Kreisel", 2000.0, 5000.0, (6000.0, 800.0, 1.0), "Kreisel3"),
# Speicherlinie, deren Huellbox den Kreisel ueberdeckt (wie bei Mubea)
_part("0040", "Gefaellestrecke", 2000.0, 5000.0, (6000.0, 1200.0, 1.0), "GF_1"),
_part("0020", "Strecke", 2000.0, -3000.0, (6000.0, 800.0, 1.0), "VF_1"),
]
tros = [
_tro("TRO01", "1Sep", ["0001"], ["0010-R"]),
_tro("TRO02", "1Sep1Swi", ["0002"], ["0010-R"]),
_tro("TRO03", "Vario", ["0003"], ["0020"]),
_tro("TRO04", "1Sep", ["0004"]),
_tro("TRO05", "1Sep", ["0005"]),
_tro("TRO06", "1Sep", ["0006"]),
]
return TroDocument.from_analysis(Analysis(tros=tros), source="x.csv", parts=parts)
def _dev(bmk, verw, pos=None, text="", tia=True, cabinet=False, direction="in"):
return HwDevice(bmk=bmk, verw=verw, bezeichnung=text, kennzeichnung="=A1+UH01-KF102",
pos=pos, cabinet=cabinet, direction=direction,
address="%E000.0" if direction == "in" else "%A000.0",
constant=(f"cIn{bmk}" if direction == "in" else f"cOut{bmk}") if tia else "",
conveyor=conveyor_of(text), conveyor_kind=conveyor_kind_of(text),
sources=["tia", "positions"] if tia else ["positions"])
def hardware(shift=(0.0, 0.0)) -> HardwareDocument:
"""E-Zeichnung = Layout - shift (shift = Lagebezug E -> Layout)."""
def at(x, y):
return (x - shift[0], y - shift[1])
devices = []
for n in range(1, 6):
x, y = SEPS[f"000{n}"]
devices.append(_dev(f"BG0{n}01", "IN SEP", at(x, y)))
if n != 3:
# MB0502 fehlt im TIA-Import -> Widerspruch
devices.append(_dev(f"MB0{n}02", "SEP", at(x, y + 270.0), tia=(n != 5),
direction="out"))
devices += [
_dev("BG0601", "STAU_REF-LT", at(0.0, 2000.0 + 1390.0)),
_dev("BG0701", "SYNC P1", at(1000.0, 5100.0), "PRUEFUNG BT (KR-K0102 PUFFER)"),
_dev("BG0702", "BT BELADEN", at(3000.0, 4900.0), "BT BELADEN KR-K0102"),
_dev("BG0801", "KETTENBRUCH", at(1000.0, -3000.0), "KETTENBRUCH CONVEYOR #106"),
_dev("BG0802", "UEBERGABE", at(3000.0, -3000.0), "UEBERGABE CONVEYOR #106"),
_dev("BG0900", "UEBERNAHME", at(50000.0, 0.0), "SENSOR UEBERNAHME"),
_dev("MA0202", "MOT", None, "ANTRIEB KREISEL-K0102", cabinet=True, direction="out"),
_dev("FC0202", "", None, "MOTORSCHUTZ KREISEL-K0102", cabinet=True),
_dev("MA0210", "MOT", None, "PIN CONVEYOR-#106", cabinet=True, direction="out"),
_dev("FC0210", "", None, "MOTORSCHUTZ CONVEYOR #106", cabinet=True),
]
return HardwareDocument(project="X", positions_file="X_positionsconv.json",
positions_sha256="abc", devices=devices,
drives=pair_drives(devices))
def run(doc=None, hw=None, overrides=None, transform=IDENT, tmp_path=None):
rules, anchors = load_rules(RULES)
return build_anlage(doc or layout(), hw or hardware(), rules, anchors,
tmp_path or Path("."), "x_tro.json", "X_hw.json",
overrides, transform=transform)
# ---------------------------------------------------------------------------
# Slots
# ---------------------------------------------------------------------------
def test_separator_sensor_raeumlich_stopper_ueber_nummer():
anlage = run()
sensor = anlage.binding("TRO01", "separator1", "sensor")
stopper = anlage.binding("TRO01", "separator1", "stopper")
assert (sensor.bmk, sensor.constant, sensor.rule, sensor.status) == (
"BG0101", "cInBG0101", "raeumlich", "zugeordnet")
assert (stopper.bmk, stopper.constant, stopper.rule) == ("MB0102", "cOutMB0102", "nummer")
assert stopper.layout_id == "0001"
def test_bmk_nicht_im_tia_ist_widerspruch():
b = run().binding("TRO05", "separator1", "stopper")
assert (b.bmk, b.status, b.note) == ("MB0502", "widerspruch", "BMK nicht im TIA-Import")
def test_pflichtrollen_ohne_bmk_sind_offen_optionale_nicht():
anlage = run()
roles = {(b.slot, b.role): b.status for b in anlage.bindings if b.tro == "TRO06"}
assert roles == {("separator1", "sensor"): "offen", ("separator1", "stopper"): "offen"}
# die Weiche hat noch keine Layout-ID -> Ventil offen
assert anlage.binding("TRO02", "switch1", "valve").status == "offen"
def test_bauteil_ohne_slot_bleibt_sichtbar():
loose = [b for b in run().bindings if not b.tro]
assert [(b.role, b.layout_id, b.bmk, b.status) for b in loose] == [
("Ausschleus.stau", "0030", "BG0601", "ohne_slot")]
def test_unzugeordnete_feldgeraete():
assert "BG0900" in run().unassigned
assert "BG0101" not in run().unassigned
# ---------------------------------------------------------------------------
# Foerderer
# ---------------------------------------------------------------------------
def test_foerderer_per_mehrheit_und_art():
"""K0102 nennt "KR" -> nur Kreisel stimmen, obwohl die Speicherlinie den
Kreisel ueberdeckt; #106 nennt "CONVEYOR" -> Strecke."""
anlage = run()
by_name = {c.eplan_name: c for c in anlage.conveyors}
k = by_name["K0102"]
assert (k.layout_id, k.layout_name, k.votes, k.total, k.status) == (
"0010", "Kreisel3", 2, 2, "zugeordnet")
assert (k.motor, k.protection) == ("MA0202", "FC0202")
assert by_name["K0106"].layout_id == "0020"
assert anlage.conveyor_for("0010-R").eplan_name == "K0102"
def test_vario_antrieb_aus_dem_foerderer():
anlage = run()
motor = anlage.binding("TRO03", "drive", "motor")
schutz = anlage.binding("TRO03", "drive", "motorProtection")
assert (motor.bmk, motor.rule, motor.constant) == ("MA0210", "mehrheit", "cOutMA0210")
assert schutz.bmk == "FC0210"
assert anlage.binding("TRO03", "drive", "senJam").status == "offen"
def test_foerderer_mit_zu_wenig_stimmen_bleibt_offen():
hw = hardware()
hw.devices = [d for d in hw.devices if d.bmk != "BG0802"]
k = next(c for c in run(hw=hw).conveyors if c.eplan_name == "K0106")
assert k.status == "offen" and "unsicher: 1 von 1" in k.note
# ---------------------------------------------------------------------------
# Handkorrekturen
# ---------------------------------------------------------------------------
def test_handkorrekturen(tmp_path):
ini = tmp_path / "hw_overrides.ini"
ini.write_text(
"[x.csv.bindings]\n"
"TRO06.separator1.sensor = BG0401 ; vom Nachbarn umhaengen\n"
"TRO06.separator1.stopper = MB4711\n"
"TRO02.separator1.stopper = -\n"
"TRO99.separator1.sensor = BG0101\n"
"[x.csv.conveyors]\n"
"K0102 = -\n"
"[andere.csv.bindings]\n"
"TRO01.separator1.sensor = BG0999\n", encoding="utf-8")
anlage = run(overrides=load_overrides(ini, "x.csv"))
moved = anlage.binding("TRO06", "separator1", "sensor")
assert (moved.bmk, moved.rule, moved.source, moved.status) == (
"BG0401", "hand", "hw_overrides.ini", "zugeordnet")
# automatisch vergeben war BG0401 an TRO04 - dort jetzt offen
assert anlage.binding("TRO04", "separator1", "sensor").status == "offen"
unknown = anlage.binding("TRO06", "separator1", "stopper")
assert (unknown.status, unknown.note) == ("widerspruch", "BMK unbekannt in der Hardwareplanung")
assert anlage.binding("TRO02", "separator1", "stopper").status == "offen"
assert any("TRO99.separator1" in w for w in anlage.warnings)
# andere Anlage nicht angewendet
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
k = next(c for c in anlage.conveyors if c.eplan_name == "K0102")
assert (k.status, k.rule, k.layout_id) == ("offen", "hand", "")
# ohne Foerderer kein Antrieb ueber K0102, der Vario-Antrieb bleibt
assert anlage.binding("TRO03", "drive", "motor").bmk == "MA0210"
def test_falscher_schluessel_in_handkorrektur(tmp_path):
ini = tmp_path / "hw_overrides.ini"
ini.write_text("[x.csv.bindings]\nTRO01.sensor = BG0101\n", encoding="utf-8")
with pytest.raises(ValueError, match="<TRO-ID>.<Slot>.<Rolle>"):
load_overrides(ini, "x.csv")
# ---------------------------------------------------------------------------
# Speicher: Linien-Slots
# ---------------------------------------------------------------------------
def test_speicher_bekommt_linien_slots_und_beide_slots_die_zuordnung():
store = _tro("TRO01", "PinStore_Auto", ["0001", "0002", "0003"], ["0016", "0017", "0018"])
assert [(s.slot, s.layout_id) for s in derive_slots(store)] == [
("separator1", "0001"), ("line1", "0001"), ("line2", "0002"), ("line3", "0003"),
("scanner1", "")]
doc = TroDocument.from_analysis(Analysis(tros=[store]), source="x.csv",
parts=[_part(i, "Separator", *SEPS[i]) for i in ("0001", "0002", "0003")])
anlage = run(doc=doc)
a = anlage.binding("TRO01", "separator1", "sensor")
b = anlage.binding("TRO01", "line1", "sensor")
assert a.bmk == b.bmk == "BG0101" and a.status == b.status == "zugeordnet"
assert anlage.binding("TRO01", "line2", "stopper").bmk == "MB0202"
# ---------------------------------------------------------------------------
# JSON und Lauf
# ---------------------------------------------------------------------------
def test_ohne_parts_kein_lauf():
doc = layout()
doc.parts = []
with pytest.raises(ValueError, match="parts"):
run(doc=doc)
def test_anlage_json_rundreise():
anlage = run()
again = AnlageDocument.from_json(anlage.to_json())
assert again == anlage
data = json.loads(anlage.to_json())
assert data["schema"] == "sps_skel-anlage-v1"
assert data["layout"]["schema"] == "sps_skel-tro-v4"
assert data["layout"]["parts"][0]["layoutId"] == "0001"
def test_gespeicherter_lagebezug_gilt_nur_fuer_denselben_dateistand(tmp_path):
t = Transform(dx=5.0, dy=6.0, csv_file="x.csv", dxf_file="E.json", fingerprint="aaa")
save_transform(tmp_path, t)
assert load_transform(tmp_path, "x.csv", "E.json", "aaa").dx == 5.0
assert load_transform(tmp_path, "x.csv", "E.json", "bbb") is None
# ohne Pruefsumme (tro_annotate) wie bisher ueber den Dateinamen
assert load_transform(tmp_path, "x.csv", "E.json").dx == 5.0
# alte Ablage ohne Pruefsumme gilt fuer einen Aufrufer mit Pruefsumme als veraltet
save_transform(tmp_path, Transform(dx=1.0, dy=1.0, csv_file="x.csv", dxf_file="Alt.json"))
assert load_transform(tmp_path, "x.csv", "Alt.json", "aaa") is None
def test_cli_mit_lagebezug(tmp_path, monkeypatch, capsys):
"""Ende zu Ende: E-Zeichnung um (100, 50) verschoben; Lagebezug wird
ermittelt, mit Pruefsumme abgelegt und beim zweiten Lauf wiederverwendet."""
results, cfg = tmp_path / "results", tmp_path / "cfg"
results.mkdir()
cfg.mkdir()
(cfg / "e_planning.ini").write_text(RULES.read_text(encoding="utf-8"), encoding="utf-8")
(results / "x_tro.json").write_text(layout().to_json(), encoding="utf-8")
hw = hardware(shift=(100.0, 50.0))
(results / "X_hw.json").write_text(hw.to_json(), encoding="utf-8")
monkeypatch.setenv("SKEL_RESULTS", str(results))
monkeypatch.setenv("SKEL_CFG", str(cfg))
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
first = capsys.readouterr().out
assert "Lagebezug neu ermittelt" in first
anlage = AnlageDocument.from_json((results / "x_anlage.json").read_text(encoding="utf-8"))
assert (anlage.registration.dx, anlage.registration.dy) == (100.0, 50.0)
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
assert (results / "x_anlage.md").is_file()
stored = json.loads((cfg / "dxf_registration.json").read_text(encoding="utf-8"))
assert stored["x.csv|X_positionsconv.json"]["fingerprint"] == "abc"
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
assert "Lagebezug neu ermittelt" not in capsys.readouterr().out
# neuer Stand der Positionsdatei, gleicher Name -> neu ermitteln
hw.positions_sha256 = "neu"
(results / "X_hw.json").write_text(hw.to_json(), encoding="utf-8")
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
assert "Lagebezug neu ermittelt" in capsys.readouterr().out
def test_cli_alte_tro_json_ohne_parts(tmp_path, monkeypatch, capsys):
results = tmp_path / "results"
results.mkdir()
doc = layout()
doc.parts = []
(results / "x_tro.json").write_text(doc.to_json(), encoding="utf-8")
(results / "X_hw.json").write_text(hardware().to_json(), encoding="utf-8")
monkeypatch.setenv("SKEL_RESULTS", str(results))
monkeypatch.setenv("SKEL_CFG", str(Path(RULES).parent))
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 1
assert "tro_flow --json neu erzeugen" in capsys.readouterr().err
+200
View File
@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/model.py - die TRO-JSON zwischen tro_flow und scl_skeleton."""
import json
import pytest
from pydantic import ValidationError
from model import SCHEMA, Analysis, Connection, DerivedTro, TroCandidate, TroDocument
from scl_skeleton import render
def _tro(tro_id: str, type_name: str, **extra) -> DerivedTro:
data = dict(
tro_id=tro_id,
type_name=type_name,
label=f"Label {tro_id}",
hosts=["0001"],
separators=["0040"],
scanners=[],
reason="Test",
confidence="mittel",
)
data.update(extra)
return DerivedTro(**data)
@pytest.fixture
def document() -> TroDocument:
"""Kleine Anlage: SYS1 -> TRO01 (Weiche) -> TRO02 / TRO03 -> SYS2."""
analysis = Analysis(
tros=[
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"],
position=(1030.0, 4700.0, 1500.0)),
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
successors=["SYS2"]),
_tro("TRO03", "Vario", separators=["0042"], predecessors=["TRO01"]),
],
boundaries=[
_tro("SYS1", "Materialeingang", separators=[], successors=["TRO01"]),
_tro("SYS2", "Materialausgang", separators=[], predecessors=["TRO02"]),
],
edges=[("SYS1", "TRO01"), ("TRO01", "TRO02"), ("TRO01", "TRO03"), ("TRO02", "SYS2")],
edge_kinds={("TRO01", "TRO02"): "weiche"},
findings=["ein Befund"],
)
return TroDocument.from_analysis(analysis, source="test.csv", note="Test")
def test_round_trip_is_lossless(document):
text = document.to_json()
again = TroDocument.from_json(text)
assert again.to_json() == text
assert again.by_id("TRO01").successors == ["TRO02", "TRO03"]
def test_json_uses_tro_json_keys(document):
data = json.loads(document.to_json())
assert data["schema"] == SCHEMA
first = data["tros"][0]
assert first["id"] == "TRO01"
assert first["type"] == "1Sep1Swi"
assert first["fbType"] == "FB_ILS_MTRO_1Sep1Swi"
assert first["items"] == {"Separator": 1, "Switch": 1}
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"}
assert data["candidates"] == []
def test_candidate_round_trip():
candidate = TroCandidate(name="Qualitaetskontrolle", type_name="1Sep1Swi",
position=(-1026.0, 5250.05, 1478.5), belongs_to="TRO09")
data = json.loads(candidate.model_dump_json(by_alias=True))
assert data["type"] == "1Sep1Swi"
assert data["position"] == {"x": -1026.0, "y": 5250.05, "z": 1478.5}
assert data["belongsTo"] == "TRO09"
assert TroCandidate.model_validate(data) == candidate
assert candidate.describe_position() == "-1026, 5250"
def test_items_and_fb_block_follow_the_type():
tro = _tro("TRO01", "1Sep")
assert tro.items == {"Separator": 1}
tro.type_name = "1Sep2Swi"
assert tro.items == {"Separator": 1, "Switch": 2, "Scanner": 1}
assert tro.fb_block == "FB_ILS_MTRO_1Sep2Swi"
def test_unknown_type_has_no_catalog_data():
station = _tro("SYS1", "Materialeingang")
assert station.definition() is None
assert station.items == {}
assert station.fb_block == ""
def test_catalog_fields_cannot_be_assigned():
tro = _tro("TRO01", "1Sep")
with pytest.raises((AttributeError, ValidationError)):
tro.fb_block = "FB_Anders"
def test_unknown_key_is_rejected(document):
data = json.loads(document.to_json())
data["tros"][0]["separatorz"] = data["tros"][0].pop("separators")
with pytest.raises(ValidationError, match="separatorz"):
TroDocument.model_validate(data)
def test_items_not_matching_catalog_are_rejected(document):
data = json.loads(document.to_json())
data["tros"][1]["items"] = {"Separator": 2}
with pytest.raises(ValidationError, match="passt nicht zum Typ"):
TroDocument.model_validate(data)
def test_fb_type_not_matching_catalog_is_rejected(document):
data = json.loads(document.to_json())
data["tros"][1]["fbType"] = "FB_ILS_MTRO_Vario"
with pytest.raises(ValidationError, match="fbType"):
TroDocument.model_validate(data)
def test_connection_accepts_alias_and_field_name():
assert Connection(**{"from": "A", "to": "B"}) == Connection(src="A", dst="B")
def test_scl_skeleton_renders_from_document(document):
scl = render(document, "test_tro.json")
assert 'TRO01 : "FB_ILS_MTRO_1Sep1Swi";' in scl
assert 'TRO03 : "FB_ILS_MTRO_Vario";' in scl
# Weiche: FC_Direction und eine Zielliste je Abgang
assert '"FC_Direction"' in scl
assert "// ueber TRO02: SYS2, TRO02" in scl
# Vario-TRO bekommt den Vario-Parameterblock
assert "#TRO03.stInVario.Settings" in scl
# Systemgrenzen sind keine Instanzen
assert "SYS1 :" not in scl
assert "Anfang bei TRO01" in scl
# ---------------------------------------------------------------------------
# Schema v4: Slots und Staubereiche
# ---------------------------------------------------------------------------
def test_slots_aus_katalog_und_layout(document):
weiche = document.by_id("TRO01")
assert [(s.slot, s.layout_id) for s in weiche.slots] == [
("separator1", "0040"), ("switch1", "")]
assert weiche.slots[0].roles == ["sensor", "stopper", "jamSensor?", "partSensor?"]
vario = document.by_id("TRO03")
assert [s.slot for s in vario.slots] == ["separator1", "drive"]
assert vario.slots[1].layout_id == "0001" # Host = angetriebene Strecke
# Systemgrenzen haben keine Slots
assert document.by_id("SYS1").slots == []
def test_ein_staubereich_je_tro_eingang_nach_separator(document):
areas = {a.jam_id: a for a in document.jam_areas}
assert sorted(areas) == ["J0040", "J0041", "J0042"]
assert areas["J0040"].tro == "TRO01"
assert areas["J0040"].predecessors == ["SYS1"]
assert areas["J0040"].position == (1030.0, 4700.0, 1500.0)
assert areas["J0041"].predecessors == ["TRO01"]
def test_zusammenfuehrung_ein_staubereich_mehrere_vorgaenger():
analysis = Analysis(tros=[
_tro("TRO01", "1Sep", separators=["0001"], successors=["TRO03"]),
_tro("TRO02", "1Sep", separators=["0002"], successors=["TRO03"]),
_tro("TRO03", "1Sep", separators=["0003"], predecessors=["TRO01", "TRO02"]),
])
doc = TroDocument.from_analysis(analysis, source="t")
eingang = [a for a in doc.jam_areas if a.tro == "TRO03"]
assert [(a.jam_id, a.predecessors) for a in eingang] == [("J0003", ["TRO01", "TRO02"])]
def test_zwei_separatoren_zwei_eingaenge_aufteilung_offen():
analysis = Analysis(tros=[
_tro("TRO01", "2Sep2Swi", separators=["0005", "0036"], predecessors=["TRO09"]),
])
areas = TroDocument.from_analysis(analysis, source="t").jam_areas
assert [(a.jam_id, a.entry, a.predecessors) for a in areas] == [
("J0005", 1, ["TRO09"]), ("J0036", 2, [])]
assert all("offen" in a.note for a in areas)
def test_v4_round_trip_und_v3_ohne_jam_areas(document):
data = json.loads(document.to_json())
assert data["schema"] == "sps_skel-tro-v4"
assert data["jamAreas"][0]["id"] == "J0040"
assert data["tros"][0]["slots"][0]["layoutId"] == "0040"
assert TroDocument.from_json(document.to_json()) == document
# v3-Datei: keine jamAreas/slots -> beim Rendern abgeleitet
del data["jamAreas"]
for tro in data["tros"]:
del tro["slots"]
data["schema"] = "sps_skel-tro-v3"
alt = TroDocument.from_json(json.dumps(data))
assert alt.jam_areas == []
assert [a.jam_id for a in alt.jam_areas_or_derived()] == ["J0040", "J0041", "J0042"]
+69
View File
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/parameter.py und cfg/parameter.ini."""
from __future__ import annotations
import configparser
import re
from pathlib import Path
import pytest
import parameter
from parameter import ParameterError, get_float, get_int
ROOT = Path(__file__).resolve().parent.parent
BASE = ROOT / "cfg" / parameter.PARAMETER_FILE
@pytest.fixture
def frisch(monkeypatch: pytest.MonkeyPatch):
"""Parameter-Cache leeren, damit SKEL_CFG im Test wirkt - und danach wieder."""
parameter._parser.cache_clear()
yield monkeypatch
parameter._parser.cache_clear()
def _base() -> configparser.ConfigParser:
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
parser.read(BASE, encoding="utf-8")
return parser
def test_base_values_are_read(frisch: pytest.MonkeyPatch) -> None:
frisch.delenv("SKEL_CFG", raising=False)
assert get_float("kreisel_junctions", "contact_tolerance") == 50.0
assert get_int("tro_flow", "min_store_lanes") == 2
def test_skel_cfg_overrides_single_value(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
(tmp_path / parameter.PARAMETER_FILE).write_text(
"[kreisel_junctions]\ncontact_tolerance = 75 ; Anlage X\n", encoding="utf-8")
frisch.setenv("SKEL_CFG", str(tmp_path))
assert get_float("kreisel_junctions", "contact_tolerance") == 75.0
# nicht ueberschriebene Werte kommen weiter aus der Grunddatei
assert get_float("kreisel_junctions", "separator_tolerance") == 100.0
def test_missing_key_is_an_error(frisch: pytest.MonkeyPatch) -> None:
frisch.delenv("SKEL_CFG", raising=False)
with pytest.raises(ParameterError, match=r"\[tro_flow\] gibt_es_nicht fehlt"):
get_float("tro_flow", "gibt_es_nicht")
def test_non_number_is_an_error(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
(tmp_path / parameter.PARAMETER_FILE).write_text(
"[tro_flow]\nmin_store_lanes = zwei\n", encoding="utf-8")
frisch.setenv("SKEL_CFG", str(tmp_path))
with pytest.raises(ParameterError, match="keine ganze Zahl"):
get_int("tro_flow", "min_store_lanes")
def test_every_key_is_used_by_its_module() -> None:
"""Jeder Schluessel der Grunddatei wird in lib/<Abschnitt>.py gelesen -
keine verwaisten Werte, die nichts mehr steuern."""
parser = _base()
for section in parser.sections():
source = (ROOT / "lib" / f"{section}.py").read_text(encoding="utf-8")
for key in parser.options(section):
assert re.search(rf'"{key}"', source), f"[{section}] {key} wird nicht gelesen"
+128
View File
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/scl_context.py - TroDocument -> Render-Kontext.
Die komplette Ausgabe sichern die Golden-Files (test_scl_skeleton_golden.py);
hier stehen die Ableitungen einzeln, auf die sich Templates (Schritt 3) und
Namensregeln (Schritt 4) aus doc/SCL_Templates.md verlassen.
"""
from pathlib import Path
import pytest
from model import Analysis, DerivedTro, TroDocument
from scl_context import SclName, build_context, flow_order
from scl_skeleton import load_document
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
def _tro(tro_id: str, type_name: str, **extra) -> DerivedTro:
data = dict(
tro_id=tro_id,
type_name=type_name,
label=f"Label {tro_id}",
hosts=["0001"],
separators=["0040"],
scanners=[],
reason="Test",
)
data.update(extra)
return DerivedTro(**data)
@pytest.fixture
def kontext():
"""SYS1 -> TRO01 (1 Weiche) -> TRO02 -> SYS2 und TRO01 -> TRO03 (Vario)."""
analysis = Analysis(tros=[
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"]),
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
successors=["SYS2"]),
_tro("TRO03", "Vario", separators=[], predecessors=["TRO01"]),
])
document = TroDocument.from_analysis(analysis, source="test.csv")
return build_context(document, "test_tro.json", created="2026-01-01 00:00")
def _by_id(context, tro_id):
return next(t for t in context.tros if t.id == tro_id)
def test_reihenfolge_und_startgrund(kontext):
assert [t.id for t in kontext.tros] == ["TRO01", "TRO02", "TRO03"]
assert kontext.start_reason == "Materialeingang ueber SYS1"
assert kontext.created == "2026-01-01 00:00"
def test_nummernteil_wie_geschrieben():
"""{nr} fuer die Namensregeln: fuehrende Nullen bleiben stehen."""
analysis = Analysis(tros=[_tro("TRO07", "1Sep"), _tro("TRO107", "1Sep"),
_tro("XY", "1Sep")])
ctx = build_context(TroDocument.from_analysis(analysis, source="t"), "t")
assert {t.id: t.nr for t in ctx.tros} == {"TRO07": "07", "TRO107": "107", "XY": ""}
def test_staubereiche_nummerierung(kontext):
"""Eingaenge ab 1, Ausgaenge ab 2 - wie in den Referenz-Aufrufstellen."""
tro = _by_id(kontext, "TRO01")
assert [(j.n, j.neighbour) for j in tro.jam_entries] == [
(1, "SYS1 (Station, kein TRO)")]
assert [(j.n, j.neighbour) for j in tro.jam_exits] == [(2, "TRO02"), (3, "TRO03")]
def test_eine_weiche_bedient_alle_abgaenge(kontext):
tro = _by_id(kontext, "TRO01")
assert len(tro.switches) == 1
weiche = tro.switches[0]
assert weiche.to1_destinations == 2
assert weiche.direction_target == "#TRO01.nInDirection"
assert [(e.via, e.targets) for e in tro.switch_exits] == [
("TRO02", ["SYS2", "TRO02"]), ("TRO03", ["TRO03"])]
def test_mehrere_weichen_keine_geratene_aufteilung():
"""2Sep2Swi (mubea): nTo1Destinations bleibt offen, Richtung je Weiche."""
ctx = build_context(load_document(FIXTURES / "mubea_tro.json"), "mubea_tro.json")
tro = next(t for t in ctx.tros if t.type == "2Sep2Swi")
assert [w.to1_destinations for w in tro.switches] == [None, None]
assert [w.direction_target for w in tro.switches] == [
f"#{tro.id}.nInDirection1", f"#{tro.id}.nInDirection2"]
def test_separatoren_aus_dem_layout(kontext):
"""Mehr Separatoren laut Typ als im Layout -> '?' statt Fehler."""
assert [s.layout for s in _by_id(kontext, "TRO01").separators] == ["0040"]
assert [s.layout for s in _by_id(kontext, "TRO03").separators] == ["?"]
def test_vario_nur_beim_vario_typ(kontext):
vario = _by_id(kontext, "TRO03")
assert vario.has_vario
assert list(vario.vario_sensors) == [
"stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam"]
assert _by_id(kontext, "TRO02").vario_sensors == {}
def test_heute_sind_alle_namen_offen(kontext):
"""Bis zu den Namensregeln (Schritt 4) ist jeder Name ein TODO-Platzhalter."""
namen: list[SclName] = []
for tro in kontext.tros:
namen += [tro.main_tro, tro.safety, tro.motor_protection, tro.vario_motor,
tro.machine_state, tro.priority_udt, tro.prio_dir2, tro.carousel_run]
namen += [j.sensor for j in tro.jam_exits]
assert all(j.sensor is None for j in tro.jam_entries)
namen += [n for s in tro.separators for n in (s.no, s.sensor, s.stopper)]
namen += [n for w in tro.switches for n in (w.no, w.valve, w.jam_db)]
namen += [j.db for j in tro.jam_entries + tro.jam_exits]
namen += list(tro.vario_sensors.values())
assert namen and all(n.todo for n in namen)
def test_flow_order_kreislauf_ohne_eingang():
analysis = Analysis(tros=[
_tro("TRO02", "1Sep", predecessors=["TRO01"], successors=["TRO01"]),
_tro("TRO01", "1Sep", predecessors=["TRO02"], successors=["TRO02"]),
])
geordnet, grund = flow_order(analysis.tros)
assert [t.tro_id for t in geordnet] == ["TRO01", "TRO02"]
assert grund.startswith("Kreislauf ohne Eingang")
+130
View File
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""Golden-File-Tests fuer scl_skeleton.py - die komplette FB_Main-Ausgabe.
Schritt 1 aus doc/SCL_Templates.md (§8.3, §11): die heutige Ausgabe wird
festgeschrieben, bevor der Generator auf Templates umgebaut wird. Solange kein
Template eingeschaltet ist, muss jede SCL byte-gleich zur Referenz bleiben.
Eingaben: tests/fixtures/scl/<name>_tro.json - feste Kopien der TRO-JSON aus
tro_flow --json, damit nur scl_skeleton geprueft wird und nicht tro_flow.
Erwartung: tests/fixtures/scl/<name>_FB_Main.scl, Zeitstempel maskiert.
Eine gewollte Aenderung der Ausgabe (z. B. ein Template wird eingeschaltet):
Diff pruefen, dann die Referenzen neu schreiben mit
set SKEL_UPDATE_GOLDEN=1 & python -m pytest tests/test_scl_skeleton_golden.py
"""
import os
import re
from pathlib import Path
import pytest
import scl_skeleton
from scl_skeleton import TEMPLATE_DIR, SclTemplates, load_document, render
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
# mubea: alle fuenf dort vorkommenden Typen, auch 2Sep2Swi (ohne Template);
# HundM05: die Referenzanlage der TRO-Templates.
PROJEKTE = ["mubea", "HundM05"]
# Die einzige Zeile, die sich von Lauf zu Lauf aendert.
_ZEITSTEMPEL = re.compile(r"^// Erzeugt: .*$", re.MULTILINE)
_MASKE = "// Erzeugt: <maskiert>"
def _maskiert(text: str) -> str:
return _ZEITSTEMPEL.sub(_MASKE, text)
def _aktualisieren() -> bool:
return os.environ.get("SKEL_UPDATE_GOLDEN") == "1"
def _vergleichen(ist: str, referenz: Path) -> None:
if _aktualisieren():
referenz.write_text(ist, encoding="utf-8", newline="\n")
pytest.skip(f"Referenz neu geschrieben: {referenz.name}")
assert referenz.is_file(), (
f"{referenz.name} fehlt - mit SKEL_UPDATE_GOLDEN=1 erzeugen")
soll = referenz.read_text(encoding="utf-8")
if ist != soll:
# Erste abweichende Zeile nennen; der volle Diff ist bei ~900 Zeilen
# in der pytest-Ausgabe nicht lesbar.
ist_z, soll_z = ist.splitlines(), soll.splitlines()
for nr, (a, b) in enumerate(zip(ist_z, soll_z), start=1):
if a != b:
pytest.fail(f"{referenz.name} Zeile {nr}:\n soll: {b}\n ist : {a}")
pytest.fail(f"{referenz.name}: {len(ist_z)} statt {len(soll_z)} Zeilen")
@pytest.mark.parametrize("name", PROJEKTE)
def test_render_wie_referenz(name: str) -> None:
"""render() liefert byte-gleich die festgeschriebene SCL."""
quelle = f"{name}_tro.json"
document = load_document(FIXTURES / quelle)
_vergleichen(_maskiert(render(document, quelle)), FIXTURES / f"{name}_FB_Main.scl")
@pytest.mark.parametrize("name", PROJEKTE)
def test_render_mit_templates_wie_referenz(name: str) -> None:
"""--templates: TROs mit Template aus dem Template, der Rest generisch.
Eigene Referenz <name>_FB_Main_templates.scl. Eine Aenderung an einem
Template zeigt sich hier als Diff - pruefen, dann neu schreiben.
"""
quelle = f"{name}_tro.json"
document = load_document(FIXTURES / quelle)
text = render(document, quelle, templates=SclTemplates(TEMPLATE_DIR))
_vergleichen(_maskiert(text), FIXTURES / f"{name}_FB_Main_templates.scl")
@pytest.mark.parametrize("name", PROJEKTE)
def test_cli_schreibt_referenz(name: str, tmp_path: Path, monkeypatch) -> None:
"""Der CLI-Weg: Exit 0, Dateiname <name>_FB_Main.scl, Inhalt wie render().
Das ist der Vertrag, auf den sich ein Workflow verlaesst (doc/SCL_Templates.md §12).
"""
if _aktualisieren():
pytest.skip("Referenzen werden nur ueber test_render_wie_referenz geschrieben")
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
exit_code = scl_skeleton.main(["--json", str(FIXTURES / f"{name}_tro.json")])
assert exit_code == 0
ausgabe = tmp_path / f"{name}_FB_Main.scl"
assert ausgabe.is_file()
soll = (FIXTURES / f"{name}_FB_Main.scl").read_text(encoding="utf-8")
assert _maskiert(ausgabe.read_text(encoding="utf-8")) == soll
def test_cli_templates_und_strict(tmp_path: Path, monkeypatch) -> None:
"""--templates schreibt die Template-Referenz; --strict bricht ab, solange
ein FB-Typ der Anlage (mubea: u. a. 2Sep2Swi) kein Template hat."""
if _aktualisieren():
pytest.skip("Referenzen werden nur ueber die render-Tests geschrieben")
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
json_file = str(FIXTURES / "mubea_tro.json")
assert scl_skeleton.main(["--json", json_file, "--templates"]) == 0
soll = (FIXTURES / "mubea_FB_Main_templates.scl").read_text(encoding="utf-8")
ist = (tmp_path / "mubea_FB_Main.scl").read_text(encoding="utf-8")
assert _maskiert(ist) == soll
(tmp_path / "mubea_FB_Main.scl").unlink()
assert scl_skeleton.main(["--json", json_file, "--templates", "--strict"]) == 1
assert not (tmp_path / "mubea_FB_Main.scl").exists()
assert scl_skeleton.main(["--json", json_file, "--templates",
str(tmp_path / "gibt_es_nicht")]) == 1
def test_todo_marker_bleiben_zaehlbar() -> None:
"""Die Zahl der offenen Stellen ist eine Qualitaetsgroesse im Workflow.
Sie darf sich nur mit Absicht aendern (dann Referenz neu schreiben).
"""
for name in PROJEKTE:
text = (FIXTURES / f"{name}_FB_Main.scl").read_text(encoding="utf-8")
assert text.count(scl_skeleton.TODO) > 0, name
+352
View File
@@ -0,0 +1,352 @@
# -*- coding: utf-8 -*-
"""Rundlauf der FB-Templates gegen die realen Aufrufstellen (doc/SCL_Templates.md §8.1).
Je Template: Kontext mit den ECHTEN Werten der Referenz-Aufrufstelle bauen
(TRO-Nummer, Sensoren, Ventile, Staubereiche ...), Template rendern und mit dem
REGION-Block der Referenz in doc/TRO_Katalog/scl_templates/ vergleichen.
Verglichen wird normalisiert: Kommentare und Leerzeilen weg, Leerraum
zusammengefasst. Stimmt das, hat die Parametrisierung nichts verloren -
gleiche Zeilen, gleiche Reihenfolge, gleiche Werte.
Wird eine Referenzdatei durch eine neuere Aufrufstelle ersetzt, zeigt dieser
Test genau die Zeilen, denen das Template folgen muss.
"""
import re
from pathlib import Path
import pytest
from model import Analysis, DerivedTro, TroDocument
from scl_context import SclName, SclTro, build_context
from scl_skeleton import TEMPLATE_DIR, SclTemplates, load_document, render
ROOT = Path(__file__).resolve().parent.parent
REFERENZ = ROOT / "doc" / "TRO_Katalog" / "scl_templates"
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
def _name(wert: str) -> SclName:
"""Ein bekannter Name aus der Referenz - also nicht mehr offen."""
return SclName(value=wert, todo=False)
def _normalisiert(text: str) -> list[str]:
zeilen = []
for zeile in text.splitlines():
zeile = re.sub(r"//.*$", "", zeile) # Kommentar
zeile = re.sub(r"\s+", " ", zeile).strip() # Leerraum
# "a:=b" und "a := b" sind dasselbe SCL (Referenzen schreiben beides)
zeile = re.sub(r"\s*(:=|=>)\s*", r" \1 ", zeile)
if zeile:
zeilen.append(zeile)
return zeilen
def _referenz_region(datei: str) -> list[str]:
"""Vom ersten 'REGION' bis zum letzten 'END_REGION' der Referenzdatei."""
zeilen = (REFERENZ / datei).read_text(encoding="utf-8-sig").splitlines()
anfang = next(i for i, z in enumerate(zeilen) if z.startswith("REGION "))
ende = max(i for i, z in enumerate(zeilen) if z.strip() == "END_REGION")
return _normalisiert("\n".join(zeilen[anfang:ende + 1]))
def _jam(jam, db: str) -> None:
"""Staubereich mit bekanntem DB (Traeger und Daten wie ueblich darin)."""
jam.db = _name(db)
jam.carriers = _name(f"{db}.arCarrier")
jam.data = _name(f"{db}.stData")
def _scanner(sc, interface: str, tag: str, hw_results: str) -> None:
sc.interface = _name(interface)
sc.tag = _name(tag)
sc.hw_results = _name(hw_results)
def _kontext(tro_id: str, type_name: str, vorgaenger: list[str],
nachfolger: list[str], separators: list[str] | None = None,
hosts: list[str] | None = None) -> SclTro:
"""Kontext eines TRO mit der Topologie der Referenzstelle."""
tro = DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id,
hosts=hosts or [], separators=separators or ["0001"],
scanners=[], reason="Referenz",
predecessors=vorgaenger, successors=nachfolger)
document = TroDocument.from_analysis(Analysis(tros=[tro]), source="referenz")
return build_context(document, "referenz").tros[0]
def _vergleich(ist: list[str], soll: list[str]) -> None:
for nr, (a, b) in enumerate(zip(ist, soll), start=1):
assert a == b, f"Zeile {nr}:\n Referenz: {b}\n Template: {a}"
assert len(ist) == len(soll), (
f"{len(ist)} statt {len(soll)} Zeilen; zuviel/fehlend: "
f"{ist[len(soll):] or soll[len(ist):]}")
@pytest.fixture(scope="module")
def templates() -> SclTemplates:
return SclTemplates(TEMPLATE_DIR)
def test_1sep1swi_wie_tro107(templates):
"""FB_ILS_MTRO_1Sep1Swi gegen TRO 107 (=A01+UH01-KF00 FB_Main)."""
t = _kontext("TRO107", "1Sep1Swi", ["TRO106"], ["TRO108", "TRO110"])
t.region_title = "TRO 107(1Sep1Swi)"
sep = t.separators[0]
sep.no = _name("cSep107.1")
sep.sensor = _name("cInBG3306")
sep.stopper = _name("MB3306")
t.jam_exits[0].sensor = _name("cInBG3305")
t.jam_exits[1].sensor = _name("cInBG3330")
weiche = t.switches[0]
weiche.no = _name("cSwi107.1")
weiche.valve = _name("MB3304")
weiche.to1_destinations = 3 # Referenz: 3 Ziele bei 2 Abgaengen (§13)
weiche.jam_db = _name('"DB_JamArea0102".stJam0102_1')
t.prio_dir2 = _name("TRUE")
t.priority_udt = _name("udt_TRO107")
t.main_tro = _name("cMainTro107")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("#fbCarousel0102.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
t.jam_entries[0].db = _name('"DB_JamArea0102".stJam0102_1')
t.jam_exits[0].db = _name('"DB_JamArea0103".stJam0103_1')
t.jam_exits[1].db = _name('"DB_JamArea0102".stJam0102_2')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep1Swi.scl"))
def test_1sep_wie_tro100(templates):
"""FB_ILS_MTRO_1Sep gegen TRO 100 (=A01+UH01-KF00 FB_Main)."""
t = _kontext("TRO100", "1Sep", ["TRO099"], ["TRO101"])
t.region_title = "TRO 100 (1Sep)"
sep = t.separators[0]
sep.no = _name("cSep100.1")
sep.sensor = _name("cInBG3242")
sep.stopper = _name("MB3242")
t.jam_exits[0].sensor = _name("cInBG3243")
t.main_tro = _name("cMainTro100")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("#fbCarousel0101.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea0100".stJam0100_1')
_jam(t.jam_exits[0], '"DB_JamArea0101".stJam0101_1')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep.scl"))
def test_vario_wie_tro115(templates):
"""FB_ILS_MTRO_Vario gegen TRO 115."""
t = _kontext("TRO115", "Vario", ["TRO114"], ["TRO116"])
t.region_title = "TRO 115 (Vario)"
sep = t.separators[0]
sep.no = _name("cSep115.1")
sep.sensor = _name("cInBG3025")
sep.stopper = _name("MB3025")
for name, wert in (("stSenCarrInPos", "cInBG3026"), ("stSenFinger", "cInBG3027"),
("stSenLastPos", "cInBG3000"), ("stSenJam", "cInBG3001")):
t.vario_sensors[name] = _name(wert)
t.is_border = _name("TRUE")
t.main_tro = _name("cMainTro115")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.motor_protection = _name('"FC0145"')
t.error_quit = _name('"SF0002"')
t.automatic = _name('#t_Automatik_OF1 AND "SF0120"')
_jam(t.jam_in, '"DB_JamArea4104".stJam4104_1')
_jam(t.jam_out, '"DB_JamArea4103".stJam4103_1')
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
t.vario_motor = _name("MA0145")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario.scl"))
def test_vario_workstation_wie_tro421(templates):
"""FB_ILS_MTRO_Vario_workStation gegen die Aufrufstelle TRO 421 (UH04)."""
t = _kontext("TRO421", "Vario_workStation", ["TRO420"], ["TRO422"])
t.region_title = "TRO 421 (Vario)"
sep = t.separators[0]
sep.no = _name("cSep421.1")
sep.sensor = _name("cInBG3326")
sep.stopper = _name("MB3326")
for name, wert in (("stSenFinger", "cInBG3315"), ("stSenLastPos", "cInBG3317"),
("stSenCarrInPos", "cInBG3324"), ("stSenJam", "cInBG3320")):
t.vario_sensors[name] = _name(wert)
t.work_station.loaded_error_sensor = _name("cInBG3325")
t.work_station.station_sensor = _name("cInBG3314")
t.work_station.station_release = _name('#"rTrig_Workstation 1".CLK')
t.main_tro = _name("cMainTro421")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMain.Q')
t.automatic = _name("#t_automatik")
t.release = '"DB_TRO_Test".TRO421'
t.motor_protection = _name('"FC0062"')
_jam(t.jam_in, '"DB_JamArea4004".stJam4004_2')
_jam(t.jam_out, '"DB_JamArea4005".stJam4005_1')
t.machine_state = _name("#stInOutControlUnitCabinet")
t.vario_motor = _name("MA0062")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario_workStation_CallSite.scl"))
def test_1sep2swi_wie_tro267(templates):
"""FB_ILS_MTRO_1Sep2Swi gegen TRO 267 (Scanner BX2100, 3 Abgaenge)."""
t = _kontext("TRO267", "1Sep2Swi", ["TRO266"], ["TRO268", "TRO269", "TRO270"])
t.region_title = "TRO 267 (1Sep2Switch)+Scanner(BX2100)"
sep = t.separators[0]
sep.no = _name("cSep267.1")
sep.sensor = _name("cInBG4440")
sep.part_sensor = _name("cInBG4465")
sep.stopper = _name("MB4440")
for jam, sensor in zip(t.jam_exits, ("cInBG4444", "cInBG4455", "cInBG4454")):
jam.sensor = _name(sensor)
for weiche, ventil in zip(t.switches, ("MB4444", "MB4455")):
weiche.valve = _name(ventil)
weiche.to1_destinations = 3 # Referenz: 3 je Weiche
t.switches[0].no = _name("cSwi267.1")
_scanner(t.scanners[0], "cScannerBX2100", "bx2100",
"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1")
t.main_tro = _name("cMainTro267")
t.safety = _name('"cTodoTRUE"')
t.release = '"DB_TRO_Test".TRO267'
t.carousel_run = _name("#fbCarousel1110.xOutConveyorFullSpeed")
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea1110".stJam1110_4')
for jam, nr in zip(t.jam_exits[:2], ("5001", "5000")): # Uebergaben
jam.carriers = _name(f"#arCarrierHandoverJam{nr}")
jam.data = _name(f"#stJamDataHandoverJam{nr}")
_jam(t.jam_exits[2], '"DB_JamArea1110".stJam1110_5')
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep2Swi.scl"))
def test_1sep_sscc_wie_tro105(templates):
"""FB_ILS_MTRO_1Sep_SSCC gegen TRO 105 (Scanner BX1104, SSCC-Scanner BX1107)."""
t = _kontext("TRO105", "1Sep_SSCC", ["TRO104"], ["TRO106"])
t.region_title = "TRO 105 (1Sep)+Scanner"
sep = t.separators[0]
sep.no = _name("cSep105.1")
sep.sensor = _name("cInBG3280")
sep.stopper = _name("MB3280")
t.jam_exits[0].sensor = _name("cInBG3281")
_scanner(t.scanners[0], "cScannerBX1104", "bx1104",
"BX1104~Ergebnisse_–_64_Bytes_1")
_scanner(t.sscc.scanner, "#stCognexIntfTro105", "bx1107",
"BX1107~Ergebnisse_–_64_Bytes_1")
t.sscc.end_sensor = _name("cInBG3281")
t.sscc.hmi = _name("SSCC1107")
t.main_tro = _name("cMainTro105")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
t.carousel_run = _name("TRUE")
t.release = ('"FB_CommunicationWCSBL_DB".fbTeleHandler.xOutConnectionActive '
'OR "IBN".xFahrenOhneTcpPartner')
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
_jam(t.jam_entries[0], '"DB_JamArea0104".stJam0104_3')
_jam(t.jam_exits[0], '"DB_JamArea0106".stJam0106_2')
t.correction_station = _name("0")
ist = _normalisiert(templates.render(t))
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep_SSCC.scl"))
def test_pinstore_auto_wie_tro208(templates):
"""FB_ILS_MTRO_PinStore_Auto gegen BLOCK 1.1.1 (TRO 208).
Die Referenz hat 19 Linien, schreibt aber nur die Linien 1-3 aus (bei
"Separator Store Out" nur Linie 1, "weitere analog"). Der Kontext hat
deshalb 19 Linien; was die Referenz abkuerzt, wird vor dem Vergleich aus
der Template-Ausgabe genommen.
"""
linien = [f"{n:04d}" for n in range(1, 20)]
t = _kontext("TRO208", "PinStore_Auto", ["TRO207"], ["TRO209"],
separators=linien, hosts=linien)
t.region_title = "BLOCK 1.1.1 (TRO 208)"
st = t.store
st.encoder = _name("#fbEncoderGF2200")
st.dest = _name("cDest11100")
st.conv_stations = _name("cHooksSorterMaxPin111")
st.stations_max = _name("cStationsMax111")
st.threshold = _name("cNoPositionsBlock1")
st.reference_sensor = _name("cInBG3001")
st.station = _name("111")
st.quit_local = _name('"SF0121" OR "SF0117"')
st.test_target = _name('"IBN".nTestTarget111')
st.run_in = _name("#fbSortConv1101.xOutConveyorFullSpeed")
st.run_out = _name("#fbExitConv1103.xOutConveyorFullSpeed")
st.db = _name('"DB_Storage1.1.1"')
st.removal = _name('"DB_HandshakeStorage".Str_StorageRemoval.'
'arrudt_StorageRemovalHandshakeTROsLvl1[1]')
st.conv_db = _name('"DB_ConveyorStations".stConveyorStations111')
st.pin_sensor = _name("cInBG3000")
st.pin_stopper = _name("MB3000")
for linie, (laenge, sensor, stau, stopper) in zip(st.lines, (
("4", "cInBG3270", "cInBG3030", "MB3270"),
('"cStorage111ExitJam2"', "cInBG3271", "cInBG3031", "MB3271"),
('"cStorage111ExitJam3"', "cInBG3272", "cInBG3032", "MB3272"))):
linie.jam_length = _name(laenge)
linie.sensor = _name(sensor)
linie.jam_sensor = _name(stau)
linie.stopper = _name(stopper)
_scanner(t.scanners[0], "cScannerBX2104", "bx2104",
"=A01+UH02-BX2104~Ergebnisse_–_64_Bytes_1")
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF2.Q')
t.release = '"DB_TRO_Test".TRO208'
t.machine_state = _name("#stInOutControlUnitCabinetOF2")
_jam(t.jam_in, '"DB_JamArea1101".stJam1101_2')
_jam(t.jam_out, '"DB_JamArea1103".stJam1103_1')
t.correction_station = _name("1")
ist, abgekuerzt = [], False
for zeile in _normalisiert(templates.render(t)):
treffer = re.fullmatch(r"REGION Sep Store (\d+)", zeile)
if treffer:
abgekuerzt = int(treffer.group(1)) > 3
if abgekuerzt: # REGION Sep Store 4..19
abgekuerzt = zeile != "END_REGION"
continue
if re.search(r"arInJamSensor\[(\d+)\]", zeile) and \
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 3:
continue
if re.search(r"arOutSeparatorStore\[(\d+)\]", zeile) and \
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 1:
continue
ist.append(zeile)
_vergleich(ist, _referenz_region("FB_ILS_MTRO_PinStore_Auto.scl"))
def test_template_ohne_bekannte_namen_hat_nur_markierte_luecken(templates):
"""Mit den heutigen Platzhaltern: jede offene Stelle traegt den TODO-Marker."""
t = _kontext("TRO07", "1Sep1Swi", ["SYS1"], ["TRO08", "TRO09"])
text = templates.render(t)
for zeile in text.splitlines():
if "____" in zeile.split("//")[0]:
assert "TODO(E-Planung)" in zeile, zeile
@pytest.mark.parametrize("name", ["mubea", "HundM05"])
def test_jedes_template_rendert_fuer_echte_anlagen(name, templates):
"""StrictUndefined: ein Tippfehler im Template faellt hier auf, nicht erst beim Kunden."""
document = load_document(FIXTURES / f"{name}_tro.json")
text = render(document, f"{name}_tro.json", templates=templates)
regionen = [z for z in text.splitlines() if z.startswith("REGION ")]
assert len(regionen) == len(document.tros)
kopf = next(z for z in text.splitlines() if z.startswith("// Aus Template :"))
for fb in {t.fb_block for t in document.tros if templates.has(t.fb_block)}:
assert fb in kopf
def test_strikter_undefined_bricht_ab(tmp_path):
"""Unbekannte Variable im Template -> Fehler statt leerer SCL-Stelle."""
(tmp_path / "FB_X.scl.j2").write_text("REGION {{ t.gibt_es_nicht }}\n", encoding="utf-8")
t = _kontext("TRO01", "1Sep", [], [])
t.fb = "FB_X"
with pytest.raises(Exception):
SclTemplates(tmp_path).render(t)
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""Tests fuer die Mouseover-Texte des --track-Graphen (material_flow.py)."""
from __future__ import annotations
from material_flow import TrackNode, _tooltip_context, _track_node_tooltip
from test_kreisel_junctions import KREISEL, SEPARATOREN, WEICHEN, _el, _kreisel
def _anlage() -> list:
kreisel = [_kreisel(*k) for k in KREISEL]
for k in kreisel:
k.name = f"Kreisel{int(k.teile_id)}" # wie im Export
kreisel[2].track_ids = ["0015"] # Kreisel 0003 laut Export
kreisel[4].track_ids = ["0025", "0026", "0020"] # Kreisel 0005
seps = []
for teile_id, pos in SEPARATOREN:
zuordnung = {"0015": "0003-L", "0010": "0009-L"}.get(teile_id, "")
seps.append(_el(teile_id, "Separator", pos, Zuordnung=zuordnung))
weichen = [_el(i, "Weiche", p) for i, p in WEICHEN]
weichen[0].merkmale.update({"Kreisel A": "0009", "Kreisel B": "0001"})
beladung = _el("0020", "Beladung", (14374.25, 1186.94))
return kreisel + seps + weichen + [beladung]
def _tooltip(elements: list, teile_id: str, kind: str) -> list[str]:
ctx = _tooltip_context(elements)
el = next(e for e in elements if e.teile_id == teile_id)
return _track_node_tooltip(TrackNode(teile_id, kind, el), ctx).split("\\n")
def test_separator_shows_zuordnung_kette_and_geometry_conflict() -> None:
lines = _tooltip(_anlage(), "0015", "Separator")
assert "Zuordnung: Kreisel3 (0003) Bahn L" in lines
assert "TrackIds-Kette: Kreisel3 (0003)" in lines
assert "Geometrie: liegt auf Kreisel2 (0002) Bahn R" in lines
assert "! Zuordnung Kreisel 0003, liegt aber auf Kreisel 0002" in lines
def test_separator_without_conflict_has_no_warning() -> None:
lines = _tooltip(_anlage(), "0010", "Separator")
assert "Geometrie: liegt auf Kreisel9 (0009) Bahn L" in lines
assert not any(line.startswith("! Zuordnung") for line in lines)
def test_weiche_shows_both_kreisel() -> None:
lines = _tooltip(_anlage(), "0022", "Weiche")
assert "Kreisel A: Kreisel9 (0009)" in lines
assert "Kreisel B: Kreisel1 (0001)" in lines
assert "Geometrie: Beruehrpunkt Kreisel 0001 / 0009" in lines
def test_beladung_shows_its_kreisel() -> None:
lines = _tooltip(_anlage(), "0020", "Beladung")
assert "TrackIds-Kette: Kreisel5 (0005)" in lines
assert lines[-1].startswith("Geometrie: naechster Kreisel Kreisel5 (0005)")
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""Tests fuer lib/tro_ids.py: ID-Datei lesen/schreiben, TROs ueber die Lage
wiederfinden (doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs)."""
from __future__ import annotations
from dataclasses import dataclass, field
import pytest
from tro_ids import (
IdEntry,
TroIds,
assign_ids,
insert_entries,
load_ids,
other_sections,
render_ids,
render_new_entries,
)
@dataclass
class FakeTro:
separators: list[str]
type_name: str = "1Sep"
label: str = ""
tro_id: str = ""
hosts: list[str] = field(default_factory=list)
ANCHORS = {
"0001": (0.0, 0.0),
"0002": (1000.0, 0.0),
"0003": (2000.0, 0.0),
"0004": (2000.0, 500.0),
}
def _tros(*seps: list[str]) -> list[FakeTro]:
return [FakeTro(separators=list(s)) for s in seps]
def test_ohne_eintraege_wird_durchnummeriert():
tros = _tros(["0001"], ["0002"], ["0003"])
result = assign_ids(tros, ANCHORS, [])
assert result.renumbered
assert [t.tro_id for t in tros] == ["TRO01", "TRO02", "TRO03"]
def test_wiederfinden_ueber_lage_auch_umbenannt():
# Neuer Export: andere TeileIds, gleiche Lage, andere Reihenfolge
anchors = {"0101": (2000.0, 0.0), "0102": (0.0, 0.0), "0103": (1000.0, 30.0)}
tros = _tros(["0101"], ["0102"], ["0103"])
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("K0102_1", 1000.0, 0.0),
IdEntry("TRO03", 2000.0, 0.0)]
result = assign_ids(tros, anchors, entries)
assert [t.tro_id for t in tros] == ["TRO03", "TRO01", "K0102_1"]
assert not result.new and not result.missing
def test_neue_tros_naechste_freie_nummer_weggefallene_nicht_wiederverwendet():
tros = _tros(["0001"], ["0003"])
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("TRO02", 1000.0, 0.0)]
result = assign_ids(tros, ANCHORS, entries)
assert [t.tro_id for t in tros] == ["TRO01", "TRO03"]
assert result.new == ["TRO03"]
assert [e.tro_id for e in result.missing] == ["TRO02"]
def test_ausserhalb_toleranz_gilt_als_neu():
tros = _tros(["0001"])
entries = [IdEntry("TRO01", 150.0, 0.0)]
result = assign_ids(tros, ANCHORS, entries, tolerance=100.0)
assert tros[0].tro_id == "TRO02"
assert result.missing[0].tro_id == "TRO01"
assert assign_ids(tros, ANCHORS, entries, tolerance=200.0).kept[0][0] == "TRO01"
def test_zusammengelegte_tros_naechster_eintrag_gewinnt():
# zwei Separatoren, frueher zwei TROs, jetzt ein 2Sep-TRO
tros = _tros(["0003", "0004"])
entries = [IdEntry("TRO05", 2000.0, 0.0), IdEntry("TRO06", 2000.0, 510.0)]
result = assign_ids(tros, ANCHORS, entries)
assert tros[0].tro_id == "TRO05"
assert result.merged_into == {"TRO06": "TRO05"}
def test_renumber_ignoriert_eintraege():
tros = _tros(["0001"], ["0002"])
result = assign_ids(tros, ANCHORS, [IdEntry("X", 1000.0, 0.0)], renumber=True)
assert result.renumbered
assert [t.tro_id for t in tros] == ["TRO01", "TRO02"]
def test_datei_rundlauf_und_anhaengen(tmp_path):
path = tmp_path / "anlage_tro_ids.ini"
tros = _tros(["0001"], ["0002"])
assign_ids(tros, ANCHORS, [])
path.write_text(render_ids(tros, ANCHORS, "anlage.csv")
+ "\n[separator]\n; spaeter\n", encoding="utf-8")
entries = load_ids(path)
assert [(e.tro_id, e.x, e.y) for e in entries] == [
("TRO01", 0.0, 0.0), ("TRO02", 1000.0, 0.0)]
tros = _tros(["0001"], ["0002"], ["0003"])
result = assign_ids(tros, ANCHORS, entries)
insert_entries(path, render_new_entries(tros, ANCHORS, result.new, "anlage.csv"))
assert [e.tro_id for e in load_ids(path)] == ["TRO01", "TRO02", "TRO03"]
# neuer Eintrag steht in [tro], nicht im folgenden Abschnitt
assert other_sections(path) == "[separator]\n; spaeter"
assert TroIds.load(path).exists
@pytest.mark.parametrize("inhalt, meldung", [
("[tro]\nTRO01 = 1, 2\nTRO01 = 3, 4\n", "already exists"),
("[tro]\nSYS1 = 1, 2\n", "reserviert"),
("[tro]\nTRO-1 = 1, 2\n", "SCL-Bezeichner"),
("[tro]\nTRO01 = 1\n", "erwartet"),
("[andere]\n", "fehlt"),
])
def test_fehlerhafte_datei(tmp_path, inhalt, meldung):
path = tmp_path / "x.ini"
path.write_text(inhalt, encoding="utf-8")
with pytest.raises(ValueError, match=meldung):
load_ids(path)
+74
View File
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""Tests fuer tro_flow.link_tros - Uebergang durch eine Kreisel-Kreisel-Weiche.
Fixture: Testanlage test_system (dxfmakros results/export.csv) mit einem
zusaetzlichen Separator 0022 auf Kreisel 0007. Kreisel 0009 hat nur einen
eigenen Separator (0010 vor Weiche 0024 zu Kreisel 0001 mit der Entladung);
in Kreisel 0009 gelangt man nur ueber Weiche 0023, die Separator 0011 auf
Kreisel 0008 steuert. Die Kreiselketten kennen Weichen nicht als Glied - ohne
eigene Weichenkante fehlte TRO(0011) -> TRO(0010), der Ausgang war vom
Eingang aus unerreichbar und stattdessen zeigte TRO(0010) rueckwaerts auf
TRO(0011).
TROs werden ueber ihre Separatoren angesprochen, nicht ueber die IDs (die
haengen an der ID-Datei der Anlage).
"""
from __future__ import annotations
from pathlib import Path
import pytest
from material_flow import build_graph, read_elements
from tro_flow import BOUNDARY_ENTRY, BOUNDARY_EXIT, analyse_tros
FIXTURES = Path(__file__).parent / "fixtures"
def _analyse(name: str):
path = FIXTURES / name
elements = read_elements(path, [])
graph = build_graph(elements, path.name, [])
return analyse_tros(graph, elements)
@pytest.fixture(scope="module")
def analysis():
return _analyse("test_system_sep22_export.csv")
def _tro_mit_sep(analysis, sep_id: str) -> str:
return next(t.tro_id for t in analysis.tros if sep_id in t.separators)
def _erreichbar(edges, start: str) -> set[str]:
nach: dict[str, list[str]] = {}
for a, b in edges:
nach.setdefault(a, []).append(b)
seen, stack = {start}, [start]
while stack:
for nxt in nach.get(stack.pop(), []):
if nxt not in seen:
seen.add(nxt)
stack.append(nxt)
return seen
def test_weiche_fuehrt_auf_partnerkreisel(analysis):
von, nach = _tro_mit_sep(analysis, "0011"), _tro_mit_sep(analysis, "0010")
assert (von, nach) in analysis.edges
def test_keine_rueckwaertskante_ueber_die_weiche(analysis):
von, nach = _tro_mit_sep(analysis, "0010"), _tro_mit_sep(analysis, "0011")
assert (von, nach) not in analysis.edges
def test_ausgang_vom_eingang_erreichbar(analysis):
# Nicht fuer test_system_export.csv: dort fehlt auf Kreisel 0007 der
# Separator vor der Weiche zu Kreisel 0008 (erst 0022 hier steuert sie) -
# die Anlage ist dort physisch nicht durchgaengig.
eingang = next(b.tro_id for b in analysis.boundaries if b.type_name == BOUNDARY_ENTRY)
ausgang = next(b.tro_id for b in analysis.boundaries if b.type_name == BOUNDARY_EXIT)
assert ausgang in _erreichbar(analysis.edges, eingang)