hw_import --dxf: gemeinsame Zeichnung (Layout + E-Planung) als Hardwarequelle

hw_import liest mit --dxf statt der Positionsdatei eine DXF, in der Layout
und E-Planung zusammen liegen (Dematic): jeder Block mit Attribut IO ist ein
E-Bauteil (IO/VERW/BEZEICHNUNG/KENNZEICHNUNG, Einfuegepunkt), schon in
Layout-Koordinaten (HardwareDocument.layoutCoordinates). --conveyor-from-motor
nimmt den Foerderer aus der Motornummer, wenn kein K0102/#106 dasteht
("Motor KR-MA0055" -> K0055); "ILS-VARIO" zaehlt als Strecke.

merge_hw_layout: bei layoutCoordinates kein Lagebezug (Identitaet, nichts
abgelegt); --rules findet einen blossen Namen in SKEL_CFG. Neue Regeldatei
cfg/e_planning_dematic.ini (Sensor/Stopper gleiche Nummer, Weichenventil =
Nummer des Stausensors, Abstaende an dematic_merge..dxf gemessen).

Fehler behoben: zwei Antriebs-TROs auf einem Layout-Foerderer (Dematic VF_6,
TRO04/TRO06) bekamen beide denselben Vario-Motor. Jetzt Widerspruch mit
Vorschlag; scl_context faellt dann nicht auf den Motor des Foerderers zurueck.

