E-Planung -> Layout: e_planning.py, scl_skeleton --eplan, Kapitel Daten
- doc/Klassenhierarchie.md: Kapitel "Daten" - alle Daten der SCL-Referenzen nach Quelle (Layout, E-Planung, Namenskonvention, Parameter) - doc/E-Planung_Bruecke.md: Bruecke ueber *_positionsdraw.json des Kabellaengen-Tools, Befunde an Mubea - lib/e_planning.py + cfg/e_planning.ini + bin/e_planning.bat/.sh: Lagebezug E-Zeichnung -> CSV, Zuordnung BMK -> Layout-Bauteil nach Regeln (Abstand, Sollabstand, Nummernregel MB = BG + 1), <csv>_eplan.json + Bericht - scl_skeleton --eplan: Separator-Sensor und Stopperventil eingesetzt, ohne Schalter unveraenderte Ausgabe (Golden-Files gleich) - tests/test_e_planning.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5764df1ac8
commit
c736288476
@@ -27,13 +27,16 @@ 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
|
||||
- `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 +93,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
|
||||
@@ -182,6 +185,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` → `<csv>_eplan.json` (`EplanDocument`): registers the E drawing to
|
||||
the CSV (`dxf_registration.py`) and assigns BMKs to layout parts by rules in
|
||||
`cfg/e_planning.ini` (distance, target distance, numbering rule `MB = BG + 1`).
|
||||
`scl_skeleton.py --eplan` fills separator sensor and stopper valve from it; jam sensors
|
||||
and outfeed/switch valves are assigned but not yet placed in the SCL. See
|
||||
`doc/Python_Scripts.md` §5c and `doc/E-Planung_Bruecke.md`.
|
||||
|
||||
What is **not** built out yet, relative to the original two-tool design:
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - E-Planung den Bauteilen des Layouts zuordnen
|
||||
REM ================================================================
|
||||
REM Aufruf:
|
||||
REM bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json
|
||||
REM bin\e_planning.bat --file mubea.csv --eplan C:\pfad\Mubea_positionsdraw.json --re-register
|
||||
REM
|
||||
REM CSV und *_positionsdraw.json (Kabellaengen-Tool) werden in %SKEL_DATA%
|
||||
REM gesucht (oder als Pfad angegeben). Regeln: %SKEL_CFG%\e_planning.ini.
|
||||
REM Ergebnis <csv>_eplan.json und Bericht <csv>_eplan.md in %SKEL_RESULTS%.
|
||||
REM
|
||||
REM Weiter: bin\scl_skeleton.bat --json <name>_tro.json --eplan <name>_eplan.json
|
||||
REM ================================================================
|
||||
|
||||
setlocal
|
||||
|
||||
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
call "%~dp0setenv.bat" >nul
|
||||
|
||||
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
|
||||
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
|
||||
)
|
||||
|
||||
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
|
||||
set "PY=py"
|
||||
py --version >nul 2>&1 || set "PY=python"
|
||||
|
||||
"%PY%" "%SKEL_LIB%\e_planning.py" %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# ================================================================
|
||||
# SPS_SKEL - E-Planung den Bauteilen des Layouts zuordnen
|
||||
# ================================================================
|
||||
# Aufruf:
|
||||
# bash bin/e_planning.sh --file mubea.csv --eplan Mubea_positionsdraw.json
|
||||
# bash bin/e_planning.sh --file mubea.csv --eplan /pfad/Mubea_positionsdraw.json --re-register
|
||||
#
|
||||
# CSV und *_positionsdraw.json (Kabellaengen-Tool) werden in $SKEL_DATA
|
||||
# gesucht (oder als Pfad angegeben). Regeln: $SKEL_CFG/e_planning.ini.
|
||||
# Ergebnis <csv>_eplan.json und Bericht <csv>_eplan.md in $SKEL_RESULTS.
|
||||
#
|
||||
# Weiter: bin/scl_skeleton.sh --json <name>_tro.json --eplan <name>_eplan.json
|
||||
# ================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
# shellcheck source=/dev/null
|
||||
. "$SCRIPT_DIR/setenv.sh" >/dev/null
|
||||
|
||||
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$SPS_SKEL/.venv/bin/activate"
|
||||
fi
|
||||
|
||||
# Python-Interpreter bestimmen
|
||||
PY="python3"
|
||||
command -v python3 >/dev/null 2>&1 || PY="python"
|
||||
|
||||
exec "$PY" "$SKEL_LIB/e_planning.py" "$@"
|
||||
@@ -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 "<Art>.<Rolle>" ist eine Regel; sie werden in dieser
|
||||
; Reihenfolge angewendet, ein E-Bauteil wird hoechstens einmal vergeben.
|
||||
; layout Muster fuer die Layout-Art (Separator, Ausschleus,
|
||||
; Einschleus, Weiche, Scanner, Beladung, Entladung).
|
||||
; Standard: der Teil vor dem Punkt im Abschnittsnamen.
|
||||
; verw Muster fuer das E-Attribut VERW (fnmatch, Komma-Liste,
|
||||
; Gross-/Kleinschreibung egal)
|
||||
; praefix BMK-Praefix (BG, MB ...), leer = egal
|
||||
; max_mm raeumlich: Hoechstabstand nach dem Lagebezug in mm
|
||||
; soll_mm raeumlich: Sollabstand - das Symbol liegt immer gleich weit
|
||||
; neben dem Bauteil; gewertet wird die Abweichung vom Soll
|
||||
; (nicht der kleinste Abstand), zulaessig bis toleranz_mm
|
||||
; toleranz_mm zulaessige Abweichung vom Sollabstand (Standard 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 <csv>_eplan.md.
|
||||
;
|
||||
; Die VERW-Texte und die Nummernregel sind projektabhaengig. Die Werte hier
|
||||
; stammen aus Mubea (500573); in den Referenz-SCL (UH01..UH05) haben Sensor
|
||||
; und Stopper dieselbe Nummer (BG3242/MB3242) -> dort nummer_versatz = 0.
|
||||
; ================================================================
|
||||
|
||||
[Registrierung]
|
||||
anker = IN SEP
|
||||
|
||||
; Sensor im Stopper -> stInSeparator<n>.stSenInSep
|
||||
; sitzt in der Zeichnung auf dem Separator (Mubea Speicher: 0-1 mm).
|
||||
; An den Kreiseln liegen Sensoren 200-550 mm daneben, teils naeher an
|
||||
; Aus-/Einschleuselementen - unsicher, deshalb bewusst eng.
|
||||
[Separator.sensor]
|
||||
verw = IN SEP
|
||||
praefix = BG
|
||||
max_mm = 300
|
||||
|
||||
; Stopperventil -> xOutStopper<n>
|
||||
; liegt ~270 mm neben dem Separator, im 800-mm-Raster der Speicherlinien
|
||||
; naeher am Nachbarn als ueber die Nummer -> Nummernregel zuerst
|
||||
[Separator.stopper]
|
||||
verw = SEP
|
||||
praefix = MB
|
||||
nummer_von = Separator.sensor
|
||||
nummer_versatz = 1
|
||||
max_mm = 600
|
||||
soll_mm = 270
|
||||
|
||||
; Stausensor hinter dem Ausschleuselement (Mubea: konstant ~1390 mm)
|
||||
[Ausschleus.stau]
|
||||
verw = STAU*
|
||||
praefix = BG
|
||||
max_mm = 1600
|
||||
soll_mm = 1390
|
||||
|
||||
; Ausschleusventil (Mubea: konstant ~1414 mm, Nummer = Stausensor + 1)
|
||||
[Ausschleus.ventil]
|
||||
verw = AE ABZW*
|
||||
praefix = MB
|
||||
nummer_von = Ausschleus.stau
|
||||
nummer_versatz = 1
|
||||
max_mm = 1600
|
||||
soll_mm = 1414
|
||||
|
||||
; Weichenventil an einer Kreisel-Weiche
|
||||
[Weiche.ventil]
|
||||
verw = ABZWEIG-*, WEICHE*
|
||||
praefix = MB
|
||||
max_mm = 1000
|
||||
@@ -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 `<zeichnung>_positionsdraw.json`.
|
||||
Damit ist die Brücke eine **räumliche Zuordnung**: E-Symbol → nächstes Layout-Bauteil
|
||||
passender Art.
|
||||
|
||||
## Datenquelle `*_positionsdraw.json`
|
||||
|
||||
| Schlüssel | Inhalt | Nutzen für sps_skel |
|
||||
|---|---|---|
|
||||
| `sensors` | je BMK (`BG0642@UNKNOWN`): `pos` [x, y], `VERW` (Verwendung), `BEZEICHNUNG` (Klartext), `BESCHR`, `ARTINR`, `KENNZEICHNUNG` (`=A1+UH01-KF107`) | Hauptquelle: Sensoren/Ventile/Motoren im Feld |
|
||||
| `schaltschrank_elemente` | Elemente im Schrank (Motorschutz `FC…`, Taster, Leuchten, `QA…`) mit `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG`, ohne Feldposition | Motorschutz ↔ Motor, Bedienelemente |
|
||||
| `mappings` | Steuerung → Liste ihrer BMK (`UH01: [...]`) | Steuerungsaufteilung, Nummernkreis |
|
||||
| `racks`, `distributors`, `tunnels` | Kabeltrassen, Verteiler | für sps_skel ohne Belang |
|
||||
| `double_ids`, `warnings` | doppelte BMK, fehlende Attribute | Qualitätsprüfung der E-Daten |
|
||||
|
||||
Die Erdungs-Beispiele (`PR500501_…_Erdung_…`, `ST500652_…`) enthalten nur
|
||||
Potentialausgleich (`POT…`) und taugen nicht für die Brücke. Brauchbar sind
|
||||
Dateien mit `BG`/`MB`/`MA`: `Mubea_positionsdraw.json` (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
|
||||
`<csv>|<positionsdraw.json>` 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<n>.stSenInSep` | BG mit `VERW` `IN SEP` | räumlich am Separator (Anker) |
|
||||
| `xOutStopper<n>` | MB mit `VERW` `SEP` | Nummernregel ab dem BG |
|
||||
| `stInSenJam<n>`, `Settings.xPluggedJam` | BG `STAU_REF-LT` | räumlich, Versatz je Symbolart; vorhanden ⇒ `xPluggedJam := TRUE` |
|
||||
| `xOutSw<n>ExTo3/4` | MB `AE ABZWEIG` / Weichen-MB | Nummernregel ab dem Stausensor bzw. räumlich an Weiche/Ausschleuselement |
|
||||
| `Settings.xPluggedPart`, `stSenPart` | BG `BT BELADEN` | räumlich; vorhanden ⇒ `TRUE` |
|
||||
| `xInMotorProtection`, `xOutVarioMotor` | MA im Feld + FC in `schaltschrank_elemente` | gleiche Nummer |
|
||||
| `xInCarouselRun` (welcher Antrieb) | FC/MA-`BEZEICHNUNG` "KREISEL-K0102", "CONVEYOR #100" | 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
|
||||
`<csv>|<positionsdraw.json>`.
|
||||
3. **Regeln in `cfg/e_planning.ini`** (projektabhängig): je Rolle Layout-Art, VERW,
|
||||
Höchstabstand, **Sollabstand** (für die konstanten Versätze von Stausensor und
|
||||
Ausschleusventil — sonst gewinnt bei 0080/0084 ein näherer, fremder Stausensor
|
||||
des Kreisels K0102) und die Nummernregel (`MB = BG + 1`).
|
||||
4. **Ausgabe** `<csv>_eplan.json` (`EplanDocument`) und Bericht `<csv>_eplan.md`.
|
||||
`scl_skeleton --eplan` setzt Separator-Sensor und Stopperventil ein
|
||||
(`SclName.todo = False`); alles andere bleibt `TODO(E-Planung)`.
|
||||
|
||||
Ergebnis Mubea 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.
|
||||
@@ -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
|
||||
@@ -132,6 +134,17 @@ lib/scl_context.py [Render-Kontext für die SCL, BaseM
|
||||
|
||||
lib/scl_skeleton.py [SCL-Ausgabe]
|
||||
└── SclTemplates (Ordner mit <fbType>.scl.j2, hält ein jinja2.Environment)
|
||||
|
||||
lib/e_planning.py [Brücke Layout <-> E-Planung]
|
||||
├── EDevice(BaseModel) (ein BMK aus *_positionsdraw.json: VERW, Bezeichnung, Position)
|
||||
├── LayoutPoint (@dataclass: punktartiges Layout-Bauteil in CSV-Koordinaten)
|
||||
├── Rule (@dataclass: eine Regel aus cfg/e_planning.ini)
|
||||
├── Assignment(BaseModel) (BMK in einer Rolle an einem Layout-Bauteil, mit Abstand)
|
||||
├── Drive(BaseModel) (Motor MA ↔ Motorschutz FC)
|
||||
└── EplanDocument(BaseModel) (= <csv>_eplan.json)
|
||||
├─ transform: Transform (aus dxf_registration)
|
||||
├─ assignments: list[Assignment]
|
||||
└─ drives: list[Drive]
|
||||
```
|
||||
|
||||
## Auffälligkeiten für ein mögliches Refactoring
|
||||
@@ -414,3 +427,188 @@ 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** | Hardwarelayout (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** | Elektroplanung (EA-Liste/WSCAD/TIA-Variablentabelle, siehe `doc/HundM/EA-Listen-Analyse.md`) | Betriebsmittelkennzeichen und Variablennamen (`BG…` Sensoren, `MB…` Ventile, `MA…` Motoren, `FC…` Motorschutz, `SF…` Taster, `BX…` Scanner), Hardware-Kennungen, welche optionalen Sensoren verbaut sind, Zuordnung zu Schaltschrank und Not-Halt-Kreis. |
|
||||
| **K** | Namenskonvention | Name folgt aus TRO-Nummer bzw. Staubereichs-/Weichennummer (`cMainTro<n>`, `cSep<n>.<i>`, `"DB_Interface_HMI".stTRO.TRO<n>` …). Keine externe Quelle nötig, sobald Nummernkreis und Regel feststehen. |
|
||||
| **P** | Parameter / Projektstandard | Weder Layout noch E-Planung: Zeiten, Schwellwerte, Projektkonstanten, WCS-/Inbetriebnahme-Logik. Kandidaten für Typ-Defaults (`doc/TRO_Typen.md`). |
|
||||
|
||||
`L + E` heisst: das Layout sagt, **dass/wie viele** es gibt und **wo**, die
|
||||
E-Planung liefert den **Namen/die Adresse**. Das ist der häufigste Fall.
|
||||
|
||||
### TRO-Kopf und Instanzaufruf
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Existenz, Anzahl, Reihenfolge der TROs | `REGION TRO 100 …` | L | `tro_flow` → `DerivedTro`; REGION-Reihenfolge `scl_context.flow_order()` |
|
||||
| TRO-Typ / FB-Typ | `FB_ILS_MTRO_1Sep1Swi` | L (→ Katalog) | Typ aus Separator-Host (`doc/500573_Mubea/TRO_Identifikation_500573.md`), FB aus `tro_catalog` |
|
||||
| TRO-Nummer, Instanzname | `#TRO107` | L + E | Anzahl aus L; Nummernkreis je Steuerung (100er = UH01, 200er = UH02 …) ist Projektvorgabe |
|
||||
| REGION-Titel | `TRO 267 (1Sep2Switch)+Scanner(BX2100)` | L + E | Typ aus L, Scanner-BMK aus E |
|
||||
| `nInMainTroNo` | `"cMainTro100"` | K | Wert der Konstante in der PLC-Konstantentabelle |
|
||||
| `nOutStateLast` | `"DB_GLOB_TroState".arTroState["cMainTro100"]` | K | |
|
||||
| `stInOutHMI` | `"DB_Interface_HMI".stTRO.TRO100` | K | PinStore/Puffer: eigener UDT `UDT_HMI_TRO_PinStore` |
|
||||
| `xInRelease` | `"DB_TRO_TEST".TRO100`, `TRUE`, WCS-Verbindung aktiv | P | Freigabe-/Testlogik, Inbetriebnahme |
|
||||
| `xInAllRdyToStart`, `xInCancel`, `xInReset` | `TRUE`, `"cTodoFalse"` | P | meist Konstante |
|
||||
| `stInSettings.xIsBorner` (`xIsBorner1/2`) | `TRUE` | L | erster TRO nach Einfahrt in die Anlage (Beladestation / Systemgrenze `SYSn`) |
|
||||
| `stInSettings.nBornerDest(1/2)` | – | P | Standardziel für neu angelegte Carrier (WCS) |
|
||||
| `stInSettings.bSepType` (`bSep1Type/2Type`) | `.%X2 := TRUE` | E + P | X0 erstes Bein halten, X1 Beladekontrolle, X2 Beladeerfassung — hängt an verbauter Sensorik und Prozess |
|
||||
| `stInSettings.nTroStopper1/2`, `nTroSwitchExit2/3` | – | L + K | Nummern der Nachbar-TROs |
|
||||
| `stInSettings.sJamSrc` / `sJamDest` | – | L + K | Namen der benachbarten Staubereiche |
|
||||
| `stInSettings.xCar2NecRight/Left` | – | L | zweiter Kreisel nötig für Richtung rechts/links (Topologie an einer Kreisel-Weiche) |
|
||||
| `tPartSenHigh`, `tInSenHigh`, `tDummyTimeNormal`, `tDummyTimeBorner` | FB-Defaults 500 ms / 3 s / 0 ms | P | Typ-Defaults |
|
||||
|
||||
### Separator (`stInSeparator<n>`, `xOutStopper<n>`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Separatoren je TRO | `stInSeparator1`, `stInSeparator2` | L | `ILS 2.0 Separator` + `Zuordnung`, Typ-Katalog `items` |
|
||||
| `Settings.nSeparatorNo` | `"cSep107.1"` | K | Layout-Separator-ID (`TeileId`) ist **nicht** diese Nummer |
|
||||
| Sensor im Stopper `stSenInSep` | `"DB_Inputs".Sensors["cInBG3242"]` | E | |
|
||||
| Sensor nach Stopper `stSenSepLeft`, Beladesensor `stSenPart`, Scan-Start `stSenStartScan` | `["cInBG4465"]` | E | nur wenn verbaut |
|
||||
| Stopperventil `xOutStopper<n>` / `xOutStopper` | `=> "MB3242"` | E | BG- und MB-Nummer sind in der E-Planung meist gleich (BG3242/MB3242) |
|
||||
| `Settings.xPluggedJam` | `TRUE` | E | Stausensor verbaut? |
|
||||
| `Settings.xPluggedPart` | `FALSE` | E | Beladesensor verbaut? |
|
||||
| `Settings.xPluggedConfirm`, `xPluggedNullBT` | `FALSE` | E | |
|
||||
| `Settings.xPluggedScanner` | `TRUE` | L | Scanner am Separator (`DerivedTro.scanners`) |
|
||||
| `Settings.tDelayToNextItem` | `"TimeForSepWaiting_Short"` / `_Medium` | P (+ L) | Wahl Short/Medium hängt am Host (Kreisel/Strecke/Gefälle) |
|
||||
| `Settings.tTrailingTime`, `tHandlingTime` | `t#600ms`, `t#200ms`, `"cTimeSepTrailing"` | P | Typ-Default, IBN |
|
||||
| `Settings.tJam` | `"TimeForSepJam"` | P | Projektkonstante |
|
||||
| `Settings.tSenFree`, `tSenWait` | `T#1500ms`, `T#4000ms` | P | |
|
||||
|
||||
### Staubereiche (Jam)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Ein-/Ausgänge und Zuordnung Exit-Nr ↔ Nachbar | `arInOutJamEntr1`, `arInOutJamExit2..4` | L | je Vorgänger ein Eingang, je Nachfolger/Weichenabgang ein Ausgang (`SclJam`) |
|
||||
| Gleicher Staubereich auf beiden Seiten einer Kante | Exit von A = Entr von B | L | Identität aus der Topologie |
|
||||
| Staubereichs-DB und -Index | `"DB_JamArea0102".stJam0102_1.arCarrier` / `.stData` | K (+ E) | Nummerierung der Staubereiche ist Projektvorgabe |
|
||||
| Stausensor je Ausgang | `stInSenJam2 := "DB_Inputs".Sensors["cInBG3243"]` | E | |
|
||||
| Übergabe-Staubereich an Steuerungsgrenze | `#arCarrierHandoverJam5001`, `#stJamDataHandoverJam5001` | L + E | Steuerungsaufteilung aus der E-Planung |
|
||||
| `xInJamAtExit2/3` | – | P | NIU |
|
||||
|
||||
### Weiche (`stInSwitch<n>`, `FC_Direction`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Weichen je TRO | `stInSwitch1`, `stInSwitch2` | L | `ILS Weiche`, Ausschleuselement, Typ-Katalog |
|
||||
| `Settings.nSwitchNo` | `"cSwi107.1"` | K | |
|
||||
| `Settings.nTo1Destinations` | `3` | L | Erreichbarkeit im TRO-Graphen (`reachable_via()`); bei mehreren Weichen Planungsentscheidung |
|
||||
| Zielliste je Abgang | `cDest…` | L + P | welche Ziele: L; Zielnummern: Projekt/WCS |
|
||||
| `Settings.xPluggedExSen3/4`, `stSenExit3/4` | `FALSE` | E | Abgangssensoren verbaut? |
|
||||
| `Settings.xNewSwitch` | `TRUE` | E | Weichenbauart (ggf. über `ARTINR` im CSV prüfbar) |
|
||||
| Weichenventile `xOutSw<n>ExTo3/4`, `xOutSw1EntrTo1/2` | `=> "MB3304"` | E | |
|
||||
| `FC_Direction`-Aufruf | `nInSwitchNo := "cSwi107.1"`, `InOutCarrier := …stJam0102_1.arCarrier[1]` | K + L | Carrier aus dem Eingangs-Staubereich |
|
||||
| Vorfahrt `stInSettings.xPrioDir<n>`, `stInPriorityManagerDir<n>` | `"DB_Prozessdaten".stPriorityManager.udt_TRO107.TroIn` | L + K | Vorfahrt nötig, wo zwei Ströme zusammenlaufen (Kreuzung/Zusammenführung) |
|
||||
| `stInPriorityManager.xReleaseOk` | `TRUE` ("braucht keine Vorfahrtsregelung") | L | TRUE, wenn keine Zusammenführung |
|
||||
|
||||
### Scanner / Barcode / SSCC
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Scanner vorhanden, Anzahl | `stInBarcodeReader`, `stInBarcodeReader1/2` | L | `ILS 2.0 Scanner`, `Anzahl_Scanner` |
|
||||
| Scanner-BMK und Index | `#arCognexInterface["cScannerBX2100"]` | E | |
|
||||
| Cognex-Variablen | `"bx2100-TriggerReady"`, `-TriggerAck`, `-Results Available`, `-General Fault`, `-TriggerEnable`, `-Trigger`, `-ResultsAck` | E | aus dem BMK nach Regel bildbar, wenn die TIA-Variablentabelle dem Muster folgt |
|
||||
| Hardware-Kennung Ergebnisse | `"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1"` | E | |
|
||||
| Kein Scanner | `#stCognexInterfaceDummy` | L | |
|
||||
| `Settings.xActivateCheckASCI`, `tTimeout` | `TRUE`, `T#5s` | P | |
|
||||
| `Settings.nStation`, `nScannerPurpose` | `111`, `0` | P | WCS |
|
||||
| Zweck SSCC-Scanner, `xInPluggedSSCC` | `#stCognexIntfTro105` | E + P | Layout kennt den zweiten Scanner, nicht seinen Zweck |
|
||||
| `stSenEndMeasurement` | `["cInBG3281"]` | E | |
|
||||
| `FC_CARR_GLOB_CORRECTION` | `nInStation := 0`, Carrier aus Eingangs-Staubereich | P + L | |
|
||||
| WCS-Telegramm, HMI-SSCC-Ausgabe | `FC_Send_GOHDReqM`, `"DB_Interface_HMI".stSSCC.SSCC1107` | P | WCS-Schnittstelle |
|
||||
|
||||
### Vario (`stInVario`) und Arbeitsplatz
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Vario vorhanden | `FB_ILS_MTRO_Vario` | L | angetriebene Strecke / Variofoerderer |
|
||||
| Sensoren `stSenCarrInPos`, `stSenFinger`, `stSenLastPos`, `stSenJam`, `stSenNextSep`, `stSenSepVario` | `["cInBG3026"]` … | E | |
|
||||
| Motor `xOutVarioMotor`, Motorschutz `xInMotorProtection` | `"MA0145"`, `"FC0145"` | E | |
|
||||
| Finger-Staubereich `arInOutJamFinger` | `"DB_JamArea4103".stJam4103_1` | L + K | |
|
||||
| `Settings.tConvEmpty` | `T#45000ms` | L + P | aus Förderlänge (`L_VF_m`) und Geschwindigkeit ableitbar |
|
||||
| `Settings.nCountCarrier`, `tWaitForJog`, `tWaitForStop`, `tCarrierInPosDelay`, `tCarrierInPosEdgeDelay`, `tRasterHigh/Low`, `xJamSenDelayActive`, `tJamSenDelay`, `xContinousEmptying`, `SettingsVarioFingerMonitoring` | `1`, `T#300ms` … | P | |
|
||||
| Arbeitsplatz `stInStationSensor`, `xInReleaseStation`, `fbConvVario.xBeladenError` | `["cInBG3314"]`, `#"rTrig_Workstation 1".CLK` | E | Taster/Sensor am Arbeitsplatz |
|
||||
|
||||
### Antrieb, Sicherheit, Betriebsart
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Fördertechnik, die den TRO trägt | `xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed`; Gefälle: `TRUE` | L | Host-Kreisel aus `DerivedTro.hosts`; Instanzname nach K |
|
||||
| `xInCarouselRun2` | zweiter Kreisel | L | TRO an der Berührung zweier Kreisel |
|
||||
| `xInConveyorRunIn/Out` (PinStore) | `#fbSortConv1101`, `#fbExitConv1103` | L + K | |
|
||||
| `xInSftyOk` | `"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q` | E | Not-Halt-Kreis |
|
||||
| `stInOutMachineState` | `#stInOutControlUnitCabinetOF1` | E | Schaltschrank (OF1/OF2) |
|
||||
| `xInAutomatic`, `xInErrorQuit`, `xInQuitLocal` | `#t_Automatik_OF1 AND "SF0120"`, `"SF0002"`, `"SF0121" OR "SF0117"` | E | Bedienelemente |
|
||||
| `nInTestTarget` | `"IBN".nTestTarget111` | P | |
|
||||
|
||||
### Pin-Lager (`FB_ILS_MTRO_PinStore_Auto`) und Leerträgerpuffer (`FB_EmptyCarrBuffer`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Lagerlinien | `nNoOfLines := 19`, `arInSep[1..5]` | L | Separatoren auf Kreisel mit Pinband / `Separatorlist` |
|
||||
| Positionen je Linie, Staulänge | `nNoOfPositions := 70`, `nJamLength := 4` / `"cStorage111ExitJam2"` | L + P | aus Linienlänge und Teilung |
|
||||
| Liniensensoren `arInSep[n].stSenTrolley`, `arInJamSensor[n]`, `stInSenJamExit` | `["cInBG3270"]` | E | |
|
||||
| Linienventile `arOutSeparatorStore[n]` | `"MB3270"` | E | |
|
||||
| Pin-Separator `stInSeparatorPin`, `xOutSeparatorPin` | `["cInBG3000"]`, `"MB3000"` | E | |
|
||||
| Referenzsensor "Zappler" `stInSensorReference` | `["cInBG3001"]` | E | |
|
||||
| Encoder `stInEncoderParameter` | `#fbEncoderGF2200.nOutPosActDif` | E | |
|
||||
| `nDestGeneral`, `nConvStations`, `nStationsMax`, `nStorageThreshold` | `"cDest11100"`, `"cHooksSorterMaxPin111"` | P | WCS/Projekt |
|
||||
| `nInBufferNr`, `xInBufferReq/Release/ReleaseNext`, `xInPause` | `101`/`102` | P | Pufferlogik |
|
||||
| Lager-/Stations-DBs | `"DB_Storage1.1.1".arLine`, `"DB_ConveyorStations"`, `"DB_HandshakeStorage"` | K + P | |
|
||||
| Linien-Zeiten, Simulation | `"cTimeSepHandlingStorageLine"`, `stSimu.*` | P | |
|
||||
|
||||
### Ladeschleife (`FB_LoadingBoom_INBOUND`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Ladeschleife vorhanden, Lage | – | L | Beladestation (`ILS 2.0 BTMT Beladung`), prüfen |
|
||||
| Bedienelemente `stInControl.*` | Taster, Knebel-/Schlüsselschalter | E | |
|
||||
| Ausleger/Stützfuß `stInBoom.*`, `stInFoot.*` | Endlagen, Motorschutz, Bumper | E | |
|
||||
| Analogwerte `stInTilt.wTilt`, `stInDistance.wDistance` | Neigung, Abstand | E | |
|
||||
| Sensoren `stInSen2mThreshold`, `stInSenJamEntr/Exit1/Exit2`, `stInSenStopper1/2`, `stInSenGrid` | | E | |
|
||||
| Ausgänge `stOutStopper.*`, `stOutBoom.*`, `stOutFoot.*`, `xOutLedMovement`, `xOutLight`, `xOutLedBumper`, `xOutCarousel` | | E | |
|
||||
| Schwellwerte `nDiff`, `nZeroOk`, `nStartSlow/Fast`, `nDistanceZero`, `tDiff`, `tZero`, `nTiltComp` | | P | Inbetriebnahme |
|
||||
| `stInSettings.*` Zeiten | `tBoomInput := T#3500ms` … | P | |
|
||||
| Exit-Staubereich `arInOutCarrierExit`, `stInOutCarrierExit` | | L + K | |
|
||||
|
||||
### Zusammenfassung
|
||||
|
||||
- **Aus dem Hardwarelayout (L)** kommt die **Struktur**: welche TROs es gibt, ihr Typ
|
||||
und damit FB und Anzahl der Parameterblöcke (Separatoren, Weichen, Scanner, Vario,
|
||||
Lagerlinien), Vorgänger/Nachfolger und damit Ein-/Ausgangs-Staubereiche,
|
||||
REGION-Reihenfolge, Ziele je Weichenabgang (`nTo1Destinations`), Vorfahrt ja/nein,
|
||||
Systemgrenzen (`xIsBorner`), die tragende Fördertechnik (`xInCarouselRun`, Gefälle →
|
||||
`TRUE`), ob ein Scanner am Separator sitzt (`xPluggedScanner`) und Längen als
|
||||
Grundlage für Zeiten/Staulängen. Das meiste davon ist heute in `DerivedTro` →
|
||||
`SclTro` abgebildet; Vorfahrt, `xIsBorner` und Längen noch nicht.
|
||||
- **Aus der Elektroplanung (E)** kommen **alle Namen und Adressen**: jeder Sensor
|
||||
(`BG`), jedes Ventil (`MB`), Motor (`MA`), Motorschutz (`FC`), Taster (`SF`), Scanner
|
||||
(`BX`) samt Cognex-Variablen und Hardware-Kennung, die `xPlugged*`-Schalter (was ist
|
||||
tatsächlich verbaut), Weichenbauart, Not-Halt-Kreis, Schaltschrank und
|
||||
Steuerungsgrenzen. Im Render-Kontext sind das die `SclName` mit `todo=True`
|
||||
(`TODO(E-Planung)`). Offen ist die Brücke: die EA-Liste kennt keine Topologie; es
|
||||
braucht eine Zuordnung Layout-Bauteil (Separator-`TeileId`) ↔ BMK (`BG3242`/`MB3242`).
|
||||
Diese Brücke baut `lib/e_planning.py` über die Symbolpositionen in
|
||||
`*_positionsdraw.json` des Kabellängen-Tools (`doc/E-Planung_Bruecke.md`);
|
||||
heute kommen damit Separator-Sensor und Stopperventil in die SCL
|
||||
(`scl_skeleton --eplan`).
|
||||
- **Namenskonvention (K)**: `cMainTro`, `cSep`, `cSwi`, Staubereichs-DBs, HMI- und
|
||||
State-Pfade — sobald Nummernkreise feststehen, ohne weitere Quelle erzeugbar.
|
||||
- **Parameter (P)**: Zeiten, Projektkonstanten, WCS-Ziele/Stationen, Test-/IBN-Signale —
|
||||
weder L noch E; gehören als Typ-Defaults in den Katalog bzw. in eine
|
||||
Projektkonfiguration.
|
||||
|
||||
+106
-6
@@ -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 ──► <name>_tro_flow.dot / .svg, _tro_doc.md, _tro_error
|
||||
│ scl_skeleton.py ──► <name>_FB_Main.scl
|
||||
│ (validates the JSON against model.py; one REGION per TRO;
|
||||
│ E-planning values left as TODO(E-Planung) gaps)
|
||||
│ ▲
|
||||
│ │ --eplan <name>_eplan.json (optional)
|
||||
│ e_planning.py ◄── <drawing>_positionsdraw.json (kabellaengen)
|
||||
│ (assigns BG/MB of the E-planning drawing to the layout
|
||||
│ parts by position + numbering rule; see §5c)
|
||||
│
|
||||
└── same derivation ──► tro_annotate.py ──► <name>_annotated.dxf (VIEWER ONLY)
|
||||
(also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF
|
||||
@@ -91,6 +98,8 @@ Each CLI tool is invoked through its `bin/<name>.bat` (Windows) / `bin/<name>.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 +111,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.
|
||||
@@ -460,7 +470,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
|
||||
`<source>_FB_Main.scl`), `--start TRO`. The last one matters while the plant has
|
||||
`<source>_FB_Main.scl`), `--eplan NAME` (see below), `--start TRO`. The last one matters while the plant has
|
||||
no loading/unloading station: the flow is then a closed loop with no entry, so
|
||||
the order has to be broken somewhere arbitrary. The chosen start and the reason
|
||||
are recorded in the file header.
|
||||
@@ -492,6 +502,16 @@ times 100/50 ms), and every value that is still open carries
|
||||
`TODO(E-Planung)`. `--strict` stops (exit `1`, no SCL) if any FB type has no
|
||||
template. Without `--templates` the output is unchanged.
|
||||
|
||||
**Names from the E-planning (opt-in, `--eplan NAME`).** Reads the assignment
|
||||
`<csv>_eplan.json` written by `e_planning.py` (§5c; bare name looked up in
|
||||
`%SKEL_RESULTS%`). For every separator it assigns, the separator sensor
|
||||
(`stSenInSep := "DB_Inputs".Sensors["cInBG0642"]`) and the stopper valve
|
||||
(`xOutStopper<n> => "MB0643"`) are filled in and lose their `TODO(E-Planung)`
|
||||
marker (`SclName.todo = False`); everything else stays open. The header names
|
||||
the E-planning file. An assignment made for a different CSV (`csv_file` ≠ the
|
||||
TRO-JSON's `source`) stops the run with exit `1` — separator numbers are only
|
||||
unique within one export. Without `--eplan` the output is unchanged.
|
||||
|
||||
Tests: `tests/test_scl_context.py` (the derivations one by one),
|
||||
`tests/test_scl_templates.py` (each template rendered with the real values of
|
||||
its reference call site must equal that call site, comments and whitespace
|
||||
@@ -500,6 +520,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 `<drawing>_positionsdraw.json`.
|
||||
|
||||
```
|
||||
<name>.csv (layout) + <drawing>_positionsdraw.json (E-planning)
|
||||
│
|
||||
▼
|
||||
e_planning.py ──► <name>_eplan.json (EplanDocument, schema sps_skel-eplan-v1)
|
||||
──► <name>_eplan.md (report: assignments, gaps, drives)
|
||||
│
|
||||
▼
|
||||
scl_skeleton.py --eplan <name>_eplan.json
|
||||
```
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Read** `sensors` (field devices) and `schaltschrank_elemente` (cabinet) of
|
||||
the positionsdraw file into `EDevice` (BMK, `VERW`, `BEZEICHNUNG`,
|
||||
`KENNZEICHNUNG` → controller `UH01` / I/O station `KF107`, position). A BMK
|
||||
listed twice keeps the entry that has a `VERW`.
|
||||
2. **Registration** E-planning drawing → CSV with `dxf_registration.py`: the
|
||||
E symbols matching `[Registrierung] anker` (default `IN SEP`, the sensor that
|
||||
sits on the separator) against the CSV separators. Stored under the key
|
||||
`<csv>|<positionsdraw>` in `%SKEL_CFG%\dxf_registration.json` and re-verified
|
||||
on every run; `--re-register` forces a new derivation.
|
||||
3. **Rules** from `%SKEL_CFG%\e_planning.ini`, one section `<kind>.<role>` per
|
||||
rule, applied in file order; each E device is given out at most once, each
|
||||
role of a layout part filled at most once:
|
||||
|
||||
| key | meaning |
|
||||
|---|---|
|
||||
| `layout` | layout kind pattern (default: the part before the dot): `Separator`, `Ausschleus`, `Einschleus`, `Weiche`, `Scanner`, `Beladung`, `Entladung` |
|
||||
| `verw`, `praefix` | pattern(s) for the E attribute `VERW`, BMK prefix (`BG`, `MB`) |
|
||||
| `max_mm` | spatial: maximum distance after registration |
|
||||
| `soll_mm`, `toleranz_mm` | spatial: the symbol always sits this far from the part; candidates are ranked by their deviation from it, not by the smallest distance |
|
||||
| `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest |
|
||||
|
||||
Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`,
|
||||
`Ausschleus.stau`, `Ausschleus.ventil`, `Weiche.ventil`. `VERW` texts and the
|
||||
numbering rule are project-specific — in the reference SCL (UH01..UH05)
|
||||
sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`.
|
||||
4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the
|
||||
report.
|
||||
|
||||
Uncertain cases stay open on purpose: a gap is better than a plausible wrong
|
||||
name. The report lists, per role, the layout parts without a match, the field
|
||||
devices without a match, and distance and runner-up distance of every match
|
||||
(close values = check by hand).
|
||||
|
||||
Switches: `--file NAME` (CSV, default `export.csv`), `--eplan NAME`
|
||||
(positionsdraw JSON, required; bare name resolved against `%SKEL_DATA%`, or a
|
||||
full path), `--rules FILE` (default `%SKEL_CFG%\e_planning.ini`),
|
||||
`--re-register`. Exit `0` also when not everything was assigned; `1` on input
|
||||
errors or when the registration cannot be proven.
|
||||
|
||||
**Today only separator sensor and stopper reach the SCL.** Jam sensors and
|
||||
outfeed/switch valves are assigned in the JSON/report, but the TRO-JSON does not
|
||||
yet say which outfeed element or switch belongs to which TRO exit, so
|
||||
`scl_skeleton` cannot place them. The same holds for the storage lines of a
|
||||
`PinStore` TRO (`arInSep[n]`): their sensors are assigned but the generator does
|
||||
not emit per-line parameters yet.
|
||||
|
||||
Result on Mubea (`mubea.csv` + `Mubea_positionsdraw.json`): registration
|
||||
dx +50613.6 / dy +8555.8 mm; separator sensor 21/38, stopper 22/38 (all 20
|
||||
storage lines exact, 0–1 mm), jam sensor 20/23, outfeed valve 20/23, switch
|
||||
valve 1/1. The open separators are on the carousels K0102/K0104/K0105, where
|
||||
the E-planning drawing and the CSV apparently differ.
|
||||
|
||||
Tests: `tests/test_e_planning.py` (reading, rules, numbering vs. distance on a
|
||||
storage-line grid, target distance, uniqueness, registration, drives, the path
|
||||
into `scl_context` and `scl_skeleton --eplan`).
|
||||
|
||||
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
|
||||
|
||||
The single source of truth for everything needed to generate or draw a TRO
|
||||
|
||||
@@ -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 <zeichnung>_positionsdraw.json.
|
||||
|
||||
Hier wird daraus die Zuordnung Layout-Bauteil -> BMK:
|
||||
|
||||
1. E-Bauteile einlesen (`sensors` und `schaltschrank_elemente`).
|
||||
2. Lagebezug E-Zeichnung -> CSV mit lib/dxf_registration.py; Anker sind die
|
||||
Separator-Sensoren der E-Planung gegen die Separatoren des CSV. Der
|
||||
gepruefte Bezug wird in SKEL_CFG/dxf_registration.json abgelegt.
|
||||
3. Regeln aus SKEL_CFG/e_planning.ini anwenden, je Rolle (z. B.
|
||||
"Separator.sensor"): welche Layout-Art, welche E-Verwendung (VERW),
|
||||
welcher Hoechstabstand - oder "Nummer = Nummer des Partners + Versatz"
|
||||
(Ventil MB0643 zum Sensor BG0642). Die Zuordnung ist eindeutig: jedes
|
||||
E-Bauteil und jede Rolle eines Layout-Bauteils wird hoechstens einmal
|
||||
vergeben, die kuerzesten Abstaende zuerst.
|
||||
4. Ergebnis als <csv>_eplan.json (EplanDocument) und Bericht
|
||||
<csv>_eplan.md nach SKEL_RESULTS.
|
||||
|
||||
scl_skeleton.py --eplan liest die JSON und setzt die gefundenen Namen in die
|
||||
SCL ein (SclName mit todo=False). Was keine Regel trifft, bleibt dort
|
||||
TODO(E-Planung) - geraten wird nicht.
|
||||
|
||||
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 "<Art>.<Rolle>")."""
|
||||
|
||||
role: str # "Separator.sensor"
|
||||
layout: list[str] # Muster fuer die Layout-Art
|
||||
verw: list[str] # Muster fuer VERW
|
||||
max_mm: float | None = None # raeumlich: Hoechstabstand
|
||||
number_from: str = "" # Nummernregel: Rolle des Partners ...
|
||||
number_offset: int = 0 # ... und Versatz der Nummer
|
||||
prefix: str = "" # BMK-Praefix, leer = egal
|
||||
# Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil.
|
||||
# Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand.
|
||||
target_mm: float | None = None
|
||||
tolerance_mm: float = 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 <csv>_eplan.json."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
schema_id: str = Field(default=EPLAN_SCHEMA, alias="schema")
|
||||
created: str = ""
|
||||
csv_file: str = ""
|
||||
eplan_file: str = ""
|
||||
transform: Transform | None = None
|
||||
assignments: list[Assignment] = Field(default_factory=list)
|
||||
drives: list[Drive] = Field(default_factory=list)
|
||||
# E-Bauteile im Feld ohne Zuordnung (BMK)
|
||||
unassigned: list[str] = Field(default_factory=list)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
def for_layout(self, layout_id: str) -> dict[str, Assignment]:
|
||||
"""Rollen eines Layout-Bauteils: {"sensor": Assignment, ...}."""
|
||||
return {
|
||||
a.role.split(".", 1)[-1]: a
|
||||
for a in self.assignments if a.layout_id == layout_id
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> "EplanDocument":
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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 "<Art>.<Rolle>" ist eine Regel, in Dateireihenfolge.
|
||||
"""
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||
interpolation=None)
|
||||
parser.optionxform = str # Schluessel nicht kleinschreiben
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
parser.read_file(fh)
|
||||
anchors = _patterns(parser.get("Registrierung", "anker", fallback="IN SEP"))
|
||||
rules = []
|
||||
for section in parser.sections():
|
||||
if "." not in section:
|
||||
continue
|
||||
s = parser[section]
|
||||
max_mm = s.get("max_mm")
|
||||
soll = s.get("soll_mm")
|
||||
rules.append(Rule(
|
||||
role=section,
|
||||
layout=_patterns(s.get("layout", section.split(".", 1)[0])),
|
||||
verw=_patterns(s.get("verw", "*")),
|
||||
max_mm=float(max_mm) if max_mm else None,
|
||||
number_from=s.get("nummer_von", "").strip(),
|
||||
number_offset=int(s.get("nummer_versatz", "0")),
|
||||
prefix=s.get("praefix", "").strip(),
|
||||
target_mm=float(soll) if soll else None,
|
||||
tolerance_mm=float(s.get("toleranz_mm", "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: <csv>_eplan.json fuer "
|
||||
"scl_skeleton --eplan und ein Bericht <csv>_eplan.md.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Beispiele:\n"
|
||||
" e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json\n"
|
||||
" e_planning.bat --file mubea.csv --eplan C:\\pfad\\Mubea_positionsdraw.json "
|
||||
"--re-register\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
|
||||
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
|
||||
)
|
||||
parser.add_argument("--file", default="export.csv", metavar="NAME",
|
||||
help="CSV-Export in %%SKEL_DATA%% (oder ein Pfad). "
|
||||
"Standard: %(default)s")
|
||||
parser.add_argument("--eplan", required=True, metavar="NAME",
|
||||
help="*_positionsdraw.json der E-Planung in %%SKEL_DATA%% "
|
||||
"(oder ein Pfad)")
|
||||
parser.add_argument("--rules", metavar="DATEI",
|
||||
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
|
||||
parser.add_argument("--re-register", action="store_true",
|
||||
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
||||
"einer abgelegt ist")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+47
-13
@@ -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 tro_catalog import TroItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from e_planning import EplanDocument
|
||||
|
||||
__all__ = [
|
||||
"TODO",
|
||||
"SclName",
|
||||
@@ -65,6 +72,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<n>-Block."""
|
||||
|
||||
@@ -183,6 +200,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 = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -282,7 +301,21 @@ def _nummer(tro_id: str) -> str:
|
||||
# Kontext bauen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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____"),
|
||||
)
|
||||
|
||||
|
||||
def _tro_context(tro: TroBase, alle: dict[str, TroBase],
|
||||
eplan: EplanDocument | None = None) -> SclTro:
|
||||
items = tro.items
|
||||
n_sep = items.get(TroItem.SEPARATOR, 1)
|
||||
n_swi = items.get(TroItem.SWITCH, 0)
|
||||
@@ -291,13 +324,7 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase]) -> SclTro:
|
||||
jam_db = '"DB_JamArea____".stJam____'
|
||||
|
||||
separators = [
|
||||
SclSeparator(
|
||||
n=i,
|
||||
layout=tro.separators[i - 1] if i <= len(tro.separators) else "?",
|
||||
no=_offen("cSep_____"),
|
||||
sensor=_offen("cInBG____"),
|
||||
stopper=_offen("MB____"),
|
||||
)
|
||||
_separator(i, tro.separators[i - 1] if i <= len(tro.separators) else "?", eplan)
|
||||
for i in range(1, n_sep + 1)
|
||||
]
|
||||
switches = [
|
||||
@@ -371,13 +398,20 @@ 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}
|
||||
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) for t in geordnet],
|
||||
eplan_source=(f"{eplan.eplan_file} (Zuordnung vom {eplan.created})"
|
||||
if eplan is not None else ""),
|
||||
)
|
||||
|
||||
+56
-4
@@ -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
|
||||
@@ -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: <csv>_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 <name>.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 <name>.csv --eplan "
|
||||
"<zeichnung>_positionsdraw.json laufen lassen.", file=sys.stderr)
|
||||
return 1
|
||||
except ValidationError as exc:
|
||||
print(f"FEHLER: {eplan_file} passt nicht zum Schema '{EPLAN_SCHEMA}' "
|
||||
f"({exc.error_count()} Abweichungen).", file=sys.stderr)
|
||||
return 1
|
||||
# Eine Zuordnung zu einem anderen Layout setzt falsche Namen ein -
|
||||
# Separatornummern sind nur innerhalb eines CSV-Exports eindeutig.
|
||||
if Path(eplan.csv_file).name != Path(document.source).name:
|
||||
print(f"FEHLER: {eplan_file.name} gehoert zu '{eplan.csv_file}', die "
|
||||
f"TRO-JSON zu '{document.source}'.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
context = build_context(document, json_file.name, args.start, eplan=eplan)
|
||||
mit, ohne = template_usage(context, templates)
|
||||
if args.strict and ohne:
|
||||
print(f"FEHLER: kein Template fuer {', '.join(ohne)} (--strict) - "
|
||||
@@ -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 (<csv>_eplan.json in "
|
||||
"%%SKEL_RESULTS%% oder ein Pfad): Separator-Sensoren und "
|
||||
"Stopperventile werden eingesetzt statt als TODO markiert.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# -*- 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) -> 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"])
|
||||
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_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"}
|
||||
Reference in New Issue
Block a user