diff --git a/CLAUDE.md b/CLAUDE.md index f4003da..ae48630 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,13 +27,18 @@ Template" below). What exists today is: tool in `lib/` (see "Environment scripts" below) - `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. + (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; + `cfg/_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 @@ -90,7 +95,7 @@ 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/model.py`, `lib/dxf_registration.py` and + `lib/e_planning.py`, plus the `lib/tro_catalog.py`, `lib/model.py`, `lib/dxf_registration.py` 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 @@ -135,6 +140,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 → `_tro.json` (with slots, `jamAreas`), hardware planning → + `hw_import.py` → `_hw.json`, `hw_assign.py` → `_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) @@ -167,8 +182,12 @@ and status: `doc/SCL_Templates.md`. Golden files in `tests/fixtures/scl/` pin th byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended change. 1. **`lib/tro_flow.py --json`** (implemented) — writes `_tro.json` (schema - `sps_skel-tro-v3`, `model.TroDocument`): TROs, system boundaries `SYSn`, connections with - their kind, findings. See `doc/Python_Scripts.md` §4. + `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`, all predecessors that feed it), findings. TRO-IDs come from `cfg/_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 (`_FB_Main.scl`): one `REGION` per TRO with the instance call and every @@ -184,6 +203,13 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha `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` → `_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`. What is **not** built out yet, relative to the original two-tool design: diff --git a/README.md b/README.md index 1f76d53..2d53d40 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ einem CSV-Export der mechanischen Planung (ILS 2.0) entstehen automatisch: - der **Materialfluss-Graph** der mechanischen Objekte (Strecken, Gefällestrecken, Kreisel), - die daraus abgeleitete **TRO-Liste** samt eigenem Flussdiagramm, -- die **TRO-JSON** `_tro.json` (Schema `sps_skel-tro-v3`, Modell +- die **TRO-JSON** `_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 diff --git a/bin/e_planning.bat b/bin/e_planning.bat new file mode 100644 index 0000000..36d0afb --- /dev/null +++ b/bin/e_planning.bat @@ -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 _eplan.json und Bericht _eplan.md in %SKEL_RESULTS%. +REM +REM Weiter: bin\scl_skeleton.bat --json _tro.json --eplan _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% diff --git a/bin/e_planning.sh b/bin/e_planning.sh new file mode 100644 index 0000000..0f4f3f2 --- /dev/null +++ b/bin/e_planning.sh @@ -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 _eplan.json und Bericht _eplan.md in $SKEL_RESULTS. +# +# Weiter: bin/scl_skeleton.sh --json _tro.json --eplan _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" "$@" diff --git a/cfg/e_planning.ini b/cfg/e_planning.ini new file mode 100644 index 0000000..2d5bf61 --- /dev/null +++ b/cfg/e_planning.ini @@ -0,0 +1,81 @@ +; ================================================================ +; 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 "." 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 150) +; 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 _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.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 +; 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 diff --git a/doc/Datenquellen_Layout_Hardwareplanung.md b/doc/Datenquellen_Layout_Hardwareplanung.md new file mode 100644 index 0000000..76dc475 --- /dev/null +++ b/doc/Datenquellen_Layout_Hardwareplanung.md @@ -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) | `-_EA.xlsx`, `-_TIA.xlsx` (PLC Tags + Constants), `-_WSCAD.xlsx`, `_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]--------------> 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 `-`, `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` | 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`, `stInSwitch` | +| 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 `_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.; 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.; 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; ob nötig: Layout + stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.udt_TRO107.TroOut, // Default: Namensregel + nInMainTroNo := "cMainTro107", // Default: Namensregel cMainTro + 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). diff --git a/doc/E-Planung_Bruecke.md b/doc/E-Planung_Bruecke.md new file mode 100644 index 0000000..828c607 --- /dev/null +++ b/doc/E-Planung_Bruecke.md @@ -0,0 +1,161 @@ +# Brücke Layout ↔ E-Planung über `*_positionsdraw.json` + +Stand 2026-10-10. 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. + +## 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 `_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` (157 Feld-BMK), +`ST500592_ILS_positionsdraw.json` (~3100), `ST500592_10_5-10_Omniflo_…` (~900), +`ST-Fortna_positions_.json` (mit 38 Scannern `BX`). + +## Test an Mubea + +Eingaben: `data/mubea.csv` (Layout, 38 Separatoren) und +`kabellaengen/work/Mubea_positionsdraw.json` (E-Planung). Die Zahlen in diesem +Abschnitt stammen aus der ersten Untersuchung (Umkreis 700 mm, ohne Regeln); das +Ergebnis der Umsetzung steht 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: die E-Zeichnung liegt im selben +Koordinatensystem wie die Layoutzeichnung. Zum Prüfen genügt also der +bestehende Mechanismus; als Schlüssel in `cfg/dxf_registration.json` kann +`|` dienen. + +### 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.stSenInSep` | BG mit `VERW` `IN SEP` | räumlich am Separator (Anker) | +| `xOutStopper` | MB mit `VERW` `SEP` | Nummernregel ab dem BG | +| `stInSenJam`, `Settings.xPluggedJam` | BG `STAU_REF-LT` | räumlich, Versatz je Symbolart; vorhanden ⇒ `xPluggedJam := TRUE` | +| `xOutSwExTo3/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" | Textabgleich mit dem Host-Kreisel des TRO | +| 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: Not-Halt-Kreis (`xInSftyOk`), Taster für Automatik/Quittieren +(stehen nur als Schrankelemente ohne Bezug zum TRO), Scanner-Hardwarekennungen, +Weichenbauart (`xNewSwitch`). + +## 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 + `|`. +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** `_eplan.json` (`EplanDocument`) und Bericht `_eplan.md`. + `scl_skeleton --eplan` setzt Separator-Sensor und Stopperventil ein + (`SclName.todo = False`); alles andere bleibt `TODO(E-Planung)`. + +Ergebnis Mubea 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. + +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. diff --git a/doc/Klassenhierarchie.md b/doc/Klassenhierarchie.md index 3175e21..c3fae05 100644 --- a/doc/Klassenhierarchie.md +++ b/doc/Klassenhierarchie.md @@ -31,6 +31,8 @@ scl_skeleton.py (Ausgabe: SclFile -> FB_Main.scl, generisch oder per Jin 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 @@ -68,8 +70,11 @@ 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 +│ ├── TroBase, TroCandidate, JamArea +├── Slot(BaseModel) (separator1/switch1/scanner1/drive + Rollen) +├── JamArea(_Positioned) (Staubereich J: Eingang eines TRO) ├── Connection(BaseModel) (from/to/kind zwischen zwei TRO-IDs) ├── TroCandidate(_Positioned) (gemeldete Stelle, an der ein TRO erwartet wird) ├── Analysis(BaseModel) @@ -80,7 +85,8 @@ lib/model.py [Instanz-Ebene / TRO-JSON] ├─ tros: list[DerivedTro] ├─ boundaries: list[DerivedTro] ├─ connections: list[Connection] - └─ candidates: list[TroCandidate] + ├─ candidates: list[TroCandidate] + └─ jam_areas: list[JamArea] (JSON: jamAreas) lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass] ├── Element (1 CSV-Zeile) @@ -132,6 +138,17 @@ lib/scl_context.py [Render-Kontext für die SCL, BaseM lib/scl_skeleton.py [SCL-Ausgabe] └── SclTemplates (Ordner mit .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) (= _eplan.json) + ├─ transform: Transform (aus dxf_registration) + ├─ assignments: list[Assignment] + └─ drives: list[Drive] ``` ## Auffälligkeiten für ein mögliches Refactoring @@ -238,13 +255,14 @@ Kantenarten) → Prüfung gegen den Katalog. ### `TroDocument` — `lib/model.py` -**Wozu:** Die TRO-JSON `_tro.json` (Schema `sps_skel-tro-v3`) — die +**Wozu:** Die TRO-JSON `_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). `to_json()` schreibt mit den +(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` @@ -414,3 +432,310 @@ generischen Aufbau (`emit_region()`). **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`, `cSep.`, `"DB_Interface_HMI".stTRO.TRO` …). 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. + +**`-_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 | + +**`-_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` | + +**`-_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 | | | + +**`_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`, `xOutStopper`) + +| 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` / `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`; 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`, `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 `xOutSwExTo3/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`, `stInPriorityManagerDir` | `"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. diff --git a/doc/Konzept_Layout_Hardware_SCL.md b/doc/Konzept_Layout_Hardware_SCL.md new file mode 100644 index 0000000..3b4e530 --- /dev/null +++ b/doc/Konzept_Layout_Hardware_SCL.md @@ -0,0 +1,379 @@ +# 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──> _tro.json EA/TIA/WSCAD.xlsx, positionsdraw.json + (TROs, Abfolge, ──hw_import──> _hw.json + Staubereiche, Slots) (globale Variablen, Geräte) + │ │ + └──────────── hw_assign ─────────────────┘ + │ + _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` | `_tro.json` | vorhanden, wird erweitert | +| 2 Hardware | `hw_import.py` | `*_TIA.xlsx`, `*_EA.xlsx`, `*_WSCAD.xlsx`, `*_positionsdraw.json` | `_hw.json` | neu | +| 3 Zuordnung | `hw_assign.py` | `_tro.json`, `_hw.json`, Regeln, `cfg/hw_overrides.ini` | `_anlage.json`, Bericht `.md` | neu (Logik aus `e_planning.py`) | +| 4 SCL | `scl_skeleton.py` | `_anlage.json` (oder nur `_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`, `switch`, + `scanner`, `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` 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), ``/`` für den + SCL-Namen der Staubereiche. + +### 2. Neues Skript `hw_import.py` (Stufe 2) + +- Liest TIA-Import (PLC Tags + Constants), EA-Liste, WSCAD-Import und optional + `positionsdraw.json`. +- 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 `hw_assign.py` (Stufe 3) + +- Eingabe: `_tro.json` + `_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 `_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. + +### 4. `scl_skeleton.py` / `scl_context.py` (Stufe 4) + +- Liest **eine** Eingabe: `_anlage.json`; ohne Hardwareteil wie heute + `_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 `_anlage.json` | + +### `e_planning.py` → `hw_import` + `hw_assign` + +| Teil heute | Wohin | +|---|---| +| `load_positionsdraw`, `EDevice`, `pair_drives` | `hw_import.py` | +| `resolve_transform`, `load_rules`, `_assign_spatial`, `_assign_by_number`, Bericht | `hw_assign.py`, Ziele = Slots statt CSV-Bauteile | +| `cfg/e_planning.ini` | Regeldatei von `hw_assign` | +| `EplanDocument` / `_eplan.json` | entfällt, ersetzt durch `bindings[]` | + +Übergang: `e_planning.py` bleibt kurz als Hülle, die `hw_import` und `hw_assign` +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/_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 `[.tro.]` in + `cfg/tro_overrides.ini` und die Zuordnungen von `hw_assign` stabil. + +### Staubereiche + +**Bisheriges Namensschema** (Referenzen UH01–UH05, `doc/TRO_Katalog/connect.ini`, +286 Staubereiche): + +```text +"DB_JamArea".stJam_ Kurzform JA_ +``` + +- `` = **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. +- `` = 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`, 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".stJam_`. `J` 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: + - ``: 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; + - ``: 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 `_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. +4. `hw_assign` mit der räumlichen Logik aus `e_planning.py`; `e_planning.py` als + Übergangshülle. +5. `scl_skeleton` auf `_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 `` 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. diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index df1d2a7..53405c4 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -1,14 +1,16 @@ # Python Scripts — CLI Tooling Overview -**As of:** 2026-10-09 +**As of:** 2026-10-10 -`lib/` currently holds ten Python modules (five of them runnable CLI tools, +`lib/` currently holds eleven Python modules (six of them runnable CLI tools, five supporting libraries), plus one documentation-generator script that lives under `doc/TRO_Katalog/tro_graphs/`. Together the CLI tools turn a CSV export of the mechanical layout (ILS 2.0) into a material-flow graph, a derived TRO list/diagram, a TRO-JSON (shared model `lib/model.py`), an `FB_Main` SCL skeleton generated from that JSON, and an annotated copy of the BricsCAD -drawing for visual checking. +drawing for visual checking. `e_planning.py` bridges to the electrical +planning: it assigns the sensors/valves of the E-planning drawing to the +layout parts, so the skeleton can carry their real names. This document is the practical "what does each script do and which switches does it take" reference. For the underlying domain model (TRO types, JSON @@ -34,6 +36,11 @@ tro_flow.py ──► _tro_flow.dot / .svg, _tro_doc.md, _tro_error │ scl_skeleton.py ──► _FB_Main.scl │ (validates the JSON against model.py; one REGION per TRO; │ E-planning values left as TODO(E-Planung) gaps) + │ ▲ + │ │ --eplan _eplan.json (optional) + │ e_planning.py ◄── _positionsdraw.json (kabellaengen) + │ (assigns BG/MB of the E-planning drawing to the layout + │ parts by position + numbering rule; see §5c) │ └── same derivation ──► tro_annotate.py ──► _annotated.dxf (VIEWER ONLY) (also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF @@ -66,6 +73,10 @@ Supporting libraries (no CLI of their own, imported by the tools above): - **`lib/dxf_registration.py`** — derives, verifies and persists the rigid (rotation + offset) transform between the CSV export's coordinate system and the DXF drawing's. +- **`lib/tro_ids.py`** — the per-plant ID file `%SKEL_CFG%\_tro_ids.ini`: + reads it, finds each TRO again by the position of its separators, assigns + new IDs, writes/extends the file. Used by `tro_flow.py` (read and write) and + `tro_annotate.py` (read only). See §4, "TRO-IDs". - **`lib/tro_overrides.py`** — reads the hand-maintained `%SKEL_CFG%\tro_overrides.ini`; only `material_flow.py --connect` still uses it. See §6a. @@ -91,6 +102,8 @@ Each CLI tool is invoked through its `bin/.bat` (Windows) / `bin/.sh bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json bin\scl_skeleton.bat --json mubea_tro.json +bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json +bin\scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend ``` @@ -102,8 +115,9 @@ 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. @@ -294,17 +308,45 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here. | `--doc` | — | off | Also write a Markdown report (`_tro_doc.md`) with the TRO list, connections, findings and the full type catalog. | | `--connect` | — | off | Also write the TRO topology as `_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 `_tro.json` (schema `sps_skel-tro-v3`, model `TroDocument` in `lib/model.py`): TROs, system boundaries (`SYSn`), connections with their kind, findings. Input for `scl_skeleton.py`. | +| `--json` | — | off | Also write the TRO-JSON `_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` | `_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%\_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` 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%`): `_tro_flow.dot` (always, with `pos` attributes when `--use-cords` was used), `_tro_flow.svg` (with `--tosvg`), `_tro_doc.md` (with `--doc`), `_tro.json` (with `--json`), `_tro_errors.md` (only if TROs are unconnected or of an -unknown type). +unknown type). In `%SKEL_CFG%`: `_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). @@ -355,6 +397,7 @@ 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. | +| `--ids` | `DATEI` | `_tro_ids.ini` | ID file as for `tro_flow.py`, **read only**: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as `tro_flow.py` would. | | `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is `0,0`. | | `--re-register` | — | off | Re-derive the coordinate offset even if a verified one is already cached in `%SKEL_CFG%`. | | `--check` | — | off | Dry run: only analyze and report, write no output file. | @@ -446,7 +489,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` in the comment) | | | destination list per switch exit (graph reachability) | times marked "pruefen" | | cross-reference to the layout parts (separator numbers, coordinate) | | @@ -460,7 +504,7 @@ than a guess. Switches: `--json NAME` (default `export_tro.json`, looked up in `%SKEL_RESULTS%`; written by `tro_flow.py --json`), `--out NAME` (default -`_FB_Main.scl`), `--start TRO`. The last one matters while the plant has +`_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. @@ -494,6 +538,16 @@ 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 +`_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 => "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 @@ -502,6 +556,86 @@ 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 `_positionsdraw.json`. + +``` +.csv (layout) + _positionsdraw.json (E-planning) + │ + ▼ +e_planning.py ──► _eplan.json (EplanDocument, schema sps_skel-eplan-v1) + ──► _eplan.md (report: assignments, gaps, drives) + │ + ▼ +scl_skeleton.py --eplan _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 + `|` 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 `.` per + rule, applied in file order; each E device is given out at most once, each + role of a layout part filled at most once: + + | key | meaning | + |---|---| + | `layout` | layout kind pattern (default: the part before the dot): `Separator`, `Ausschleus`, `Einschleus`, `Weiche`, `Scanner`, `Beladung`, `Entladung` | + | `verw`, `praefix` | pattern(s) for the E attribute `VERW`, BMK prefix (`BG`, `MB`) | + | `max_mm` | spatial: maximum distance after registration | + | `soll_mm`, `toleranz_mm` | spatial: the symbol always sits this far from the part; candidates are ranked by their deviation from it, not by the smallest distance | + | `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest | + + Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`, + `Ausschleus.stau`, `Ausschleus.ventil`, `Weiche.ventil`. `VERW` texts and the + numbering rule are project-specific — in the reference SCL (UH01..UH05) + sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`. +4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the + report. + +Uncertain cases stay open on purpose: a gap is better than a plausible wrong +name. The report lists, per role, the layout parts without a match, the field +devices without a match, and distance and runner-up distance of every match +(close values = check by hand). + +Switches: `--file NAME` (CSV, default `export.csv`), `--eplan NAME` +(positionsdraw JSON, required; bare name resolved against `%SKEL_DATA%`, or a +full path), `--rules FILE` (default `%SKEL_CFG%\e_planning.ini`), +`--re-register`. Exit `0` also when not everything was assigned; `1` on input +errors or when the registration cannot be proven. + +**Today only separator sensor and stopper reach the SCL.** Jam sensors and +outfeed/switch valves are assigned in the JSON/report, but the TRO-JSON does not +yet say which outfeed element or switch belongs to which TRO exit, so +`scl_skeleton` cannot place them. The same holds for the storage lines of a +`PinStore` TRO (`arInSep[n]`): their sensors are assigned but the generator does +not emit per-line parameters yet. + +Result on Mubea (`mubea.csv` + `Mubea_positionsdraw.json`): registration +dx +50613.6 / dy +8555.8 mm; separator sensor 21/38, stopper 22/38 (all 20 +storage lines exact, 0–1 mm), jam sensor 20/23, outfeed valve 20/23, switch +valve 1/1. The open separators are on the carousels K0102/K0104/K0105, where +the E-planning drawing and the CSV apparently differ. + +Tests: `tests/test_e_planning.py` (reading, rules, numbering vs. distance on a +storage-line grid, target distance, uniqueness, registration, drives, the path +into `scl_context` and `scl_skeleton --eplan`). + ## 6. `tro_catalog.py` — TRO type catalog (library, no CLI) The single source of truth for everything needed to generate or draw a TRO @@ -535,8 +669,8 @@ 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, Connection, TroCandidate, - Analysis, TroDocument +model.py instance level TroBase ─► DerivedTro, Slot, JamArea, Connection, + TroCandidate, Analysis, TroDocument ▲ tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws) ``` @@ -544,11 +678,13 @@ 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`. Also used for the system boundaries `SYSn`. | +| `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`, `slots`. Also used for the system boundaries `SYSn` (no slots). | +| `Slot` | A place on a TRO where hardware belongs: `slot` (`separator1`, `switch1`, `scanner1`, `drive` — the stable key together with the TRO-ID), `layoutId` (TeileId in the current export, for tracing only; empty for switches, whose element `tro_flow` does not keep yet), `roles` the hardware planning must fill (`sensor`, `stopper`, `jamSensor?` …; `?` = optional). Built by `derive_slots()` from the catalog type. | +| `JamArea` | A jam area = the queue in front of a separator, the entry of exactly one TRO: `id` (`J`, 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".stJam_` 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 `[.openpoint.]`; 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-v3`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `findings`. `from_analysis()`, `to_json()`, `from_json()`. | +| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v4`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `jamAreas`, `findings`. `from_analysis()` (derives slots and jam areas), `to_json()`, `from_json()`, `jam_areas_or_derived()` (a v3 file without `jamAreas` still renders). | `TroBase` and `TroCandidate` share the `position` handling (JSON `{x,y,z}`, `x`/`y`/`z`, `describe_position()`) through the private mixin `_Positioned`. @@ -560,7 +696,9 @@ 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`. +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 diff --git a/doc/SCL_Templates.md b/doc/SCL_Templates.md index f48ae12..9f20b30 100644 --- a/doc/SCL_Templates.md +++ b/doc/SCL_Templates.md @@ -14,7 +14,7 @@ It is written so that it fits the existing main path unchanged: ``` export.csv ─► tro_flow --json ─► _tro.json ─► scl_skeleton ─► _FB_Main.scl (dxfmakros (derive) (TroDocument, (render) (skeleton with - EXPORTCSV) sps_skel-tro-v3) TODO(E-Planung)) + EXPORTCSV) sps_skel-tro-v4) TODO(E-Planung)) ``` Nothing upstream of `scl_skeleton` changes. The TRO-JSON stays the only input, and the @@ -274,11 +274,11 @@ 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".` 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` = first predecessor, `JamFinger` = first successor; `xInErrorQuit`/`xInAutomatic` open | +| `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` 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".` 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` — a store with more feeds/exits (mubea: 2/2) gets a TODO in the REGION header | +| `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 @@ -290,7 +290,8 @@ 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`). More reference lines are written and every open value is marked; the count +`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). --- @@ -420,7 +421,7 @@ With `--templates` the console summary gets a `Templates` line (implemented); th ``` Templates = 4 von 5 Typen (generisch: FB_ILS_MTRO_2Sep2Swi) -Offene Stellen = 489 (Marker 'TODO(E-Planung)') +Offene Stellen = 482 (Marker 'TODO(E-Planung)') ``` --- @@ -478,7 +479,7 @@ bin\scl_skeleton.bat --json _tro.json [--templates [DIR]] [--strict] [--na | File | Where | Who writes it | |---|---|---| | `.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` | -| `_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v3`) | +| `_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v4`) | | `scl_naming.ini`, `_scl_naming.ini` | `%SKEL_CFG%` | you, once per project (optional) | | `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers | diff --git a/lib/e_planning.py b/lib/e_planning.py new file mode 100644 index 0000000..c2e78d3 --- /dev/null +++ b/lib/e_planning.py @@ -0,0 +1,715 @@ +# -*- 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 _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 _eplan.json (EplanDocument) und Bericht + _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. + +Schichten: e_planning importiert material_flow (CSV) und dxf_registration +(Lagebezug); 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 configparser +import fnmatch +import json +import math +import re +import sys +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path + +from pydantic import BaseModel, ConfigDict, Field + +from dxf_registration import ( + RegistrationError, + Transform, + derive_transform, + load_transform, + save_transform, + verify_transform, +) + +__all__ = [ + "EPLAN_SCHEMA", + "EDevice", + "LayoutPoint", + "Rule", + "Assignment", + "Drive", + "EplanDocument", + "load_positionsdraw", + "load_rules", + "assign", + "pair_drives", +] + +EPLAN_SCHEMA = "sps_skel-eplan-v1" +RULES_FILE = "e_planning.ini" + +_CONFIG = ConfigDict(extra="forbid") + +# "=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+)$") + + +# --------------------------------------------------------------------------- +# Modell +# --------------------------------------------------------------------------- + + +class EDevice(BaseModel): + """Ein Sensor/Aktor/Schrankelement der E-Planung.""" + + 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 + + +@dataclass +class LayoutPoint: + """Ein punktartiges Bauteil des Layouts in CSV-Koordinaten.""" + + layout_id: str + kind: str + x: float + y: float + name: str = "" + + +@dataclass +class Rule: + """Eine Zuordnungsregel aus e_planning.ini (Abschnitt ".").""" + + 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 = 150.0 + + 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 + + +class Drive(BaseModel): + """Motor im Feld und sein Motorschutz im Schrank (gleiche Nummer).""" + + model_config = _CONFIG + + motor: str + protection: str = "" + bezeichnung: str = "" + protection_bezeichnung: str = "" + + +class EplanDocument(BaseModel): + """Die Zuordnungs-JSON _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) + + +# --------------------------------------------------------------------------- +# Einlesen +# --------------------------------------------------------------------------- + + +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 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 _patterns(text: str) -> list[str]: + return [p.strip() for p in text.split(",") if p.strip()] + + +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 "." 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", "150")), + )) + 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 + + +def _match(text: str, patterns: list[str]) -> bool: + return any(fnmatch.fnmatchcase(text.upper(), p.upper()) for p in patterns) + + +# --------------------------------------------------------------------------- +# Zuordnung +# --------------------------------------------------------------------------- + + +@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: + continue + m = _BMK.match(partner.bmk) + if not m: + continue + dev = cands.get(int(m.group(2)) + 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)) + + +def pair_drives(devices: list[EDevice]) -> list[Drive]: + """Motor MAnnnn <-> Motorschutz FCnnnn (gleiche Nummer).""" + fc = {d.number: d for d in devices if d.prefix.upper() == "FC" and d.number is not None} + 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) + drives.append(Drive( + motor=d.bmk, + protection=schutz.bmk if schutz else "", + bezeichnung=d.bezeichnung, + protection_bezeichnung=schutz.bezeichnung if schutz else "", + )) + return drives + + +# --------------------------------------------------------------------------- +# Layout und Lagebezug +# --------------------------------------------------------------------------- + +# Element.kind der punktartigen Layout-Bauteile +LAYOUT_KINDS = ("Separator", "Scanner", "Weiche", "Ausschleus", "Einschleus", + "Beladung", "Entladung") + + +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 + + +def _anchor_points(points: list[LayoutPoint], devices: list[EDevice], + anchors: list[str]): + csv_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 csv_pts, e_pts + + +def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str, + eplan_name: str, re_register: bool, + warnings: list[str]) -> Transform: + """Abgelegten Lagebezug nachrechnen oder neu ermitteln und ablegen.""" + csv_pts, e_pts = _anchor_points(points, devices, anchors) + if not re_register: + stored = load_transform(cfg_dir, csv_name, eplan_name) + if stored: + verify_transform(stored, csv_pts, e_pts) + return stored + transform = derive_transform(csv_pts, e_pts, csv_name, eplan_name) + saved = save_transform(cfg_dir, transform) + warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}") + return transform + + +# --------------------------------------------------------------------------- +# 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) + 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: _eplan.json fuer " + "scl_skeleton --eplan und ein Bericht _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()) diff --git a/lib/model.py b/lib/model.py index 89ff30a..94080bd 100644 --- a/lib/model.py +++ b/lib/model.py @@ -21,6 +21,11 @@ Klassen: 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 Analysis Arbeitsergebnis von tro_flow.analyse_tros TroDocument die TRO-JSON (_tro.json): von tro_flow --json geschrieben, von scl_skeleton gelesen und geprueft @@ -47,22 +52,28 @@ from pydantic import ( model_validator, ) -from tro_catalog import TroDefinition, get_tro +from tro_catalog import TroDefinition, TroItem, get_tro __all__ = [ "SCHEMA", "TroBase", + "Slot", + "JamArea", "DerivedTro", "Connection", "TroCandidate", "Analysis", "TroDocument", + "derive_slots", + "derive_jam_areas", ] # 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`. -SCHEMA = "sps_skel-tro-v3" +# `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). +SCHEMA = "sps_skel-tro-v4" # Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte # Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt @@ -191,6 +202,50 @@ class TroBase(_Positioned): 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`` (Mubea: Sep1024 -> J1024). Mehrere Vorgaenger + koennen in denselben Staubereich einfahren (Zusammenfuehrung); deren + Ausgang zeigt dann auf diesen Staubereich. Der SCL-Name + ``"DB_JamArea".stJam_`` (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 DerivedTro(TroBase): """ Ein von tro_flow aus dem CSV abgeleiteter TRO (oder eine Systemgrenze). @@ -207,6 +262,8 @@ class DerivedTro(TroBase): # 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: @@ -288,16 +345,23 @@ class TroDocument(BaseModel): 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") findings: list[str] = Field(default_factory=list) @classmethod def from_analysis(cls, analysis: Analysis, source: str, note: str = "") -> TroDocument: - """Dokument aus dem Ergebnis von tro_flow.analyse_tros bilden.""" + """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. + """ + tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros] return cls( source=source, note=note, - tros=analysis.tros, + tros=tros, boundaries=analysis.boundaries, + jam_areas=derive_jam_areas(tros), connections=[ Connection(src=src, dst=dst, kind=analysis.edge_kinds.get((src, dst), "")) for src, dst in analysis.edges @@ -310,6 +374,10 @@ class TroDocument(BaseModel): """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" @@ -319,3 +387,75 @@ class TroDocument(BaseModel): if tro.tro_id == tro_id: return tro return None + + +# --------------------------------------------------------------------------- +# 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") + + +def _at(values: list[str], index: int) -> str: + return values[index] if index < len(values) else "" + + +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. + """ + 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))) + 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; fehlt der Separator im Layout, J_. 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 diff --git a/lib/scl_context.py b/lib/scl_context.py index fce9a3e..debf2d2 100644 --- a/lib/scl_context.py +++ b/lib/scl_context.py @@ -16,8 +16,11 @@ Schichten: Namen (Separator-, Weichen-, MainTro-Nummer, Adressen ...) sind ein :class:`SclName`: Wert plus Merker, ob er noch aus der E-Planung kommen muss. -Heute sind alle Platzhalter mit ``todo=True``; die Namensregeln aus Schritt 4 -(cfg/scl_naming.ini) ersetzen dann nur den Wert und loeschen den Merker. +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 @@ -25,12 +28,16 @@ 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 TroBase, TroDocument +from model import JamArea, TroBase, TroDocument from tro_catalog import TroItem +if TYPE_CHECKING: + from e_planning import EplanDocument + __all__ = [ "TODO", "SclName", @@ -69,6 +76,16 @@ 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-Block.""" @@ -130,12 +147,19 @@ class SclScanner(BaseModel): class SclJam(BaseModel): - """Ein Staubereich am Ein- oder Ausgang (JamEntr / JamExit).""" + """Ein Staubereich am Ein- oder Ausgang (JamEntr / JamExit). + + Ein Eingang ist ein Staubereich des TRO selbst (TroDocument.jamAreas), ein + Ausgang der Eingangs-Staubereich des Nachfolgers. + """ model_config = _CONFIG n: int - # Nachbar als Text fuer den Kommentar, SYSn als Station markiert + # Kennung aus der TRO-JSON (J); "" 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); None an Eingaengen @@ -145,6 +169,11 @@ class SclJam(BaseModel): 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.""" @@ -209,6 +238,11 @@ class SclStore(BaseModel): # 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.""" @@ -282,6 +316,8 @@ class SclFile(BaseModel): 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 = "" # --------------------------------------------------------------------------- @@ -388,9 +424,10 @@ def _scanner(n: int, suffix: str, interface: str = "cScanner____") -> SclScanner ) -def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None) -> SclJam: +def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None, + jam_id: str = "") -> SclJam: return SclJam( - n=n, neighbour=neighbour, db=_offen(jam_db), sensor=sensor, + 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"), ) @@ -434,23 +471,55 @@ def _store(tro: TroBase) -> SclStore: ) -def _tro_context(tro: TroBase, alle: dict[str, TroBase]) -> SclTro: +def _separator(i: int, layout: str, eplan: EplanDocument | None) -> SclSeparator: + rollen = eplan.for_layout(layout) if eplan is not None and layout != "?" else {} + sensor = rollen.get("sensor") + stopper = rollen.get("stopper") + return SclSeparator( + n=i, + layout=layout, + no=_offen("cSep_____"), + sensor=_bekannt(_SENSOR.format(bmk=sensor.bmk)) if sensor else _offen("cInBG____"), + stopper=_bekannt(_VENTIL.format(bmk=stopper.bmk)) if stopper else _offen("MB____"), + # 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 = [ - SclSeparator( - n=i, - layout=tro.separators[i - 1] if i <= len(tro.separators) else "?", - no=_offen("cSep_____"), - sensor=_offen("cInBG____"), - stopper=_offen("MB____"), - part_sensor=_offen("cInBG____"), - ) + _separator(i, tro.separators[i - 1] if i <= len(tro.separators) else "?", eplan) for i in range(1, n_sep + 1) ] switches = [ @@ -475,15 +544,14 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase]) -> SclTro: SclSwitchExit(via=abgang, targets=reachable_via(tro_id, abgang, alle)) for abgang in tro.successors ] if n_swi else [] - # 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. - jam_entries = [ - _jam(n, _nachbar(vor, alle), jam_db) - for n, vor in enumerate(tro.predecessors, start=1) - ] + # 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(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) ] @@ -541,13 +609,23 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase]) -> SclTro: def build_context(document: TroDocument, source: str, start_id: str = "", - created: str | None = None) -> SclFile: - """TroDocument -> SclFile (TROs in Fliessrichtung).""" + 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) for t in geordnet], + 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 ""), ) diff --git a/lib/scl_skeleton.py b/lib/scl_skeleton.py index b24cd2e..3831335 100644 --- a/lib/scl_skeleton.py +++ b/lib/scl_skeleton.py @@ -51,6 +51,7 @@ from jinja2 import ( ) from pydantic import ValidationError +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 @@ -220,11 +221,11 @@ def emit_call(add, tro: SclTro) -> None: for jam in tro.jam_entries: zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier", - offen(jam.db, f"Staubereich von {jam.neighbour}"))) + 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"Staubereich nach {jam.neighbour}"))) + offen(jam.db, f"{jam.title} vor {jam.neighbour}"))) zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", "")) for s in tro.separators: @@ -329,6 +330,12 @@ def render_context(context: SclFile, templates: SclTemplates | None = None) -> s add("//") add("// Reihenfolge der REGIONs = Materialfluss.") 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: _eplan.md (lib/e_planning.py).") if templates is not None: mit, ohne = template_usage(context, templates) add("//") @@ -372,9 +379,7 @@ 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_flow.bat --file .csv --json laufen lassen.", @@ -412,7 +417,28 @@ def main(argv: list[str] | None = None) -> int: return 1 templates = SclTemplates(ordner) - context = build_context(document, json_file.name, args.start) + 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 .csv --eplan " + "_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) - " @@ -440,6 +466,9 @@ def main(argv: list[str] | None = None) -> int: 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("================================================================") @@ -447,6 +476,21 @@ 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.""" + return sum(not name.todo + for tro in context.tros for s in tro.separators + for name in (s.sensor, s.stopper)) + + def _schema_of(path: Path) -> str: """Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar).""" try: @@ -465,6 +509,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: " 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 (auch wenn TROs ohne Template generisch erzeugt wurden)\n" @@ -500,6 +545,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: "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 (_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", diff --git a/lib/scl_templates/FB_ILS_MTRO_1Sep.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_1Sep.scl.j2 index 414b243..0c5d8b8 100644 --- a/lib/scl_templates/FB_ILS_MTRO_1Sep.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_1Sep.scl.j2 @@ -50,11 +50,11 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ j.neighbour))) %} +{% 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, "Staubereich nach " ~ j.neighbour))) %} +{% 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 %} diff --git a/lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2 index 5e550ce..6f6fe93 100644 --- a/lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2 @@ -42,11 +42,11 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ j.neighbour))) %} +{% 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, "Staubereich nach " ~ j.neighbour))) %} +{% 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, "")) %} diff --git a/lib/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl.j2 index 9dbb99a..1733121 100644 --- a/lib/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl.j2 @@ -83,11 +83,11 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ j.neighbour))) %} +{% 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, "Staubereich nach " ~ j.neighbour))) %} +{% 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 %} diff --git a/lib/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl.j2 index 53b2bc8..496bf86 100644 --- a/lib/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl.j2 @@ -77,11 +77,11 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ j.neighbour))) %} +{% 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, "Staubereich nach " ~ j.neighbour))) %} +{% 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, "")) %} diff --git a/lib/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl.j2 index e29b4e4..3ae884d 100644 --- a/lib/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl.j2 @@ -15,8 +15,10 @@ Sep Store n nJamLength offen (Referenz: 4 bzw. "cStorage111ExitJam"), Zeiten ueber die Konstanten cTimeSep...StorageLine arInSep[7..19].stSimu Simulationsmodus der Referenz - nicht geschrieben - JamEntr/JamExit je einer: erster Vorgaenger/Nachfolger. Hat das - Lager mehr Zu-/Ablaeufe, steht ein TODO im Kopf. + 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. @@ -28,9 +30,9 @@ {% set kamera = "" if sc.tag.todo else " (" ~ (sc.tag.value|upper) ~ ")" %} REGION {{ t.region_title }} {{ m.kopf(t) }} -{% if t.predecessors|length > 1 or t.successors|length > 1 %} - // {{ TODO }}: {{ t.predecessors|length }} Zulaeufe / {{ t.successors|length }} Ablaeufe im Layout - - // der FB hat je nur einen Staubereich (JamEntr/JamExit, dazu JamBypass) +{% 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 "-" }} @@ -103,9 +105,9 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ t.jam_in.neighbour))) %} +{% 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, "Staubereich nach " ~ t.jam_out.neighbour))) %} +{% 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", "")) %} diff --git a/lib/scl_templates/FB_ILS_MTRO_Vario.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_Vario.scl.j2 index 21f5ce2..6af94cf 100644 --- a/lib/scl_templates/FB_ILS_MTRO_Vario.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_Vario.scl.j2 @@ -69,9 +69,9 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ t.jam_in.neighbour))) %} +{% 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, "Staubereich nach " ~ t.jam_out.neighbour))) %} +{% 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", "")) %} diff --git a/lib/scl_templates/FB_ILS_MTRO_Vario_workStation.scl.j2 b/lib/scl_templates/FB_ILS_MTRO_Vario_workStation.scl.j2 index 98c0a68..8703823 100644 --- a/lib/scl_templates/FB_ILS_MTRO_Vario_workStation.scl.j2 +++ b/lib/scl_templates/FB_ILS_MTRO_Vario_workStation.scl.j2 @@ -71,9 +71,9 @@ REGION {{ t.region_title }} {% 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, "Staubereich von " ~ t.jam_in.neighbour))) %} +{% 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, "Staubereich nach " ~ t.jam_out.neighbour))) %} +{% 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", "")) %} diff --git a/lib/tro_annotate.py b/lib/tro_annotate.py index 13f9c75..bd4a9fb 100644 --- a/lib/tro_annotate.py +++ b/lib/tro_annotate.py @@ -116,7 +116,9 @@ from tro_flow import ( analyse_tros, find_sibling_raw_json, load_sepliste_counts, + resolve_ids_file, ) +from tro_ids import TroIds # --------------------------------------------------------------------------- # Konstanten @@ -844,6 +846,9 @@ 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("--ids", metavar="DATEI", + help="ID-Datei der TROs wie bei tro_flow (nur gelesen). " + "Standard: _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", @@ -907,12 +912,21 @@ def main(argv: list[str] | None = None) -> int: return 1 graph = build_graph(elements, csv_file.name, read_warnings) # Derselbe Aufruf wie in tro_flow.main (ohne cfg/tro_overrides.ini, mit - # der Separator-Gegenprobe aus _raw.json), damit die Zeichnung genau + # der Separator-Gegenprobe aus _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 {}) - analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts) + # 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 diff --git a/lib/tro_flow.py b/lib/tro_flow.py index 5c2fa63..a3e66fa 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -90,6 +90,14 @@ Ausgabe (alles in SKEL_RESULTS) fuer scl_skeleton.py _tro_errors.md nur wenn TRO-Fehler gefunden wurden +TRO-IDs (ID-Datei in SKEL_CFG) +------------------------------ +Die IDs kommen aus %SKEL_CFG%/_tro_ids.ini (lib/tro_ids.py): jeder TRO +wird ueber die Lage seiner Separatoren wiedergefunden und behaelt seine ID, +auch wenn die SPS-Programmierer sie umbenannt haben. Fehlt die Datei, werden +die TROs entlang des Materialflusses durchnummeriert und die Datei angelegt; +neue TROs werden angehaengt. --renumber nummeriert alles neu. + Aufruf ueber bin/tro_flow.bat bzw. bin/tro_flow.sh. """ @@ -126,6 +134,16 @@ from material_flow import ( resolve_input, ) from model import Analysis, DerivedTro, TroDocument +from tro_ids import ( + DEFAULT_TOLERANCE, + TroIds, + assign_ids, + ids_path, + insert_entries, + other_sections, + render_ids, + render_new_entries, +) from tro_catalog import ( CATALOG_AS_OF, ITEM_FB, @@ -655,6 +673,7 @@ def derive_tros( findings: list[str], *, sepliste_counts: dict[str, int] | None = None, + tro_ids: TroIds | None = None, ) -> list[DerivedTro]: """TRO-Liste aus den Separatoren und ihren angeschlossenen Objekten bilden. @@ -666,6 +685,10 @@ def derive_tros( der CSV abgeleitete Separatorenzahl eines Strecke/Gefaellestrecke-Hosts nicht mit dieser Zahl ueberein, wird ein Finding angehaengt; die TRO-Daten selbst bleiben unangetastet (CSV bleibt die massgebliche Quelle). + + tro_ids (optional): ID-Datei der Anlage. Ohne sie werden die TROs entlang + des Materialflusses neu durchnummeriert; mit ihr behalten wiedergefundene + TROs ihre ID (Ergebnis in tro_ids.result). """ def resolve_host(sep: Element, findings: list[str]) -> str | None: return _default_resolve_host(sep, findings, graph) @@ -1156,11 +1179,16 @@ def derive_tros( tro.reason = f"{tro.reason}; {len(exits)} abgehende Wege -> Weiche" tro.confidence = "niedrig" - # 5) Stabile, laufende Nummern in Reihenfolge der Host-Knoten - tros.sort(key=lambda t: (t.hosts[0], t.separators[0] if t.separators else "")) - width = max(2, len(str(len(tros)))) - for index, tro in enumerate(tros, start=1): - tro.tro_id = f"TRO{index:0{width}d}" + # 5) IDs: Reihenfolge entlang des Materialflusses, wiedergefundene TROs + # behalten ihre ID aus der ID-Datei (siehe lib/tro_ids.py) + tros.sort(key=_flow_key(graph, elements_by_id)) + anchors = separator_anchors(elements) + if tro_ids is None: + assign_ids(tros, anchors, [], renumber=True) + else: + tro_ids.result = assign_ids( + tros, anchors, tro_ids.entries, + tolerance=tro_ids.tolerance, renumber=tro_ids.renumber) # 6) Scanner am Separator: 1Sep_SSCC laesst sich mechanisch nicht erkennen for tro in tros: @@ -1174,6 +1202,56 @@ def derive_tros( return tros +def separator_anchors(elements: list[Element]) -> dict[str, tuple[float, float]]: + """Separator-TeileId -> (x, y) in mm, Bezugslage fuer die ID-Datei.""" + return {e.teile_id: (e.position[0], e.position[1]) + for e in elements if e.kind == "Separator" and e.position} + + +def _flow_rank(graph: Graph) -> dict[str, int]: + """Knoten -> Rang entlang des Materialflusses (Tiefensuche). + + Start an den Materialeingaengen (Beladung/Eingang), danach an jedem noch + nicht erreichten Knoten - eine geschlossene Anlage ohne Station wird so + trotzdem vollstaendig erfasst. Gleichrangige Nachfolger nach Knotennamen. + """ + succ: dict[str, list[str]] = {} + for edge in graph.edges: + succ.setdefault(edge.src, []).append(edge.dst) + starts = sorted(n for n, node in graph.nodes.items() if node.element.is_entry) + rank: dict[str, int] = {} + for start in starts + sorted(graph.nodes): + stack = [start] + while stack: + node = stack.pop() + if node in rank: + continue + rank[node] = len(rank) + stack.extend(n for n in sorted(succ.get(node, []), reverse=True) + if n not in rank) + return rank + + +def _flow_key(graph: Graph, elements_by_id: dict[str, Element]): + """Sortierschluessel fuer TROs entlang des Materialflusses. + + Erst der Rang des Host-Knotens, dann die Lage des Separators in der + TrackIds-Kette des Hosts (Kreisel: Umlaufrichtung, Strecke: Baureihenfolge). + """ + rank = _flow_rank(graph) + + def key(tro: DerivedTro) -> tuple: + host_rank = min((rank.get(h, len(rank)) for h in tro.hosts), + default=len(rank)) + sep = tro.separators[0] if tro.separators else "" + host = elements_by_id.get(tro.hosts[0]) if tro.hosts else None + track = host.track_ids if host else [] + in_track = track.index(sep) if sep in track else len(track) + return (host_rank, in_track, sep) + + return key + + # Typnamen fuer Systemgrenz-Knoten (siehe derive_boundaries) - bewusst NICHT # in TRO_CATALOG: eine Be-/Entladestation (BTMT oder generisches Eingang/ # Ausgang-Symbol) ist kein Steuerobjekt mit eigenem FB-Baustein, nur der @@ -1781,11 +1859,15 @@ def analyse_tros( elements: list[Element], *, sepliste_counts: dict[str, int] | None = None, + tro_ids: TroIds | None = None, ) -> Analysis: - """TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.""" + """TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen. + + tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu nummerieren. + """ analysis = Analysis() analysis.tros = derive_tros(graph, elements, analysis.findings, - sepliste_counts=sepliste_counts) + sepliste_counts=sepliste_counts, tro_ids=tro_ids) analysis.boundaries = derive_boundaries(elements, graph) # Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen: # link_tros() registriert eine Kante nur zu einem Knoten, der Host eines @@ -2638,6 +2720,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: " tro_flow.bat --file mubea.csv --tosvg --doc\n" " tro_flow.bat --file mubea.csv --tosvg --use-cords\n" " tro_flow.bat --file mubea.csv --json\n" + " tro_flow.bat --file mubea_export.csv --ids mubea_tro_ids.ini --json\n" + " tro_flow.bat --file mubea.csv --renumber\n" "\n" "Exit-Codes:\n" " 0 ok\n" @@ -2694,9 +2778,93 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: f"Leerstrecken ueber {COORD_MAX_LUECKE} mm zwischen den Knoten " "(Reihenfolge und Nachbarschaft bleiben erhalten).", ) + parser.add_argument( + "--ids", + metavar="DATEI", + help="ID-Datei der Anlage (Name in %%SKEL_CFG%% oder vollstaendiger " + "Pfad). Standard: _tro_ids.ini in %%SKEL_CFG%%. Fehlt sie, " + "wird sie angelegt.", + ) + parser.add_argument( + "--renumber", + action="store_true", + help="Alle TRO-IDs neu entlang des Materialflusses vergeben und die " + "Abschnitt [tro] der ID-Datei neu schreiben (fruehe Projektphase - " + "umbenannte IDs gehen verloren, andere Abschnitte bleiben).", + ) + parser.add_argument( + "--id-tolerance", + dest="id_tolerance", + type=float, + default=DEFAULT_TOLERANCE, + metavar="MM", + help="Hoechster Abstand (mm) zwischen Eintrag der ID-Datei und " + "Separator, bis zu dem ein TRO als wiedergefunden gilt. " + "Standard: %(default)s", + ) return parser.parse_args(argv) +def resolve_ids_file(arg: str | None, csv_file: Path) -> Path: + """Pfad der ID-Datei: --ids (Name in SKEL_CFG oder Pfad), sonst Standard.""" + cfg_dir = env_dir("SKEL_CFG", "cfg") + if not arg: + return ids_path(csv_file.stem, cfg_dir) + path = Path(arg) + if path.is_absolute() or path.parent != Path("."): + return path + return cfg_dir / path + + +def write_ids_file(tro_ids: TroIds, analysis: Analysis, elements: list[Element], + source: str) -> str: + """ID-Datei anlegen, neu schreiben oder neue TROs anhaengen. + + Rueckgabe: kurze Beschreibung fuer den Bericht. + """ + result = tro_ids.result + if result is None: + return "unveraendert" + anchors = separator_anchors(elements) + if result.renumbered: + tro_ids.path.parent.mkdir(parents=True, exist_ok=True) + rest = other_sections(tro_ids.path) + text = render_ids(analysis.tros, anchors, source) + tro_ids.path.write_text(text + (f"\n{rest}\n" if rest else ""), + encoding="utf-8") + return "neu geschrieben" if tro_ids.exists else "angelegt" + if result.new: + insert_entries(tro_ids.path, + render_new_entries(analysis.tros, anchors, result.new, source)) + return f"{len(result.new)} neue(r) TRO(s) angehaengt" + return "unveraendert" + + +def report_ids(tro_ids: TroIds, status: str) -> None: + """Abschnitt TRO-IDs im Konsolenbericht.""" + result = tro_ids.result + if result is None: + return + print(f"TRO-IDs = {tro_ids.path} ({status})") + if result.renumbered: + grund = "--renumber" if tro_ids.renumber else "keine ID-Datei" + print(f" neu entlang des Materialflusses nummeriert " + f"({grund})") + return + print(f" wiedergefunden: {len(result.kept)}, " + f"neu: {len(result.new)}, weggefallen: {len(result.missing)} " + f"(Toleranz {tro_ids.tolerance:g} mm)") + if result.new: + print(f" + neu (vorlaeufige ID, in der ID-Datei umbenennbar): " + f"{', '.join(result.new)}") + for entry in result.missing: + merged = result.merged_into.get(entry.tro_id) + grund = (f"Separator gehoert jetzt zu {merged}" if merged else + f"kein Separator innerhalb {tro_ids.tolerance:g} mm") + print(f" - weggefallen: {entry.tro_id} bei {entry.x:.1f}, {entry.y:.1f} " + f"({grund}; Eintrag bleibt stehen, ID wird nicht wiederverwendet)") + + def main(argv: list[str] | None = None) -> int: args = parse_args(argv) @@ -2739,11 +2907,20 @@ def main(argv: list[str] | None = None) -> int: connect_target = results / f"{stem}_tro_connect.ini" json_target = results / f"{stem}_tro.json" - analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts) + try: + tro_ids = TroIds.load(resolve_ids_file(args.ids, csv_file), + tolerance=args.id_tolerance, renumber=args.renumber) + 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: {csv_file} enthaelt keine Separatoren - keine TROs " f"ableitbar.", file=sys.stderr) return 1 + ids_status = write_ids_file(tro_ids, analysis, elements, csv_file.name) dot_file = results / f"{stem}_tro_flow.dot" svg_file = results / f"{stem}_tro_flow.svg" @@ -2806,6 +2983,7 @@ def main(argv: list[str] | None = None) -> int: report(analysis, csv_file, dot_file, created_svg, error_file, doc_file, warnings, engine=engine) + report_ids(tro_ids, ids_status) if connect_file: print(f"connect.ini = {connect_file}") if json_file: diff --git a/lib/tro_ids.py b/lib/tro_ids.py new file mode 100644 index 0000000..d70bab4 --- /dev/null +++ b/lib/tro_ids.py @@ -0,0 +1,309 @@ +# -*- 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%/_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 + +__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. Separatoren derselben Bahn liegen deutlich weiter +# auseinander; ein verschobenes Bauteil faellt so als "neu" auf. +DEFAULT_TOLERANCE = 100.0 + +# Eine ID wird zum SCL-Bezeichner (Instanz #, "DB_TRO_TEST".). +_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: /_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") diff --git a/tests/fixtures/scl/HundM05_FB_Main.scl b/tests/fixtures/scl/HundM05_FB_Main.scl index 7921a3d..c602753 100644 --- a/tests/fixtures/scl/HundM05_FB_Main.scl +++ b/tests/fixtures/scl/HundM05_FB_Main.scl @@ -72,13 +72,9 @@ REGION TRO20 (1Sep) Sep 0007 xInRelease := "DB_TRO_TEST".TRO20, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS1 (Station, kein TRO) + 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, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS2 (Station, kein TRO) - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO19 - stInOutJamEntr3 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO01 + 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, @@ -122,11 +118,11 @@ REGION TRO01 (1Sep1Swi) Sep 0008 xInRelease := "DB_TRO_TEST".TRO01, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO20 + 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 nach TRO02 + 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 nach TRO06 + 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) @@ -183,9 +179,9 @@ REGION TRO02 (Vario) Sep 0025 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + 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 nach TRO03 + 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) @@ -230,13 +226,11 @@ REGION TRO06 (1Sep1Swi) Sep 0010 xInRelease := "DB_TRO_TEST".TRO06, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 von TRO01, TRO05 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO05 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO07 + 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 nach TRO09 + 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) @@ -277,9 +271,9 @@ REGION TRO03 (1Sep) Sep 0026 xInRelease := "DB_TRO_TEST".TRO03, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO02 + 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 nach TRO04 + 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, @@ -329,9 +323,9 @@ REGION TRO07 (Vario) Sep 0031 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + 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 nach TRO08 + 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) @@ -376,13 +370,11 @@ REGION TRO09 (1Sep1Swi) Sep 0012 xInRelease := "DB_TRO_TEST".TRO09, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO06, TRO08 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO08 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO10 + 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 nach TRO12 + 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) @@ -439,9 +431,9 @@ REGION TRO04 (Vario) Sep 0027 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO03 + 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 nach TRO05 + 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) @@ -476,9 +468,9 @@ REGION TRO08 (1Sep) Sep 0032 xInRelease := "DB_TRO_TEST".TRO08, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO07 + 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 nach TRO09 + 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, @@ -528,9 +520,9 @@ REGION TRO10 (Vario) Sep 0035 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + 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 nach TRO11 + 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) @@ -575,13 +567,11 @@ REGION TRO12 (1Sep1Swi) Sep 0014 xInRelease := "DB_TRO_TEST".TRO12, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 von TRO09, TRO11 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO11 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO13 + 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 nach TRO15 + 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) @@ -622,9 +612,9 @@ REGION TRO05 (1Sep) Sep 0028 xInRelease := "DB_TRO_TEST".TRO05, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO04 + 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 nach TRO06 + 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, @@ -658,9 +648,9 @@ REGION TRO11 (1Sep) Sep 0036 xInRelease := "DB_TRO_TEST".TRO11, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + 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 nach TRO12 + 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, @@ -710,9 +700,9 @@ REGION TRO13 (Vario) Sep 0039 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO12 + 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 nach TRO14 + 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) @@ -757,13 +747,11 @@ REGION TRO15 (1Sep1Swi) Sep 0016 xInRelease := "DB_TRO_TEST".TRO15, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO12 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 von TRO12, TRO14 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO14 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO16 + 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 nach TRO17 + 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) @@ -804,9 +792,9 @@ REGION TRO14 (1Sep) Sep 0040 xInRelease := "DB_TRO_TEST".TRO14, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + 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 nach TRO15 + 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, @@ -840,11 +828,9 @@ REGION TRO16 (1Sep) Sep 0018 xInRelease := "DB_TRO_TEST".TRO16, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 von TRO15, TRO18 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO19 + 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, @@ -894,9 +880,9 @@ REGION TRO17 (Vario) Sep 0043 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + 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 nach TRO18 + 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) @@ -931,11 +917,11 @@ REGION TRO19 (1Sep) Sep 0020 xInRelease := "DB_TRO_TEST".TRO19, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO16 + 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 nach SYS3 (Station, kein TRO) + 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 nach TRO20 + 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, @@ -969,9 +955,9 @@ REGION TRO18 (1Sep) Sep 0044 xInRelease := "DB_TRO_TEST".TRO18, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO17 + 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 nach TRO16 + 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, diff --git a/tests/fixtures/scl/HundM05_FB_Main_templates.scl b/tests/fixtures/scl/HundM05_FB_Main_templates.scl index c8ce91a..18293fa 100644 --- a/tests/fixtures/scl/HundM05_FB_Main_templates.scl +++ b/tests/fixtures/scl/HundM05_FB_Main_templates.scl @@ -79,13 +79,9 @@ REGION TRO20 (1Sep) Sep 0007 xInRelease := "DB_TRO_TEST".TRO20, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS1 (Station, kein TRO) + 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, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS2 (Station, kein TRO) - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO19 - stInOutJamEntr3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO01 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 vor TRO01 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -133,11 +129,11 @@ REGION TRO01 (1Sep1Swi) Sep 0008 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO20 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 von TRO20 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO02 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 vor TRO02 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO06 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO01, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -204,9 +200,9 @@ REGION TRO02 (Vario) Sep 0025 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO02, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 von TRO01 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO03 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 vor TRO03 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -257,13 +253,11 @@ REGION TRO06 (1Sep1Swi) Sep 0010 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 von TRO01, TRO05 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO05 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO07 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 vor TRO07 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO09 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO06, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -309,9 +303,9 @@ REGION TRO03 (1Sep) Sep 0026 xInRelease := "DB_TRO_TEST".TRO03, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO02 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 von TRO02 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO04 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 vor TRO04 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -371,9 +365,9 @@ REGION TRO07 (Vario) Sep 0031 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO07, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 von TRO06 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO08 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 vor TRO08 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -424,13 +418,11 @@ REGION TRO09 (1Sep1Swi) Sep 0012 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO06, TRO08 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO08 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO10 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 vor TRO10 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO12 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO09, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -497,9 +489,9 @@ REGION TRO04 (Vario) Sep 0027 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO04, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO03 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 von TRO03 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO05 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 vor TRO05 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -541,9 +533,9 @@ REGION TRO08 (1Sep) Sep 0032 xInRelease := "DB_TRO_TEST".TRO08, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO07 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 von TRO07 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO09 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -603,9 +595,9 @@ REGION TRO10 (Vario) Sep 0035 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO10, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 von TRO09 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO11 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 vor TRO11 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -656,13 +648,11 @@ REGION TRO12 (1Sep1Swi) Sep 0014 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 von TRO09, TRO11 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO11 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO13 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 vor TRO13 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO15 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO12, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -708,9 +698,9 @@ REGION TRO05 (1Sep) Sep 0028 xInRelease := "DB_TRO_TEST".TRO05, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO04 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 von TRO04 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO06 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -749,9 +739,9 @@ REGION TRO11 (1Sep) Sep 0036 xInRelease := "DB_TRO_TEST".TRO11, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von TRO10 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO12 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -811,9 +801,9 @@ REGION TRO13 (Vario) Sep 0039 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO13, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO12 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 von TRO12 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO14 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 vor TRO14 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -864,13 +854,11 @@ REGION TRO15 (1Sep1Swi) Sep 0016 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO12 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 von TRO12, TRO14 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO14 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO16 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO17 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO17 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO15, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -916,9 +904,9 @@ REGION TRO14 (1Sep) Sep 0040 xInRelease := "DB_TRO_TEST".TRO14, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO13 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO15 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -957,11 +945,9 @@ REGION TRO16 (1Sep) Sep 0018 xInRelease := "DB_TRO_TEST".TRO16, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 von TRO15, TRO18 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO19 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 vor TRO19 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1021,9 +1007,9 @@ REGION TRO17 (Vario) Sep 0043 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO17, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO15 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO18 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO18 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -1066,11 +1052,11 @@ REGION TRO19 (1Sep) Sep 0020 xInRelease := "DB_TRO_TEST".TRO19, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO16 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 von TRO16 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach SYS3 (Station, kein TRO) + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO) stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO20 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0007 vor TRO20 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1109,9 +1095,9 @@ REGION TRO18 (1Sep) Sep 0044 xInRelease := "DB_TRO_TEST".TRO18, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO17 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO17 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO16 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) diff --git a/tests/fixtures/scl/mubea_FB_Main.scl b/tests/fixtures/scl/mubea_FB_Main.scl index efd89fd..ee646f7 100644 --- a/tests/fixtures/scl/mubea_FB_Main.scl +++ b/tests/fixtures/scl/mubea_FB_Main.scl @@ -70,11 +70,9 @@ REGION TRO03 (1Sep) Sep 0042 xInRelease := "DB_TRO_TEST".TRO03, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS1 (Station, kein TRO) + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 von SYS1 (Station, kein TRO), TRO04 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO04 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO05 + 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, @@ -108,9 +106,9 @@ REGION TRO05 (1Sep) Sep 0044 xInRelease := "DB_TRO_TEST".TRO05, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO03 + 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 nach TRO18 + 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, @@ -150,13 +148,11 @@ REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, xInRelease := "DB_TRO_TEST".TRO18, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO05 + 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, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO12 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO07 + 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 nach TRO10 + 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, @@ -190,11 +186,9 @@ REGION TRO07 (1Sep) Sep 0045 xInRelease := "DB_TRO_TEST".TRO07, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 von TRO09, TRO18 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO04 + 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, @@ -265,17 +259,15 @@ REGION TRO10 (2Sep2Swi) Sep 0005, 0036 xInRelease := "DB_TRO_TEST".TRO10, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + 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 von TRO06 + arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von - (Aufteilung der Vorgaenger auf die Eingaenge offen) stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr3 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO01 + 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 nach TRO02 + 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 nach TRO11 + 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) @@ -322,9 +314,9 @@ REGION TRO04 (1Sep) Sep 0043 xInRelease := "DB_TRO_TEST".TRO04, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO07 + 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 nach TRO03 + 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, @@ -358,9 +350,9 @@ REGION TRO01 (1Sep) Sep 0040 xInRelease := "DB_TRO_TEST".TRO01, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + 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 nach TRO10 + 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, @@ -394,13 +386,13 @@ REGION TRO02 (1Sep) Sep 0012 xInRelease := "DB_TRO_TEST".TRO02, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + 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 nach SYS2 (Station, kein TRO) + 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 nach SYS3 (Station, kein TRO) + 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 nach TRO13 + 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, @@ -450,9 +442,9 @@ REGION TRO11 (Vario) Sep 0058 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + 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 nach TRO12 + 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) @@ -497,11 +489,11 @@ REGION TRO13 (1Sep1Swi) Sep 0011 xInRelease := "DB_TRO_TEST".TRO13, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO02 + 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 nach TRO16 + 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 nach TRO17 + 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) @@ -542,9 +534,9 @@ REGION TRO12 (1Sep) Sep 0059 xInRelease := "DB_TRO_TEST".TRO12, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO11 + 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 nach TRO18 + 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, @@ -594,9 +586,9 @@ REGION TRO16 (Vario) Sep 0062 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + 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 nach TRO15 + 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) @@ -631,11 +623,9 @@ REGION TRO17 (1Sep) Sep 0063 xInRelease := "DB_TRO_TEST".TRO17, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 von TRO13, TRO14 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO14 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO06 + 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, @@ -669,9 +659,9 @@ REGION TRO15 (1Sep) Sep 0038 xInRelease := "DB_TRO_TEST".TRO15, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO16 + 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 nach TRO14 + 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, @@ -715,11 +705,11 @@ REGION TRO06 (1Sep1Swi) Sep 0003 xInRelease := "DB_TRO_TEST".TRO06, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO17 + 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 nach TRO08 + 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 nach TRO10 + 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) @@ -760,9 +750,9 @@ REGION TRO14 (1Sep) Sep 0037 xInRelease := "DB_TRO_TEST".TRO14, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + 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 nach TRO17 + 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, @@ -812,9 +802,9 @@ REGION TRO08 (Vario) Sep 0054 xInAutomatic := #t_automatik, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + 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 nach TRO09 + 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) @@ -849,9 +839,9 @@ REGION TRO09 (1Sep) Sep 0055 xInRelease := "DB_TRO_TEST".TRO09, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO08 + 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 nach TRO07 + 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, diff --git a/tests/fixtures/scl/mubea_FB_Main_templates.scl b/tests/fixtures/scl/mubea_FB_Main_templates.scl index eb02b33..f8536fa 100644 --- a/tests/fixtures/scl/mubea_FB_Main_templates.scl +++ b/tests/fixtures/scl/mubea_FB_Main_templates.scl @@ -77,11 +77,9 @@ REGION TRO03 (1Sep) Sep 0042 xInRelease := "DB_TRO_TEST".TRO03, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von SYS1 (Station, kein TRO) + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 von SYS1 (Station, kein TRO), TRO04 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO04 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO05 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO05 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -120,9 +118,9 @@ REGION TRO05 (1Sep) Sep 0044 xInRelease := "DB_TRO_TEST".TRO05, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO03 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO03 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO18 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -136,8 +134,8 @@ REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, // Nachfolger: TRO07, TRO10 // Einstufung: hoch // Aufbau nach Template FB_ILS_MTRO_PinStore_Auto.scl.j2 (Referenz: doc/TRO_Katalog/scl_templates) - // TODO(E-Planung): 2 Zulaeufe / 2 Ablaeufe im Layout - - // der FB hat je nur einen Staubereich (JamEntr/JamExit, dazu JamBypass) + // TODO(E-Planung): 1 Eingangs- / 2 Ausgangs-Staubereiche im Layout - + // der FB hat je nur einen (JamEntr/JamExit, dazu JamBypass) // Linien (Separatoren aus dem Layout): 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123 REGION Encoder Settings @@ -417,9 +415,9 @@ REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, xInConveyorRunOut := #fbExitConv____.xOutConveyorFullSpeed, // TODO(E-Planung) - Foerderer am Ablauf stInOutControlUnitCabinet := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank arInOutStorageLines := "DB_Storage____".arLine, // TODO(E-Planung) - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO05 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 von TRO05, TRO12 (Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator) stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO07 + arInOutJamExit := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07 stInOutJamExit := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) arInOutJamBypass := #arCarrierPlaceHolder, stInOutJamBypass := #stJamDataPlaceHolder, @@ -501,11 +499,9 @@ REGION TRO07 (1Sep) Sep 0045 xInRelease := "DB_TRO_TEST".TRO07, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO09 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 von TRO09, TRO18 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO04 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO04 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -577,17 +573,15 @@ REGION TRO10 (2Sep2Swi) Sep 0005, 0036 xInRelease := "DB_TRO_TEST".TRO10, stInOutMachineState := #stInOutControlUnitCabinet, stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO01 + 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 von TRO06 + arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von - (Aufteilung der Vorgaenger auf die Eingaenge offen) stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, - arInOutJamEntr3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO18 - stInOutJamEntr3 := "DB_JamArea____".stJam____.stData, - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO01 + 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 nach TRO02 + 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 nach TRO11 + 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) @@ -638,9 +632,9 @@ REGION TRO04 (1Sep) Sep 0043 xInRelease := "DB_TRO_TEST".TRO04, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO07 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO07 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO03 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 vor TRO03 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -679,9 +673,9 @@ REGION TRO01 (1Sep) Sep 0040 xInRelease := "DB_TRO_TEST".TRO01, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO10 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO10 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -722,13 +716,13 @@ REGION TRO02 (1Sep) Sep 0012 xInRelease := "DB_TRO_TEST".TRO02, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO10 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach SYS2 (Station, kein TRO) + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS2 (Station, kein TRO) stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach SYS3 (Station, kein TRO) + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO) stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO13 + arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 vor TRO13 stInOutJamExit4 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -788,9 +782,9 @@ REGION TRO11 (Vario) Sep 0058 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO11, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO10 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0058 von TRO10 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO12 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 vor TRO12 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -841,11 +835,11 @@ REGION TRO13 (1Sep1Swi) Sep 0011 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO02 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 von TRO02 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO16 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 vor TRO16 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO17 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO13, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -891,9 +885,9 @@ REGION TRO12 (1Sep) Sep 0059 xInRelease := "DB_TRO_TEST".TRO12, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO11 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 von TRO11 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO18 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -953,9 +947,9 @@ REGION TRO16 (Vario) Sep 0062 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO16, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 von TRO13 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO15 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 vor TRO15 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -997,11 +991,9 @@ REGION TRO17 (1Sep) Sep 0063 xInRelease := "DB_TRO_TEST".TRO17, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO13 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 von TRO13, TRO14 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO14 - stInOutJamEntr2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO06 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 vor TRO06 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1040,9 +1032,9 @@ REGION TRO15 (1Sep) Sep 0038 xInRelease := "DB_TRO_TEST".TRO15, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO16 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 von TRO16 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO14 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 vor TRO14 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1090,11 +1082,11 @@ REGION TRO06 (1Sep1Swi) Sep 0003 xInRelease := TRUE, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO17 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 von TRO17 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO08 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 vor TRO08 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO10 + arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10 stInOutJamExit3 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutHMI := "DB_Interface_HMI".stTRO.TRO06, nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1140,9 +1132,9 @@ REGION TRO14 (1Sep) Sep 0037 xInRelease := "DB_TRO_TEST".TRO14, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO15 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 von TRO15 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO17 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) @@ -1202,9 +1194,9 @@ REGION TRO08 (Vario) Sep 0054 xInErrorQuit := "SF____", // TODO(E-Planung) - Quittierung xInAutomatic := #t_automatik, // TODO(E-Planung) - Automatik des Schaltschranks xInRelease := "DB_TRO_TEST".TRO08, - arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO06 + arInOutJamEntr := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 von TRO06 stInOutJamEntr := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO09 + arInOutJamFinger := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 vor TRO09 stInOutJamFinger := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, @@ -1246,9 +1238,9 @@ REGION TRO09 (1Sep) Sep 0055 xInRelease := "DB_TRO_TEST".TRO09, stInOutMachineState := #stInOutControlUnitCabinet, // TODO(E-Planung) - Schaltschrank stInOutCognexInterface := #stCognexInterfaceDummy, - arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich von TRO08 + arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 von TRO08 stInOutJamEntr1 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) - arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich nach TRO07 + arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07 stInOutJamExit2 := "DB_JamArea____".stJam____.stData, // TODO(E-Planung) xOutStopper1 => "MB____", // TODO(E-Planung) nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"], // TODO(E-Planung) diff --git a/tests/test_e_planning.py b/tests/test_e_planning.py new file mode 100644 index 0000000..f3d495e --- /dev/null +++ b/tests/test_e_planning.py @@ -0,0 +1,253 @@ +# -*- 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 _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"} diff --git a/tests/test_model.py b/tests/test_model.py index 6508830..c6174c4 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -136,3 +136,65 @@ def test_scl_skeleton_renders_from_document(document): # 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"] diff --git a/tests/test_tro_ids.py b/tests/test_tro_ids.py new file mode 100644 index 0000000..0d0e2f5 --- /dev/null +++ b/tests/test_tro_ids.py @@ -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)