Dematic: 115 Namen im FB_Main (alle im TIA-Import), offene Stellen 730 -> 615.
cfg/dematic_merge_tro_ids.ini: TRO-IDs der Dematic-Anlage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 18:21:20 +02:00
parent d414bd6cf3
commit 2fbd2f119d
10 changed files with 467 additions and 38 deletions
+7 -2
View File
@@ -37,7 +37,8 @@ Template" below). What exists today is:
- `cfg/dxf_registration.json` — persisted, verified CSV↔DXF coordinate transform; - `cfg/dxf_registration.json` — persisted, verified CSV↔DXF coordinate transform;
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`); `cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`);
`cfg/e_planning.ini` — project-specific rules for assigning E-planning symbols to layout parts `cfg/e_planning.ini` — project-specific rules for assigning E-planning symbols to layout parts
(read by `merge_hw_layout.py`, also for `e_planning.py`); (read by `merge_hw_layout.py`, also for `e_planning.py`); `cfg/e_planning_dematic.ini` — the
same for Dematic (`merge_hw_layout --rules e_planning_dematic.ini`);
`cfg/hw_overrides.ini` — hand corrections of `merge_hw_layout.py` (bindings per TRO slot role, conveyors); `cfg/hw_overrides.ini` — hand corrections of `merge_hw_layout.py` (bindings per TRO slot role, conveyors);
`cfg/parameter.ini` — tolerances, thresholds, scales and drawing sizes of the `lib/` modules `cfg/parameter.ini` — tolerances, thresholds, scales and drawing sizes of the `lib/` modules
(read by `lib/parameter.py`; no numeric settings in the code, see `doc/Python_Scripts.md` §7a); (read by `lib/parameter.py`; no numeric settings in the code, see `doc/Python_Scripts.md` §7a);
@@ -221,7 +222,11 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
`*_positionsconv.json` → `<projekt>_hw.json` (`HardwareDocument`): per BMK address, `*_positionsconv.json` → `<projekt>_hw.json` (`HardwareDocument`): per BMK address,
index constant `cIn…`/`cOut…`, VERW, station, position, conveyor (`K0102`; `#106` → index constant `cIn…`/`cOut…`, VERW, station, position, conveyor (`K0102`; `#106` →
`K0106`); drives MA↔FC (+ encoder GF); cross-check of both sources. EA list and WSCAD `K0106`); drives MA↔FC (+ encoder GF); cross-check of both sources. EA list and WSCAD
are not needed. See `doc/Python_Scripts.md` §5d. are not needed. Instead of the positions file `--dxf` reads a **merged drawing** (layout +
E planning in one DXF, Dematic): every block with an `IO` attribute is a device, already
in layout coordinates (`layoutCoordinates`, no registration in the merge);
`--conveyor-from-motor` takes the conveyor from the motor number (`MA0055` → `K0055`).
See `doc/Python_Scripts.md` §5d.
7. **`lib/merge_hw_layout.py`** (implemented, stage 3) — `<csv>_tro.json` + `<projekt>_hw.json` 7. **`lib/merge_hw_layout.py`** (implemented, stage 3) — `<csv>_tro.json` + `<projekt>_hw.json`
→ `<csv>_anlage.json` (`model.AnlageDocument`: layout unchanged + `bindings[]` keyed by → `<csv>_anlage.json` (`model.AnlageDocument`: layout unchanged + `bindings[]` keyed by
TRO-ID + slot + role, `conveyors[]`). Reads no CSV: the TRO-JSON carries `parts` TRO-ID + slot + role, `conveyors[]`). Reads no CSV: the TRO-JSON carries `parts`
+37
View File
@@ -0,0 +1,37 @@
; ============================================================
; TRO-IDs zu dematic_merge.csv - erzeugt von lib/tro_flow.py am 2026-10-21
;
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
; ============================================================
[tro]
TRO01 = 34716.5, 3406.7 ; Vario | VF_4 | Sep 0074
TRO02 = 20161.7, 3406.7 ; 1Sep | VF_4 | Sep 0075
TRO03 = 42164.0, 4329.5 ; 1Sep2Swi | Separator 0048 an Ausschleuselementen 0073, 0081 (Strecken 0008, 0010) | Sep 0048
TRO04 = 40437.8, 2448.3 ; Vario | VF_6 | Sep 0082
TRO05 = 33048.6, 2448.3 ; 1Sep | VF_6 | Sep 0083
TRO06 = 26860.8, 2448.3 ; Vario | VF_6 | Sep 0084
TRO07 = 20134.0, 2448.3 ; 1Sep | VF_6 | Sep 0085
TRO08 = 20161.9, 6635.8 ; Vario | VF_5 | Sep 0078
TRO09 = 20161.9, 11560.3 ; 1Sep | VF_5 | Sep 0079
TRO10 = 19604.0, 4982.3 ; 1Sep1Swi | Separator 0043 an Ausschleuselement 0077 (Strecke 0009, Anfang) | Sep 0043
TRO11 = 3098.7, 12119.7 ; 1Sep | Separator 0038 auf Kreisel2 Bahn L | Sep 0038
TRO12 = 34990.4, 12919.7 ; 1Sep | Separator 0046 auf Kreisel2 Bahn R | Sep 0046
TRO13 = 5034.1, 12919.7 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0040, 0088, 0091, 0094, 0097, 0100, 0103, 0106, 0109, 0112, 0115
TRO14 = 35122.8, 25278.8 ; 1Sep | Separator 0047 auf Kreisel3 Bahn R | Sep 0047
TRO15 = 2703.7, 21969.7 ; Vario | VF_1 | Sep 0062
TRO16 = 2703.7, 13476.7 ; 1Sep | VF_1 | Sep 0063
TRO17 = 3542.3, 24478.8 ; 1Sep1Swi | Separator 0039 an Ausschleuselement 0061 (Strecke 0005, Anfang) | Sep 0039
TRO18 = 14674.7, 21969.7 ; Vario | VF_2 | Sep 0066
TRO19 = 14674.7, 13476.7 ; 1Sep | VF_2 | Sep 0067
TRO20 = 15472.4, 24478.8 ; 1Sep1Swi | Separator 0041 an Ausschleuselement 0065 (Strecke 0006, Anfang) | Sep 0041
TRO21 = 23232.7, 21969.7 ; Vario | VF_3 | Sep 0070
TRO22 = 23232.7, 13476.7 ; 1Sep | VF_3 | Sep 0071
TRO23 = 24062.0, 24478.8 ; 1Sep1Swi | Separator 0044 an Ausschleuselement 0069 (Strecke 0007, Anfang) | Sep 0044
TRO24 = 15934.1, 12919.8 ; PinStore_Auto | Speicher (7 Schwerkraftlinien) | Sep 0042, 0118, 0121, 0124, 0127, 0130, 0133, 0136
TRO25 = 24734.2, 12919.8 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0045, 0139, 0142, 0145, 0148, 0151, 0154, 0157, 0160, 0163, 0166
+56
View File
@@ -0,0 +1,56 @@
; ================================================================
; e_planning_dematic.ini - Zuordnungsregeln fuer Dematic
; ================================================================
; Wie e_planning.ini (Aufbau und Schluessel siehe dort), aber fuer die
; gemeinsame Zeichnung Layout + E-Planung (hw_import --dxf). Aufruf:
; merge_hw_layout.bat --tro dematic_merge_tro.json --hw dematic_hw.json
; --rules e_planning_dematic.ini
;
; Die E-Symbole liegen schon in Layout-Koordinaten, aber neben dem Bauteil:
; die Abstaende unten sind Zeichenabstaende, gemessen 2026-10-21 an
; dematic_merge..dxf + dematic_merge_export.csv (Median je Rolle in Klammern).
; Unterschiede zu Mubea:
; - Sensor und Stopper haben dieselbe Nummer (BG3151 / MB3151)
; - Stausensor und Weichenventil einer Speicherlinie haben dieselbe Nummer
; (Stau 1 = BG3031 / Weiche Abzw. 1 = MB3031)
; - VERW-Texte "In Sep n", "Sep n", "Stau n", "Weiche Abzw. n"
; ================================================================
[Registrierung]
; wird bei der gemeinsamen Zeichnung nicht gebraucht (kein Lagebezug)
anker = IN SEP*
; Sensor im Stopper -> stInSeparator<n>.stSenInSep
; (Speicherlinien 568/609 mm, Kreisel/VF 220-1340 mm; VF_1 0062 1731 mm.)
; Zugeordnet wird eins zu eins, kleinster Abstand zuerst.
[Separator.sensor]
verw = IN SEP*
praefix = BG
max_mm = 1800
; Stopperventil -> xOutStopper<n>: gleiche Nummer wie der Sensor; das
; Symbol liegt an derselben Stelle
[Separator.stopper]
verw = SEP*
praefix = MB
nummer_von = Separator.sensor
nummer_versatz = 0
max_mm = 1800
; Stausensor am Anfang einer Speicherlinie (537-554 mm vom Ausschleuselement);
; haengt an keinem TRO-Slot, dient nur als Partner fuer die Nummernregel
[Ausschleus.stau]
verw = STAU*
praefix = BG
max_mm = 1000
soll_mm = 540
; Weichenventil am Ausschleuselement -> Slot switch<n>.valve (xOutSwitch).
; Speicherlinie: Nummer = Stausensor; Kreisel-Weichen ohne Stausensor:
; raeumlich (408-865 mm, Kreisel4 0081 1015 mm)
[Ausschleus.valve]
verw = WEICHE*
praefix = MB
nummer_von = Ausschleus.stau
nummer_versatz = 0
max_mm = 1400
+52 -9
View File
@@ -746,9 +746,19 @@ positions file exists.
``` ```
*_TIA.xlsx (PLC Tags + Constants) ─┐ *_TIA.xlsx (PLC Tags + Constants) ─┐
├─► hw_import.py ──► <project>_hw.json (HardwareDocument, sps_skel-hw-v1) ├─► hw_import.py ──► <project>_hw.json (HardwareDocument, sps_skel-hw-v1)
*_positionsconv.json (optional) ────┘ ──► <project>_hw.md (report: devices, drives, checks) *_positionsconv.json (optional) ────┤ ──► <project>_hw.md (report: devices, drives, checks)
or merged drawing *.dxf (--dxf) ───┘
``` ```
- **Merged drawing** (`--dxf`, instead of `--positions`; Dematic): layout and
E planning in one DXF. Every model-space block with a BMK in the attribute
`IO` is an E device (`IO` → BMK, `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG`,
insert point → position). Blocks without `IO` (layout parts, bus modules,
the green motor labels `ILS_Motorkennzeichnung*`) are ignored — the motor
symbol is authoritative. The positions are already layout coordinates;
`layoutCoordinates: true` tells `merge_hw_layout` to skip the registration.
A BMK drawn twice is a warning (the entry with `VERW` wins).
- **TIA import**: sheets are recognised by their header row, not their name - **TIA import**: sheets are recognised by their header row, not their name
(English and German TIA exports): a sheet with `Logical Address` / (English and German TIA exports): a sheet with `Logical Address` /
`Logische Adresse` holds tags, one with `Value` / `Wert` constants. Read with `Logische Adresse` holds tags, one with `Value` / `Wert` constants. Read with
@@ -762,6 +772,10 @@ positions file exists.
`K0104` (kind `Kreisel`); `CONVEYOR-#103` and `CONVEYOR-K0103` are the same `K0104` (kind `Kreisel`); `CONVEYOR-#103` and `CONVEYOR-K0103` are the same
conveyor and both become `K0103` (kind `Strecke`) — the four-digit conveyor conveyor and both become `K0103` (kind `Strecke`) — the four-digit conveyor
number of the SCL naming scheme (`DB_JamArea0103`, `fbCarousel0102`). number of the SCL naming scheme (`DB_JamArea0103`, `fbCarousel0102`).
With `--conveyor-from-motor` (Dematic) a text without `K…`/`#…` that names a
motor gives the conveyor by motor number: `Motor KR-MA0055`, `Rad dreht
ILS-KR (MA0046)` → `K0055`, `K0046`; `ILS-VARIO` counts as kind `Strecke`.
Not for Mubea, where motor `MA0203` drives conveyor `K0103`.
- **Drives**: motor `MA` ↔ motor protection `FC` (same number), conveyor and - **Drives**: motor `MA` ↔ motor protection `FC` (same number), conveyor and
kind from their texts, encoder `GF…` of the same conveyor. kind from their texts, encoder `GF…` of the same conveyor.
- **Cross-check** (`checks`, also printed as `?` lines): BMK only in the - **Cross-check** (`checks`, also printed as `?` lines): BMK only in the
@@ -772,7 +786,8 @@ positions file exists.
through. through.
Switches: `--tia NAME` (required), `--positions NAME` (optional; without it no Switches: `--tia NAME` (required), `--positions NAME` (optional; without it no
positions), `--project NAME` (default from the positions file name: positions), `--dxf NAME` (merged drawing instead of `--positions`),
`--conveyor-from-motor`, `--project NAME` (default from the positions file name:
`Mubea_positionsconv.json` → `Mubea`). Names resolve against `%SKEL_DATA%`, `Mubea_positionsconv.json` → `Mubea`). Names resolve against `%SKEL_DATA%`,
also with a sub-folder. Exit `0` (findings are in the report), `1` on input or also with a sub-folder. Exit `0` (findings are in the report), `1` on input or
read errors. read errors.
@@ -782,10 +797,17 @@ Result on Mubea (`mubea-1_TIA.xlsx` + `Mubea_positionsconv.json`): 238 devices,
11 drives all with motor protection and conveyor (encoder `GF20000` at K0102); 11 drives all with motor protection and conveyor (encoder `GF20000` at K0102);
checks: `QA7000` only in the positions file, `BG0514` without controller. checks: `QA7000` only in the positions file, `BG0514` without controller.
Result on Dematic (`dematic_hw-2_TIA.xlsx` + `dematic_merge..dxf`,
`--conveyor-from-motor`): 1022 devices, 278 from the drawing, 267 in both
sources; 44 drives, all with motor protection and conveyor. Only in the drawing:
the network devices `BX2100–2108` (scanners), `GF2200` (encoder), `BC2050`
(RFID). 378 field devices only in TIA (areas not drawn).
Tests: `tests/test_hw_import.py` (xlsx reader with shared/inline strings, Tests: `tests/test_hw_import.py` (xlsx reader with shared/inline strings,
header-based sheet detection, conveyor texts, merge per BMK, drives, every header-based sheet detection, conveyor texts incl. motor numbers, merge per
cross-check, TIA-only run, JSON round trip, CLI with a sub-folder in BMK, drives, every cross-check, TIA-only run, JSON round trip, CLI with a
`%SKEL_DATA%`). The test builds its own small TIA xlsx. sub-folder in `%SKEL_DATA%`, merged drawing). The test builds its own small TIA
xlsx and DXF.
## 5e. `merge_hw_layout.py` — merge layout and hardware planning into the plant JSON ## 5e. `merge_hw_layout.py` — merge layout and hardware planning into the plant JSON
@@ -809,8 +831,14 @@ Steps:
layout separators), stored with the SHA-256 of the positions file. Warning layout separators), stored with the SHA-256 of the positions file. Warning
when fewer than `registration_min_share` of the separators find a partner — when fewer than `registration_min_share` of the separators find a partner —
a sign that layout and E drawing are different planning states. a sign that layout and E drawing are different planning states.
A merged drawing (`layoutCoordinates`, `hw_import --dxf`) needs none:
identity, nothing stored. The symbols' offset beside their part (Dematic
~600 mm) is drawing distance, covered by the rules' `max_mm`/`soll_mm`.
2. **Rules** from `e_planning.ini`, unchanged logic (spatial with maximum / 2. **Rules** from `e_planning.ini`, unchanged logic (spatial with maximum /
target distance, numbering rule `MB = BG + 1`). target distance, numbering rule `MB = BG + 1`). Per project another file
via `--rules` — Dematic: `cfg/e_planning_dematic.ini` (`In Sep n` / `Sep n`
with the same number, `Weiche Abzw. n` = number of `Stau n`, distances
measured on the merged drawing).
3. **Slots**: each assignment goes to the slot role of the TRO that holds the 3. **Slots**: each assignment goes to the slot role of the TRO that holds the
part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per
storage line (`line1` … `lineN`, same split as `scl_context._store`); storage line (`line1` … `lineN`, same split as `scl_context._store`);
@@ -828,7 +856,11 @@ Steps:
layout conveyor. This gives motor, motor protection and encoder per layout layout conveyor. This gives motor, motor protection and encoder per layout
conveyor (`AnlageDocument.conveyor_for("0010-R")` → the TROs on that conveyor (`AnlageDocument.conveyor_for("0010-R")` → the TROs on that
Kreisel, i.e. `xInCarouselRun`) and fills `drive.motor` / Kreisel, i.e. `xInCarouselRun`) and fills `drive.motor` /
`drive.motorProtection` of Vario TROs. `drive.motorProtection` of Vario TROs. When several TROs have a drive slot
on the same layout conveyor (Dematic VF_6: two Vario TROs), the drive is a
`widerspruch` with the conveyor's motor as a suggestion — each TRO has its
own motor; set it in `hw_overrides.ini`. `scl_skeleton` then keeps
`MA____` instead of falling back to the conveyor's motor.
5. **Corrections** from `hw_overrides.ini` (last): 5. **Corrections** from `hw_overrides.ini` (last):
`[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and `[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and
`[<csv>.conveyors] K0102 = 0010`. `[<csv>.conveyors] K0102 = 0010`.
@@ -838,7 +870,8 @@ Steps:
Every binding carries `constant` and `address` from the TIA import. Every binding carries `constant` and `address` from the TIA import.
Switches: `--tro NAME`, `--hw NAME` (both required, resolved against Switches: `--tro NAME`, `--hw NAME` (both required, resolved against
`%SKEL_RESULTS%`), `--rules FILE`, `--overrides FILE`, `--re-register`. Exit `%SKEL_RESULTS%`), `--rules FILE` (a bare name resolves against `%SKEL_CFG%`),
`--overrides FILE`, `--re-register`. Exit
`0` also when not everything was assigned; `1` on input errors, an old `0` also when not everything was assigned; `1` on input errors, an old
TRO-JSON without `parts`, or an unprovable registration. Parameters: TRO-JSON without `parts`, or an unprovable registration. Parameters:
`cfg/parameter.ini [merge_hw_layout]`. `cfg/parameter.ini [merge_hw_layout]`.
@@ -852,6 +885,14 @@ K0105 = Kreisel1 (2/3)**, each with motor and motor protection. The other
conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or
have a single vote and stay open. have a single vote and stay open.
Result on Dematic (`dematic_merge_tro.json` + `dematic_hw.json`, rules
`e_planning_dematic.ini`): 113 slot roles assigned (all 27 storage lines with
sensor and stopper, 23 of 25 separators, 5 of 6 switch valves, 6 Vario drives),
39 open (Vario sensors, scanners, VF_4/VF_6 where mechanical parts are still
missing), 4 contradictions (the two Vario TROs on VF_6). Then
`scl_skeleton --templates --strict`: 115 names inserted, all known in TIA,
open gaps 730 → 615.
**Stage 4:** `scl_skeleton --json <csv>_anlage.json` reads the plant JSON **Stage 4:** `scl_skeleton --json <csv>_anlage.json` reads the plant JSON
directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together
with `e_planning.py`. with `e_planning.py`.
@@ -860,7 +901,9 @@ Tests: `tests/test_merge_hw_layout.py` (sensor spatial / stopper by number,
contradiction, open roles, part without slot, conveyor majority with contradiction, open roles, part without slot, conveyor majority with
overlapping boxes, Vario drive, too few votes, corrections incl. moving a BMK overlapping boxes, Vario drive, too few votes, corrections incl. moving a BMK
and clearing a conveyor, storage-line slots, missing `parts`, JSON round trip, and clearing a conveyor, storage-line slots, missing `parts`, JSON round trip,
fingerprint of the stored registration, CLI end to end with a real offset). fingerprint of the stored registration, CLI end to end with a real offset,
merged drawing without registration, two drive TROs on one conveyor, Dematic
rules file).
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI) ## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
+119 -16
View File
@@ -16,13 +16,19 @@ genuegen (Befund doc/E-Planung_Bruecke.md, "Nachpruefung 2026-10-15"):
Kabellaengen-Tools (optional) - je BMK die Lage in der E-Zeichnung, die Kabellaengen-Tools (optional) - je BMK die Lage in der E-Zeichnung, die
Verwendung (VERW, z. B. "IN SEP"), Klartext und E/A-Station. Verwendung (VERW, z. B. "IN SEP"), Klartext und E/A-Station.
* **Gemeinsame Zeichnung** ``*.dxf`` (statt der Positionsdatei) - Layout
und E-Planung in einer DXF (Dematic): jeder Block mit Attribut ``IO`` ist
ein E-Bauteil (IO = BMK, VERW, BEZEICHNUNG, KENNZEICHNUNG, Einfuegepunkt).
Die Lage steht schon in Layout-Koordinaten; merge_hw_layout ermittelt
dann keinen Lagebezug (``layoutCoordinates``).
EA-Liste und WSCAD-Import bringen bei vorhandener Positionsdatei nichts EA-Liste und WSCAD-Import bringen bei vorhandener Positionsdatei nichts
dazu und werden nicht gelesen. dazu und werden nicht gelesen.
Ergebnis ``HardwareDocument`` (Schema sps_skel-hw-v1): je BMK ein Geraet mit Ergebnis ``HardwareDocument`` (Schema sps_skel-hw-v1): je BMK ein Geraet mit
beiden Seiten, die Antriebe (Motor MA <-> Motorschutz FC, Geber GF) je beiden Seiten, die Antriebe (Motor MA <-> Motorschutz FC, Geber GF) je
Foerderer der E-Planung (K0102; "#106" wird K0106), und die Gegenpruefung Foerderer der E-Planung (K0102; "#106" wird K0106; mit --conveyor-from-motor
beider Quellen. auch "MA0055" -> K0055), und die Gegenpruefung beider Quellen.
Die Zuordnung zum Layout macht merge_hw_layout.py. Die Zuordnung zum Layout macht merge_hw_layout.py.
Die xlsx-Datei wird ohne Fremdbibliothek gelesen (zipfile + XML): der Die xlsx-Datei wird ohne Fremdbibliothek gelesen (zipfile + XML): der
@@ -59,6 +65,7 @@ __all__ = [
"read_xlsx", "read_xlsx",
"load_tia", "load_tia",
"load_positionsdraw", "load_positionsdraw",
"load_dxf",
"pair_drives", "pair_drives",
"conveyor_of", "conveyor_of",
"conveyor_kind_of", "conveyor_kind_of",
@@ -79,10 +86,13 @@ _ADDRESS = re.compile(r"^%?([EIAQM])\s*(\d+)\.(\d)$", re.IGNORECASE)
_CONVEYOR_K = re.compile(r"(?<![A-Z0-9])K(\d{4})(?!\d)") _CONVEYOR_K = re.compile(r"(?<![A-Z0-9])K(\d{4})(?!\d)")
# ... bzw. "CONVEYOR #100", "CONVEYOR-#106", "CONVEYOR#110" # ... bzw. "CONVEYOR #100", "CONVEYOR-#106", "CONVEYOR#110"
_CONVEYOR_HASH = re.compile(r"#\s*(\d{3})(?!\d)") _CONVEYOR_HASH = re.compile(r"#\s*(\d{3})(?!\d)")
# ... bzw. ueber den Motor (Dematic, nur mit motor=True): "Motor KR-MA0055",
# "Rad dreht ILS-KR (MA0046)", "ILS-VARIO-MA0043" -> K0055 / K0046 / K0043
_CONVEYOR_MA = re.compile(r"(?<![A-Z0-9])MA(\d{4})(?!\d)")
# Art des Foerderers im Klartext: "KREISEL-K0102", "KR-K0104" -> Kreisel; # Art des Foerderers im Klartext: "KREISEL-K0102", "KR-K0104" -> Kreisel;
# "CONVEYOR #106", "KLINKENFOERDERER" -> Strecke # "CONVEYOR #106", "KLINKENFOERDERER", "ILS-VARIO" -> Strecke
_KIND_KREISEL = re.compile(r"(?<![A-Z])(KREISEL|KR)(?![A-Z])") _KIND_KREISEL = re.compile(r"(?<![A-Z])(KREISEL|KR)(?![A-Z])")
_KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER") _KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER|(?<![A-Z])VARIO(?![A-Z])")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -207,6 +217,9 @@ class HardwareDocument(BaseModel):
tia_sha256: str = Field(default="", alias="tiaSha256") tia_sha256: str = Field(default="", alias="tiaSha256")
positions_file: str = Field(default="", alias="positionsFile") positions_file: str = Field(default="", alias="positionsFile")
positions_sha256: str = Field(default="", alias="positionsSha256") positions_sha256: str = Field(default="", alias="positionsSha256")
# True: die Lage kommt aus der gemeinsamen Zeichnung (load_dxf) und steht
# schon in Layout-Koordinaten - merge_hw_layout ermittelt keinen Lagebezug
layout_coordinates: bool = Field(default=False, alias="layoutCoordinates")
devices: list[HwDevice] = Field(default_factory=list) devices: list[HwDevice] = Field(default_factory=list)
drives: list[Drive] = Field(default_factory=list) drives: list[Drive] = Field(default_factory=list)
# Konstanten ohne passendes Signal (z. B. Projektkonstanten) # Konstanten ohne passendes Signal (z. B. Projektkonstanten)
@@ -416,6 +429,44 @@ def load_positionsdraw(path: Path, warnings: list[str] | None = None) -> list[ED
return list(devices.values()) return list(devices.values())
def load_dxf(path: Path, warnings: list[str] | None = None) -> list[EDevice]:
"""E-Bauteile aus einer gemeinsamen Zeichnung (Layout + E-Planung) lesen.
Jeder Block im Modellbereich mit einem BMK im Attribut ``IO`` ist ein
E-Bauteil; die Lage ist der Einfuegepunkt - schon in Layout-Koordinaten.
Bloecke ohne IO (Layout-Bauteile, Busverteiler, die Motorkennzeichnung
"ILS-Kreisel MA0053") zaehlen nicht: der Motor steht im Motorsymbol.
Steht ein BMK doppelt da, gewinnt der Eintrag mit VERW.
"""
import ezdxf
warnings = warnings if warnings is not None else []
doc = ezdxf.readfile(str(path))
devices: dict[str, EDevice] = {}
count: Counter = Counter()
for insert in doc.modelspace().query("INSERT"):
attrs = {a.dxf.tag.upper(): a.dxf.text.strip() for a in insert.attribs}
bmk = attrs.get("IO", "")
if not _BMK.match(bmk):
continue
count[bmk] += 1
dev = EDevice(
bmk=bmk, verw=attrs.get("VERW", ""), bezeichnung=attrs.get("BEZEICHNUNG", ""),
kennzeichnung=attrs.get("KENNZEICHNUNG", ""),
pos=(float(insert.dxf.insert.x), float(insert.dxf.insert.y)))
old = devices.get(bmk)
if old is None or (not old.verw and dev.verw):
devices[bmk] = dev
for bmk, n in sorted(count.items()):
if n > 1:
warnings.append(f"Zeichnung: {bmk} steht {n}x in der Zeichnung "
f"- verwendet wird ein Eintrag")
if not devices:
raise ValueError(f"{path.name}: kein Block mit BMK im Attribut 'IO' gefunden "
f"- ist das die Zeichnung mit der E-Planung?")
return list(devices.values())
def _positions_warnings(path: Path) -> list[str]: def _positions_warnings(path: Path) -> list[str]:
"""Die Hinweise, die das Kabellaengen-Tool selbst in die Datei schreibt.""" """Die Hinweise, die das Kabellaengen-Tool selbst in die Datei schreibt."""
data = json.loads(path.read_text(encoding="utf-8")) data = json.loads(path.read_text(encoding="utf-8"))
@@ -433,13 +484,18 @@ def _positions_warnings(path: Path) -> list[str]:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def conveyor_of(*texts: str) -> str: def conveyor_of(*texts: str, motor: bool = False) -> str:
"""Foerderer der E-Planung aus einem Klartext als "K<NNNN>", sonst "". """Foerderer der E-Planung aus einem Klartext als "K<NNNN>", sonst "".
Die E-Planung schreibt denselben Foerderer zweifach: "CONVEYOR-K0103" Die E-Planung schreibt denselben Foerderer zweifach: "CONVEYOR-K0103"
(Motorschutz FC0203) und "CONVEYOR-#103" (Motor MA0203). Beides ist die (Motorschutz FC0203) und "CONVEYOR-#103" (Motor MA0203). Beides ist die
vierstellige Foerderernummer des SCL-Namensschemas (DB_JamArea0103, vierstellige Foerderernummer des SCL-Namensschemas (DB_JamArea0103,
fbCarousel0102) und wird zu "K0103". fbCarousel0102) und wird zu "K0103".
``motor=True`` (Dematic): ohne K/# nennt der Klartext nur den Motor
("Motor KR-MA0055", "Rad dreht ILS-KR (MA0046)"); dann ist die
Motornummer die Foerderernummer -> "K0055". Bei Mubea waere das falsch
(Motor MA0203 treibt K0103), deshalb nur auf Wunsch.
""" """
for text in texts: for text in texts:
upper = (text or "").upper() upper = (text or "").upper()
@@ -449,6 +505,11 @@ def conveyor_of(*texts: str) -> str:
m = _CONVEYOR_HASH.search(upper) m = _CONVEYOR_HASH.search(upper)
if m: if m:
return f"K{int(m.group(1)):04d}" return f"K{int(m.group(1)):04d}"
if motor:
for text in texts:
m = _CONVEYOR_MA.search((text or "").upper())
if m:
return f"K{m.group(1)}"
return "" return ""
@@ -497,10 +558,18 @@ def _constant_name(bmk: str, direction: str) -> str:
def build_hardware(tia_path: Path | None, positions_path: Path | None, def build_hardware(tia_path: Path | None, positions_path: Path | None,
project: str = "") -> HardwareDocument: project: str = "", dxf_path: Path | None = None,
"""Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen.""" conveyor_from_motor: bool = False) -> HardwareDocument:
if tia_path is None and positions_path is None: """Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen.
raise ValueError("weder TIA-Import noch Positionsdatei angegeben")
Die Lage kommt aus der Positionsdatei oder aus der gemeinsamen Zeichnung
``dxf_path`` (load_dxf), nicht aus beiden. ``conveyor_from_motor``: siehe
conveyor_of(motor=True).
"""
if tia_path is None and positions_path is None and dxf_path is None:
raise ValueError("weder TIA-Import noch Positionsdatei/Zeichnung angegeben")
if positions_path is not None and dxf_path is not None:
raise ValueError("Positionsdatei und Zeichnung zugleich - nur eine Quelle fuer die Lage")
warnings: list[str] = [] warnings: list[str] = []
checks: list[str] = [] checks: list[str] = []
@@ -512,6 +581,9 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
if positions_path is not None: if positions_path is not None:
e_devices = load_positionsdraw(positions_path, warnings) e_devices = load_positionsdraw(positions_path, warnings)
warnings.extend(_positions_warnings(positions_path)) warnings.extend(_positions_warnings(positions_path))
elif dxf_path is not None:
e_devices = load_dxf(dxf_path, warnings)
positions_path = dxf_path
tag_of = {} tag_of = {}
for t in tags: for t in tags:
@@ -546,7 +618,8 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
comment=tag.comment if tag else "", comment=tag.comment if tag else "",
constant=cname if const else "", constant=cname if const else "",
constant_value=value, constant_value=value,
conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else ""), conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else "",
motor=conveyor_from_motor),
conveyor_kind=conveyor_kind_of(e.bezeichnung if e else "", conveyor_kind=conveyor_kind_of(e.bezeichnung if e else "",
tag.comment if tag else ""), tag.comment if tag else ""),
sources=[s for s, ok in (("tia", tag is not None), ("positions", e is not None)) sources=[s for s, ok in (("tia", tag is not None), ("positions", e is not None))
@@ -561,6 +634,7 @@ def build_hardware(tia_path: Path | None, positions_path: Path | None,
tia_sha256=file_fingerprint(tia_path) if tia_path else "", tia_sha256=file_fingerprint(tia_path) if tia_path else "",
positions_file=positions_path.name if positions_path else "", positions_file=positions_path.name if positions_path else "",
positions_sha256=file_fingerprint(positions_path) if positions_path else "", positions_sha256=file_fingerprint(positions_path) if positions_path else "",
layout_coordinates=dxf_path is not None,
devices=devices, devices=devices,
drives=pair_drives(devices), drives=pair_drives(devices),
constants=[c for c in constants if c.name not in used_constants], constants=[c for c in constants if c.name not in used_constants],
@@ -627,7 +701,10 @@ def render_report(doc: HardwareDocument) -> str:
add(f"# Hardwareplanung: {doc.project}") add(f"# Hardwareplanung: {doc.project}")
add("") add("")
add(f"- TIA-Import: `{doc.tia_file or '-'}`") add(f"- TIA-Import: `{doc.tia_file or '-'}`")
add(f"- Positionsdatei: `{doc.positions_file or '-'}`") if doc.layout_coordinates:
add(f"- Gemeinsame Zeichnung (Lage in Layout-Koordinaten): `{doc.positions_file}`")
else:
add(f"- Positionsdatei: `{doc.positions_file or '-'}`")
add(f"- Erzeugt: {doc.created}") add(f"- Erzeugt: {doc.created}")
add("") add("")
add("## Geraete") add("## Geraete")
@@ -699,20 +776,30 @@ def project_name(tia_path: Path | None, positions_path: Path | None) -> str:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
import ezdxf
from material_flow import env_dir, resolve_input from material_flow import env_dir, resolve_input
args = parse_args(argv) args = parse_args(argv)
tia_path = resolve_input(args.tia) if args.tia else None tia_path = resolve_input(args.tia) if args.tia else None
positions_path = resolve_input(args.positions) if args.positions else None positions_path = resolve_input(args.positions) if args.positions else None
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path)): dxf_path = resolve_input(args.dxf) if args.dxf else None
if positions_path is not None and dxf_path is not None:
print("FEHLER: --positions und --dxf zugleich - nur eine Quelle fuer die Lage",
file=sys.stderr)
return 1
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path),
("Zeichnung", dxf_path)):
if path is not None and not path.is_file(): if path is not None and not path.is_file():
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr) print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
return 1 return 1
project = args.project or project_name(tia_path, positions_path) project = args.project or project_name(tia_path, positions_path or dxf_path)
try: try:
doc = build_hardware(tia_path, positions_path, project) doc = build_hardware(tia_path, positions_path, project, dxf_path,
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError) as exc: args.conveyor_from_motor)
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError,
ezdxf.DXFError) as exc:
print(f"FEHLER: {exc}", file=sys.stderr) print(f"FEHLER: {exc}", file=sys.stderr)
return 1 return 1
@@ -730,7 +817,10 @@ def main(argv: list[str] | None = None) -> int:
print("HARDWAREPLANUNG EINLESEN") print("HARDWAREPLANUNG EINLESEN")
print("================================================================") print("================================================================")
print(f"TIA-Import = {tia_path or '-'}") print(f"TIA-Import = {tia_path or '-'}")
print(f"Positionsdatei = {positions_path or '-'}") if dxf_path is not None:
print(f"Zeichnung = {dxf_path} (Lage in Layout-Koordinaten)")
else:
print(f"Positionsdatei = {positions_path or '-'}")
print(f"Projekt = {project}") print(f"Projekt = {project}")
print(f"Geraete = {len(doc.devices)} (TIA {n_tia}, Positionsdatei {n_pos}, " print(f"Geraete = {len(doc.devices)} (TIA {n_tia}, Positionsdatei {n_pos}, "
f"beide {sum(1 for d in doc.devices if len(d.sources) == 2)})") f"beide {sum(1 for d in doc.devices if len(d.sources) == 2)})")
@@ -763,6 +853,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
" hw_import.bat --tia Mubea/mubea-1_TIA.xlsx " " hw_import.bat --tia Mubea/mubea-1_TIA.xlsx "
"--positions Mubea/Mubea_positionsconv.json\n" "--positions Mubea/Mubea_positionsconv.json\n"
" hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea\n" " hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea\n"
" hw_import.bat --tia dematic_hw-2_TIA.xlsx --dxf dematic_merge..dxf "
"--project dematic --conveyor-from-motor\n"
"\n" "\n"
"Exit-Codes:\n" "Exit-Codes:\n"
" 0 ok (Befunde der Gegenpruefung stehen im Bericht)\n" " 0 ok (Befunde der Gegenpruefung stehen im Bericht)\n"
@@ -774,6 +866,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument("--positions", metavar="NAME", parser.add_argument("--positions", metavar="NAME",
help="*_positionsconv.json bzw. *_positionsdraw.json in " help="*_positionsconv.json bzw. *_positionsdraw.json in "
"%%SKEL_DATA%% (oder ein Pfad); ohne sie keine Lage") "%%SKEL_DATA%% (oder ein Pfad); ohne sie keine Lage")
parser.add_argument("--dxf", metavar="NAME",
help="statt --positions: gemeinsame Zeichnung (Layout + "
"E-Planung, *.dxf) in %%SKEL_DATA%% (oder ein Pfad); "
"jeder Block mit Attribut IO ist ein E-Bauteil, die Lage "
"steht schon in Layout-Koordinaten")
parser.add_argument("--conveyor-from-motor", dest="conveyor_from_motor",
action="store_true",
help="Foerderer auch aus der Motornummer im Klartext "
"(\"Motor KR-MA0055\" -> K0055), wenn kein K0102/#106 "
"dasteht (Dematic). Nicht fuer Mubea: dort treibt MA0203 "
"den Foerderer K0103.")
parser.add_argument("--project", metavar="NAME", parser.add_argument("--project", metavar="NAME",
help="Projektname fuer <projekt>_hw.json. Standard: aus dem " help="Projektname fuer <projekt>_hw.json. Standard: aus dem "
"Namen der Positionsdatei (Mubea_positionsconv.json -> Mubea)") "Namen der Positionsdatei (Mubea_positionsconv.json -> Mubea)")
+48 -6
View File
@@ -371,6 +371,19 @@ def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str,
return transform return transform
def _same_drawing(points: list[LayoutPoint], drawing: str) -> Transform:
"""Lagebezug einer gemeinsamen Zeichnung (hw_import --dxf): keiner.
Die E-Symbole stehen schon in Layout-Koordinaten. Ihr Versatz neben dem
Bauteil (Dematic ~600 mm) ist Zeichenabstand, keine Verschiebung - ein
eingepasster Bezug wuerde ihn nur verzerren. Er steckt in max_mm/soll_mm
der Regeln.
"""
n = sum(1 for p in points if p.kind == "Separator")
return Transform(dx=0.0, dy=0.0, source="zeichnung", anchor="gemeinsame Zeichnung",
matched=n, candidates=n, dxf_file=drawing)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Slots # Slots
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -529,9 +542,20 @@ def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding], def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
hw: HardwareDocument) -> list[Binding]: hw: HardwareDocument) -> list[Binding]:
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers.""" """Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers.
Tragen mehrere TROs einen Antriebs-Slot auf demselben Layout-Foerderer
(Dematic VF_6: zwei Vario-TROs, drei Vario-Motoren), hat jeder TRO seinen
eigenen Motor - welcher, sagt die Mehrheit je Foerderer nicht. Dann wird
der Antrieb ein Widerspruch (BMK als Vorschlag), Korrektur von Hand.
"""
devices = hw.by_bmk() devices = hw.by_bmk()
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"} by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
sharing: dict[str, list[str]] = {}
for tro in doc.tros:
for s in tro.slots:
if s.layout_id and any(r.rstrip("?") in _DRIVE_ROLES for r in s.roles):
sharing.setdefault(s.layout_id.split("-", 1)[0], []).append(tro.tro_id)
bindings = [] bindings = []
for tro in doc.tros: for tro in doc.tros:
for s in tro.slots: for s in tro.slots:
@@ -541,17 +565,24 @@ def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
if conv is None: if conv is None:
continue continue
roles = [r.rstrip("?") for r in s.roles] roles = [r.rstrip("?") for r in s.roles]
shared = sharing.get(s.layout_id.split("-", 1)[0], [])
for role, attr in _DRIVE_ROLES.items(): for role, attr in _DRIVE_ROLES.items():
bmk = getattr(conv, attr) bmk = getattr(conv, attr)
if role not in roles or not bmk: if role not in roles or not bmk:
continue continue
dev = devices.get(bmk) dev = devices.get(bmk)
status = "zugeordnet" if dev and dev.in_tia else "widerspruch"
note = f"Antrieb von {conv.eplan_name}"
if len(shared) > 1:
status = "widerspruch"
note = (f"{conv.layout_name or conv.layout_id} traegt {len(shared)} "
f"Antriebs-TROs ({', '.join(shared)}) - Antrieb je TRO von Hand "
f"({OVERRIDES_FILE}); Vorschlag {conv.eplan_name}")
bindings.append(Binding( bindings.append(Binding(
tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id, tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id,
bmk=bmk, constant=dev.constant if dev else "", bmk=bmk, constant=dev.constant if dev else "",
address=dev.address if dev else "", source="positionsdraw", address=dev.address if dev else "", source="positionsdraw",
rule="mehrheit", status="zugeordnet" if dev and dev.in_tia else "widerspruch", rule="mehrheit", status=status, note=note))
note=f"Antrieb von {conv.eplan_name}"))
return bindings return bindings
@@ -690,7 +721,9 @@ def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
assignments: list[Assignment] = [] assignments: list[Assignment] = []
conveyors: list[ConveyorBinding] = [] conveyors: list[ConveyorBinding] = []
if hw.field_devices(): if hw.field_devices():
if transform is None: if transform is None and hw.layout_coordinates:
transform = _same_drawing(points, hw.positions_file)
elif transform is None:
transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source, transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source,
hw.positions_file, re_register, warnings, hw.positions_file, re_register, warnings,
hw.positions_sha256) hw.positions_sha256)
@@ -856,6 +889,14 @@ def _in_results(name: str, results: Path) -> Path:
return results / name return results / name
def _in_cfg(name: str, cfg_dir: Path) -> Path:
"""Regeldatei: ein blosser Name, der hier nicht liegt, steht in %SKEL_CFG%."""
path = Path(name)
if path.is_absolute() or path.parent != Path(".") or path.is_file():
return path
return cfg_dir / name
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
from material_flow import env_dir from material_flow import env_dir
@@ -864,7 +905,7 @@ def main(argv: list[str] | None = None) -> int:
cfg_dir = env_dir("SKEL_CFG", "cfg") cfg_dir = env_dir("SKEL_CFG", "cfg")
tro_path = _in_results(args.tro, results) tro_path = _in_results(args.tro, results)
hw_path = _in_results(args.hw, results) hw_path = _in_results(args.hw, results)
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE rules_path = _in_cfg(args.rules, cfg_dir) if args.rules else cfg_dir / RULES_FILE
overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE
for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path), for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path),
("Regeln", rules_path)): ("Regeln", rules_path)):
@@ -955,7 +996,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% " help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% "
"(oder ein Pfad)") "(oder ein Pfad)")
parser.add_argument("--rules", metavar="DATEI", parser.add_argument("--rules", metavar="DATEI",
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}") help=f"Regeldatei (Name in %%SKEL_CFG%% oder ein Pfad), z. B. "
f"e_planning_dematic.ini. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
parser.add_argument("--overrides", metavar="DATEI", parser.add_argument("--overrides", metavar="DATEI",
help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} " help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} "
f"(fehlt sie, keine)") f"(fehlt sie, keine)")
+14 -3
View File
@@ -150,6 +150,13 @@ class HwSource:
geraet = self.device(tro_id, slot, role, layout) geraet = self.device(tro_id, slot, role, layout)
return _bekannt(fmt.format(bmk=geraet[0])) if geraet else _offen(offen) return _bekannt(fmt.format(bmk=geraet[0])) if geraet else _offen(offen)
def conflict(self, tro_id: str, slot: str, role: str) -> bool:
"""True, wenn die Anlagen-JSON fuer die Slot-Rolle einen Widerspruch fuehrt."""
if self.anlage is None:
return False
b = self.anlage.binding(tro_id, slot, role)
return b is not None and b.status == "widerspruch"
def conveyor(self, host: str) -> ConveyorBinding | None: def conveyor(self, host: str) -> ConveyorBinding | None:
"""Foerderer der Hardwareplanung (K0102) am Layout-Foerderer ("0010-R").""" """Foerderer der Hardwareplanung (K0102) am Layout-Foerderer ("0010-R")."""
if self.anlage is None or not host: if self.anlage is None or not host:
@@ -643,7 +650,9 @@ def _drive(tro: TroBase, hw: HwSource) -> tuple[SclName, SclName]:
Erst der Antriebs-Slot (Rollen motor/motorProtection); fehlt er, der Erst der Antriebs-Slot (Rollen motor/motorProtection); fehlt er, der
Foerderer der Hardwareplanung am Host des TRO - der Vario-Antrieb ist Foerderer der Hardwareplanung am Host des TRO - der Vario-Antrieb ist
dessen Motor (MA/FC-Paar aus hw_import). dessen Motor (MA/FC-Paar aus hw_import). Ein Widerspruch im Slot (z. B.
zwei Vario-TROs auf einem Foerderer) bleibt offen, statt den Motor des
Foerderers zu erben.
""" """
motor = hw.output(tro.tro_id, "drive", "motor", offen="MA____") motor = hw.output(tro.tro_id, "drive", "motor", offen="MA____")
schutz = hw.output(tro.tro_id, "drive", "motorProtection", schutz = hw.output(tro.tro_id, "drive", "motorProtection",
@@ -651,9 +660,11 @@ def _drive(tro: TroBase, hw: HwSource) -> tuple[SclName, SclName]:
hosts = list(getattr(tro, "hosts", []) or []) hosts = list(getattr(tro, "hosts", []) or [])
conveyor = hw.conveyor(hosts[0]) if hosts else None conveyor = hw.conveyor(hosts[0]) if hosts else None
if conveyor is not None: if conveyor is not None:
if motor.todo and conveyor.motor: if (motor.todo and conveyor.motor
and not hw.conflict(tro.tro_id, "drive", "motor")):
motor = _bekannt(conveyor.motor) motor = _bekannt(conveyor.motor)
if schutz.todo and conveyor.protection: if (schutz.todo and conveyor.protection
and not hw.conflict(tro.tro_id, "drive", "motorProtection")):
schutz = _bekannt(f'"{conveyor.protection}"') schutz = _bekannt(f'"{conveyor.protection}"')
return motor, schutz return motor, schutz
+85
View File
@@ -191,6 +191,91 @@ def test_foerderer_aus_klartext(text, name, kind):
assert conveyor_kind_of(text) == kind assert conveyor_kind_of(text) == kind
@pytest.mark.parametrize("text, name, kind", [
# Dematic nennt nur den Motor
("Motor KR-MA0055", "K0055", "Kreisel"),
("Rad dreht ILS-KR (MA0046)", "K0046", "Kreisel"),
("Motor ILS-VARIO-MA0043", "K0043", "Strecke"),
("Abfrage Finger in Position ILS-VARIO (MA0050)", "K0050", "Strecke"),
# K/# gehen vor: der Motor MA0202 treibt hier K0102
("ANTRIEB KREISEL-K0102 MA0202", "K0102", "Kreisel"),
("Separator Str. 1", "", ""),
])
def test_foerderer_aus_motornummer(text, name, kind):
assert conveyor_of(text, motor=True) == name
assert conveyor_kind_of(text) == kind
# ohne motor=True (Mubea) zaehlt die Motornummer nicht
assert conveyor_of(text) == ("K0102" if "K0102" in text else "")
# ---------------------------------------------------------------------------
# Gemeinsame Zeichnung (Layout + E-Planung in einer DXF, Dematic)
# ---------------------------------------------------------------------------
def dxf_file(tmp_path: Path) -> Path:
"""E-Symbole wie in dematic_merge..dxf: Bloecke mit IO/VERW/BEZEICHNUNG."""
import ezdxf
doc = ezdxf.new()
tags = ("SPS", "IO", "VERW", "BEZEICHNUNG", "KENNZEICHNUNG", "ID")
sym = doc.blocks.new("ILS_720002003-Separator")
for i, tag in enumerate(tags):
sym.add_attdef(tag, (0, -i * 10))
label = doc.blocks.new("ILS_Motorkennzeichnung-Kreisel")
label.add_attdef("MOTORKENNZEICHNUNG", (0, 0))
msp = doc.modelspace()
def place(x, y, **attrs):
msp.add_blockref("ILS_720002003-Separator", (x, y)).add_auto_attribs(attrs)
place(100.0, 200.0, SPS="2", IO="BG0532", VERW="In Sep 1",
BEZEICHNUNG="in Separator Str. 1", KENNZEICHNUNG="=A02+UC0103-KF1DI1")
place(100.0, 200.0, SPS="2", IO="MB0533", VERW="Sep 1",
BEZEICHNUNG="Separator Str. 1", KENNZEICHNUNG="=A02+UC0103-KF1DQ1")
place(900.0, 50.0, SPS="2", IO="MA0202", VERW="ILS-KR-MA0202",
BEZEICHNUNG="Motor KR-MA0202", KENNZEICHNUNG="=A02+UH01-KF1DQ2")
# doppelt: der Eintrag mit VERW gewinnt
place(5.0, 5.0, IO="BG0777")
place(7.0, 7.0, IO="BG0777", VERW="Stau 1")
# ohne BMK in IO: kein E-Bauteil
place(0.0, 0.0, IO="")
msp.add_blockref("ILS_Motorkennzeichnung-Kreisel", (0, 0)).add_auto_attribs(
{"MOTORKENNZEICHNUNG": "MA0999"})
path = tmp_path / "anlage_merge.dxf"
doc.saveas(path)
return path
def test_zeichnung_liest_jeden_block_mit_bmk(tmp_path):
warnings: list[str] = []
devices = {d.bmk: d for d in hw_import.load_dxf(dxf_file(tmp_path), warnings)}
assert sorted(devices) == ["BG0532", "BG0777", "MA0202", "MB0533"]
s = devices["BG0532"]
assert (s.verw, s.bezeichnung, s.pos, s.station) == (
"In Sep 1", "in Separator Str. 1", (100.0, 200.0), "KF1DI1")
assert (devices["BG0777"].verw, devices["BG0777"].pos) == ("Stau 1", (7.0, 7.0))
assert warnings == ["Zeichnung: BG0777 steht 2x in der Zeichnung - verwendet wird ein Eintrag"]
def test_zeichnung_mit_tia_lage_in_layout_koordinaten(tmp_path):
doc = build_hardware(tia_file(tmp_path), None, "X", dxf_path=dxf_file(tmp_path),
conveyor_from_motor=True)
assert doc.layout_coordinates and doc.positions_file == "anlage_merge.dxf"
s = doc.by_bmk()["BG0532"]
assert s.sources == ["tia", "positions"] and s.constant == "cInBG0532"
[drive] = doc.drives
# K0102 aus dem TIA-Kommentar geht der Motornummer vor
assert (drive.motor, drive.protection, drive.conveyor) == ("MA0202", "FC0202", "K0102")
assert HardwareDocument.from_json(doc.to_json()).layout_coordinates
def test_positionsdatei_und_zeichnung_schliessen_sich_aus(tmp_path):
with pytest.raises(ValueError, match="nur eine Quelle"):
build_hardware(tia_file(tmp_path), positions_file(tmp_path), "X",
dxf_path=dxf_file(tmp_path))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Zusammenfuehren # Zusammenfuehren
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+35 -2
View File
@@ -37,7 +37,8 @@ def _tro(tro_id, type_name, seps, hosts=()):
separators=list(seps), scanners=[], reason="Test") separators=list(seps), scanners=[], reason="Test")
def layout() -> TroDocument: def layout(second_vario: bool = False) -> TroDocument:
"""``second_vario``: TRO06 ist ein zweiter Vario-TRO auf VF_1 (Dematic VF_6)."""
parts = [_part(i, "Separator", x, y) for i, (x, y) in SEPS.items()] parts = [_part(i, "Separator", x, y) for i, (x, y) in SEPS.items()]
parts += [ parts += [
_part("0030", "Ausschleus", 0.0, 2000.0), _part("0030", "Ausschleus", 0.0, 2000.0),
@@ -52,7 +53,8 @@ def layout() -> TroDocument:
_tro("TRO03", "Vario", ["0003"], ["0020"]), _tro("TRO03", "Vario", ["0003"], ["0020"]),
_tro("TRO04", "1Sep", ["0004"]), _tro("TRO04", "1Sep", ["0004"]),
_tro("TRO05", "1Sep", ["0005"]), _tro("TRO05", "1Sep", ["0005"]),
_tro("TRO06", "1Sep", ["0006"]), _tro("TRO06", "Vario", ["0006"], ["0020"]) if second_vario
else _tro("TRO06", "1Sep", ["0006"]),
] ]
return TroDocument.from_analysis(Analysis(tros=tros), source="x.csv", parts=parts) return TroDocument.from_analysis(Analysis(tros=tros), source="x.csv", parts=parts)
@@ -183,6 +185,37 @@ def test_vario_antrieb_aus_dem_foerderer():
assert anlage.binding("TRO03", "drive", "senJam").status == "offen" assert anlage.binding("TRO03", "drive", "senJam").status == "offen"
def test_zwei_antriebs_tros_auf_einem_foerderer_sind_widerspruch():
"""Jeder Vario-TRO hat seinen eigenen Motor; die Mehrheit je Foerderer
kennt nur einen - nicht beiden denselben Motor geben."""
anlage = run(doc=layout(second_vario=True))
for tro in ("TRO03", "TRO06"):
motor = anlage.binding(tro, "drive", "motor")
assert (motor.bmk, motor.status) == ("MA0210", "widerspruch")
assert "TRO03, TRO06" in motor.note and "von Hand" in motor.note
assert anlage.binding(tro, "drive", "motorProtection").status == "widerspruch"
def test_gemeinsame_zeichnung_ohne_lagebezug(tmp_path):
"""hw_import --dxf: E-Symbole stehen schon in Layout-Koordinaten - kein
Lagebezug wird ermittelt oder abgelegt."""
hw = hardware()
hw.layout_coordinates = True
anlage = run(hw=hw, transform=None, tmp_path=tmp_path)
reg = anlage.registration
assert (reg.dx, reg.dy, reg.rotation, reg.source) == (0.0, 0.0, 0.0, "zeichnung")
assert not list(tmp_path.iterdir())
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
def test_dematic_regeln_gleiche_nummer():
rules, _ = load_rules(RULES.parent / "e_planning_dematic.ini")
by_role = {r.role: r for r in rules}
assert by_role["Separator.stopper"].number_from == "Separator.sensor"
assert by_role["Separator.stopper"].number_offset == 0
assert by_role["Ausschleus.valve"].number_offset == 0
def test_foerderer_mit_zu_wenig_stimmen_bleibt_offen(): def test_foerderer_mit_zu_wenig_stimmen_bleibt_offen():
hw = hardware() hw = hardware()
hw.devices = [d for d in hw.devices if d.bmk != "BG0802"] hw.devices = [d for d in hw.devices if d.bmk != "BG0802"]
+14
View File
@@ -120,6 +120,20 @@ def test_vario_antrieb_ueber_den_foerderer():
assert tro.vario_sensors["stSenFinger"].todo assert tro.vario_sensors["stSenFinger"].todo
def test_widerspruch_im_antrieb_erbt_nicht_den_foerderermotor():
"""Zwei Vario-TROs auf einem Foerderer: merge_hw_layout fuehrt den Antrieb
als Widerspruch - dann nicht doch den Motor des Foerderers einsetzen."""
doc = anlage()
doc.bindings += [
_bind("TRO03", "drive", "motor", "MA0210", "cOutMA0210", status="widerspruch"),
_bind("TRO03", "drive", "motorProtection", "FC0210", "cInFC0210",
status="widerspruch"),
]
tro = _by_id(_kontext(doc), "TRO03")
assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA____", True)
assert tro.motor_protection.todo
def test_ohne_hardwareplanung_alles_offen(): def test_ohne_hardwareplanung_alles_offen():
doc = anlage() doc = anlage()
ctx = build_context(doc.layout, "anlage_tro.json") ctx = build_context(doc.layout, "anlage_tro.json")