Compare commits
46
Commits
fa96a6e666
..
master
No files matched your search
@@ -68,3 +68,8 @@ ipython_config.py
|
||||
/work
|
||||
/log
|
||||
/results
|
||||
|
||||
# Claude Code: persoenliche Einstellungen und TRO-Kriterien-Waechter
|
||||
.claude/settings.local.json
|
||||
.claude/hooks/
|
||||
.claude/tro_criteria_baseline
|
||||
@@ -10,28 +10,39 @@ of Siemens SCL code** ("TRO" — Transfer Route Object — blocks for a conveyor
|
||||
control system) that can be imported directly into TIA Portal.
|
||||
|
||||
The repository has moved past pure analysis: `lib/` now holds working Python tooling that
|
||||
runs the **full DXF → JSON → SCL pipeline in a first version**. From a CSV export (ILS 2.0)
|
||||
it derives a material-flow graph and a TRO list, annotates a copy of the BricsCAD drawing
|
||||
with the result, reads the (possibly hand-edited) drawing back into a TRO-JSON, and emits an
|
||||
`FB_Main` **SCL skeleton** from that JSON. The skeleton is deliberately not a running
|
||||
runs the **full CSV → TRO-JSON → SCL pipeline in a first version**. From a CSV export (ILS
|
||||
2.0) it derives a material-flow graph and a TRO list, writes the TROs as a TRO-JSON defined
|
||||
by the shared pydantic model `lib/model.py`, and emits an `FB_Main` **SCL skeleton** from
|
||||
that JSON. A copy of the BricsCAD drawing annotated with the same TROs serves as a viewer
|
||||
for visual checking; it is no longer a data source. The skeleton is deliberately not a running
|
||||
program — every value that must come from the electrical planning is left as a visible
|
||||
`TODO(E-Planung)` gap rather than guessed. What is **not** done yet is the fuller layout-JSON
|
||||
schema (`doc/HundM/Json_Layout-Konzept.md`) as an intermediate format and the generation of
|
||||
the other per-controller blocks (`FB_CallSensors`, `FC_Direction`/`FC_Call_Jams` as
|
||||
standalone files); see "Roadmap" below. `tests/` and `examples/` are still empty scaffolding
|
||||
(see "Standard Programm Template" below). What exists today is:
|
||||
standalone files); see "Roadmap" below. `tests/` holds the first tests
|
||||
(`tests/test_model.py`); `examples/` is still empty scaffolding (see "Standard Programm
|
||||
Template" below). What exists today is:
|
||||
|
||||
- `bin/` — environment/venv management scripts, plus one `.bat`/`.sh` wrapper pair per CLI
|
||||
tool in `lib/` (see "Environment scripts" below)
|
||||
- `lib/*.py` — CLI tools and libraries that turn a CSV export + BricsCAD DXF into a
|
||||
material-flow graph, a derived TRO list/diagram, an annotated copy of the drawing, a
|
||||
TRO-JSON read back out of that drawing, and an `FB_Main` SCL skeleton.
|
||||
- `lib/*.py` — CLI tools and libraries that turn a CSV export into a material-flow graph,
|
||||
a derived TRO list/diagram, a TRO-JSON (`lib/model.py`), an `FB_Main` SCL skeleton, and
|
||||
(with the BricsCAD DXF) an annotated copy of the drawing for visual checking;
|
||||
`lib/e_planning.py` assigns the E-planning's sensors/valves (from the cable-length
|
||||
tool's `*_positionsdraw.json`) to the layout parts so the skeleton can carry real names.
|
||||
See **`doc/Python_Scripts.md`** for what each script does and which switches it takes.
|
||||
- `data/` — input CSV exports (gitignored, not committed)
|
||||
- `cad/tro_types.lsp` — generated type list for the BricsCAD `TROEDIT` dialog (written by
|
||||
`tro_annotate.py --emit-lisp`)
|
||||
- `cfg/dxf_registration.json` — persisted, verified CSV↔DXF coordinate transform;
|
||||
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`)
|
||||
`cfg/tro_overrides.ini` — hand-maintained corrections to the derived TROs (see `tro_overrides.py`);
|
||||
`cfg/e_planning.ini` — project-specific rules for assigning E-planning symbols to layout parts
|
||||
(read by `merge_hw_layout.py`, also for `e_planning.py`);
|
||||
`cfg/hw_overrides.ini` — hand corrections of `merge_hw_layout.py` (bindings per TRO slot role, conveyors);
|
||||
`cfg/parameter.ini` — tolerances, thresholds, scales and drawing sizes of the `lib/` modules
|
||||
(read by `lib/parameter.py`; no numeric settings in the code, see `doc/Python_Scripts.md` §7a);
|
||||
`cfg/<csv>_tro_ids.ini` — per-plant TRO-ID file (versioned, IDs renamed by the SPS
|
||||
programmers), created/extended by `tro_flow.py` via `lib/tro_ids.py`
|
||||
- `doc/*.md` — analysis documents that reverse-engineer the SCL patterns and propose the
|
||||
fuller JSON layout schema for the parts of the pipeline still to be built out
|
||||
|
||||
@@ -73,13 +84,12 @@ source bin/activate_venv.sh
|
||||
```
|
||||
|
||||
Requires Python 3.10+. `requirements.txt` pins `pydantic>=2.0.0` (used throughout `lib/`
|
||||
for validated models) and `ezdxf>=1.4.0` (DXF read/write, `tro_annotate.py` only); `pytest`
|
||||
is commented out as not yet needed.
|
||||
for validated models), `ezdxf>=1.4.0` (DXF read/write, `tro_annotate.py` only) and
|
||||
`pytest>=9.0.0`.
|
||||
|
||||
There is no build/lint/test command configured yet — `tests/` is empty and no test
|
||||
runner or linter is set up. Once tests exist, use `pytest` (already anticipated in
|
||||
`requirements.txt`, just commented out) and set `PYTHONPATH` via `bin/setenv` first so
|
||||
imports resolve against `lib/`.
|
||||
Tests: `python -m pytest tests/` with `.venv` active. `tests/conftest.py` puts `lib/` on
|
||||
`sys.path`, so `bin/setenv` is not required for the tests. No build step or linter is set
|
||||
up.
|
||||
|
||||
## Domain model (from doc/)
|
||||
|
||||
@@ -89,8 +99,9 @@ this order for onboarding:
|
||||
|
||||
0. **`doc/Python_Scripts.md`** — the existing CLI tooling (`lib/material_flow.py`,
|
||||
`lib/tro_flow.py`, `lib/tro_annotate.py`, `lib/tro_extract.py`, `lib/scl_skeleton.py`,
|
||||
plus the `lib/tro_catalog.py`, `lib/dxf_registration.py` and `lib/tro_overrides.py`
|
||||
libraries they share): what each script does, its switches, inputs/outputs. This is
|
||||
`lib/e_planning.py`, plus the `lib/tro_catalog.py`, `lib/model.py`, `lib/dxf_registration.py`,
|
||||
`lib/kreisel_junctions.py` (switches between touching Kreisel, from geometry and direction
|
||||
of travel) and `lib/tro_overrides.py` libraries they share): what each script does, its switches, inputs/outputs. This is
|
||||
working code, not a proposal — read it before touching `lib/`.
|
||||
1. **`doc/HundM/Json_Layout-Konzept.md`** — the core proposal: a JSON file as single source
|
||||
of truth (`plc`, `controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
|
||||
@@ -134,6 +145,16 @@ this order for onboarding:
|
||||
its generated images come from `doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py`
|
||||
(doc-tooling, not part of the generator — see `doc/Python_Scripts.md` §8) built from
|
||||
`doc/TRO_Katalog/connect.ini`.
|
||||
9. **`doc/Datenquellen_Layout_Hardwareplanung.md`** — which TRO/SCL data comes from the
|
||||
mechanical layout (CSV), which from the hardware planning (EA list, TIA import,
|
||||
WSCAD, `positionsdraw`) and which is a default; two reference call sites annotated
|
||||
line by line. The full raw field lists of both sources are in
|
||||
`doc/Klassenhierarchie.md`, chapter "Daten".
|
||||
10. **`doc/Konzept_Layout_Hardware_SCL.md`** — the target workflow (planned, not built):
|
||||
layout → `<csv>_tro.json` (with slots, `jamAreas`), hardware planning →
|
||||
`hw_import.py` → `<projekt>_hw.json`, `merge_hw_layout.py` → `<csv>_anlage.json`,
|
||||
then `scl_skeleton.py`; what happens to `e_planning.py` (split),
|
||||
`tro_annotate.py` (viewer reading the JSON) and `tro_extract.py` (dropped).
|
||||
|
||||
### Used libraries (referenced but not vendored)
|
||||
|
||||
@@ -146,19 +167,34 @@ local, fully-vendored SCL source (`FB_ILS_MTRO_Vario_workStation`, `FB_EmptyCarr
|
||||
`doc/TRO_Katalog/scl_templates/*.scl` as **read-only reference material** for pattern
|
||||
extraction, not code to execute or modify.
|
||||
|
||||
## Roadmap: DXF → JSON → SCL (first version implemented)
|
||||
## Roadmap: CSV → TRO-JSON → SCL (first version implemented)
|
||||
|
||||
The DXF → JSON → SCL pipeline now runs end-to-end in a first version. After `tro_annotate.py`
|
||||
burns the TROs into a copy of the drawing, the user can hand-edit that DXF in BricsCAD (via
|
||||
the `TRO_INSERT`/`TRO_EDIT` commands, see `doc/HundM/BricsCAD_TRO_Symbol.md`) — moving TROs,
|
||||
adding new ones, changing a type — and two further `lib/` tools carry the drawing the rest of
|
||||
the way to importable SCL:
|
||||
Since 2026-10-09 the main path is **CSV → `tro_flow.py --json` → TRO-JSON →
|
||||
`scl_skeleton.py` → SCL**. Layer order (who may import whom): `tro_catalog.py` (TRO *types*)
|
||||
← `lib/model.py` (TRO *instances* and the TRO-JSON: `TroBase` → `DerivedTro`, `Connection`,
|
||||
`TroCandidate`, `Analysis`, `TroDocument`) ← derivation (`material_flow.py`, `tro_flow.py`) ←
|
||||
outputs (`scl_skeleton.py`, `tro_annotate.py`, reports). `items`/`fbType` are always read
|
||||
from the catalog via the TRO type, never stored. See `doc/Python_Scripts.md` §6b.
|
||||
Inside the SCL output, `lib/scl_context.py` (TRO-JSON → render context, all derivations)
|
||||
sits between `model.py` and `scl_skeleton.py` (context → text only). With `--templates`
|
||||
(opt-in) REGIONs come from Jinja2 templates in `lib/scl_templates/<fbType>.scl.j2`
|
||||
(all 7 types with a reference call site: `1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`,
|
||||
`Vario`, `Vario_workStation`, `PinStore_Auto`; `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom`,
|
||||
`2Sep2Swi` stay generic until a call site exists), each round-trip tested against its reference call site
|
||||
in `doc/TRO_Katalog/scl_templates/` (`tests/test_scl_templates.py`); every line holding a
|
||||
placeholder must carry `TODO(E-Planung)`. Migration steps, decisions per template line
|
||||
and status: `doc/SCL_Templates.md`. Golden files in `tests/fixtures/scl/` pin the SCL output byte for
|
||||
byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended change.
|
||||
|
||||
1. **`lib/tro_extract.py`** (implemented) — reads the (possibly hand-edited) annotated DXF
|
||||
**only** (not the CSV) and derives a TRO-JSON (`<drawing>_tro.json`): the TROs tagged with
|
||||
XDATA, their `predecessors`/`successors` computed from the flow arrows, and plant
|
||||
coordinates resolved through the persisted registration. See `doc/Python_Scripts.md` §5a.
|
||||
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON and emits an `FB_Main` SCL
|
||||
1. **`lib/tro_flow.py --json`** (implemented) — writes `<csv>_tro.json` (schema
|
||||
`sps_skel-tro-v4`, `model.TroDocument`): TROs with their hardware `slots`, system
|
||||
boundaries `SYSn`, connections with their kind, `jamAreas` (one per TRO entry, key
|
||||
`J<Separator>`, all predecessors that feed it), findings. TRO-IDs come from `cfg/<csv>_tro_ids.ini` (`lib/tro_ids.py`): each
|
||||
TRO is found again by the position of its separators, so IDs survive a new layout export
|
||||
(TeileIds do not); `--renumber` renumbers along the material flow, `--ids` points another
|
||||
export of the same plant at the same file. See `doc/Python_Scripts.md` §4.
|
||||
2. **`lib/scl_skeleton.py`** (implemented) — reads that TRO-JSON through `TroDocument` (a
|
||||
wrong/renamed key or `items` not matching the catalog stops the run) and emits an `FB_Main` SCL
|
||||
skeleton (`<source>_FB_Main.scl`): one `REGION` per TRO with the instance call and every
|
||||
parameter line in the right order, plus inline `FC_Direction` calls for switch TROs.
|
||||
Everything derivable from the layout is filled (REGION order, FB type, parameter-block
|
||||
@@ -166,20 +202,47 @@ the way to importable SCL:
|
||||
everything that must come from the electrical planning is a `TODO(E-Planung)` gap.
|
||||
Switches `--json`, `--out`, `--start` (the last picks an entry point while the plant is a
|
||||
closed loop with no loading/unloading station). See `doc/Python_Scripts.md` §5b.
|
||||
3. **`lib/tro_annotate.py`** — **viewer only**: draws the same TROs (same `analyse_tros`
|
||||
call as `tro_flow.py`) into a copy of the DXF for a visual check in BricsCAD.
|
||||
4. **`lib/tro_extract.py`** — **parked**: reads the annotated DXF back into the old
|
||||
`sps_skel-tro-extract-v1` JSON, which `scl_skeleton.py` no longer accepts. Not deleted, not
|
||||
extended; if drawing edits (`TRO_INSERT`/`TRO_EDIT`) are ever needed again, it should
|
||||
write `model.TroDocument`. See `doc/Python_Scripts.md` §5a.
|
||||
5. **`lib/e_planning.py`** (implemented, first version) — CSV + E-planning
|
||||
`*_positionsdraw.json` → `<csv>_eplan.json` (`EplanDocument`): registers the E drawing to
|
||||
the CSV (`dxf_registration.py`) and assigns BMKs to layout parts by rules in
|
||||
`cfg/e_planning.ini` (distance, target distance, numbering rule `MB = BG + 1`).
|
||||
`scl_skeleton.py --eplan` fills separator sensor and stopper valve from it; jam sensors
|
||||
and outfeed/switch valves are assigned but not yet placed in the SCL. See
|
||||
`doc/Python_Scripts.md` §5c and `doc/E-Planung_Bruecke.md`. Since 2026-10-15 its model
|
||||
and assignment logic live in `hw_import.py`/`merge_hw_layout.py` (re-exported here).
|
||||
6. **`lib/hw_import.py`** (implemented, stage 2 of `doc/Konzept_Layout_Hardware_SCL.md`) —
|
||||
TIA import `*_TIA.xlsx` (PLC Tags + Constants, read without openpyxl) + positions file
|
||||
`*_positionsconv.json` → `<projekt>_hw.json` (`HardwareDocument`): per BMK address,
|
||||
index constant `cIn…`/`cOut…`, VERW, station, position, conveyor (`K0102`; `#106` →
|
||||
`K0106`); drives MA↔FC (+ encoder GF); cross-check of both sources. EA list and WSCAD
|
||||
are not needed. See `doc/Python_Scripts.md` §5d.
|
||||
7. **`lib/merge_hw_layout.py`** (implemented, stage 3) — `<csv>_tro.json` + `<projekt>_hw.json`
|
||||
→ `<csv>_anlage.json` (`model.AnlageDocument`: layout unchanged + `bindings[]` keyed by
|
||||
TRO-ID + slot + role, `conveyors[]`). Reads no CSV: the TRO-JSON carries `parts`
|
||||
(positions/bounding boxes, written by `tro_flow --json`). Conveyors are matched by
|
||||
majority vote of the devices naming them; corrections in `cfg/hw_overrides.ini`,
|
||||
numbers in `cfg/parameter.ini [merge_hw_layout]`. `scl_skeleton` does not read the plant JSON
|
||||
yet (stage 4). See `doc/Python_Scripts.md` §5e.
|
||||
|
||||
What is **not** built out yet, relative to the original two-tool design:
|
||||
|
||||
- the fuller **layout-JSON schema** of `doc/HundM/Json_Layout-Konzept.md` (`plc`,
|
||||
`controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
|
||||
`emptyCarrBuffers[]`, `routing`, `jamAreas[]`, `scanners[]`, `connections[]`,
|
||||
`destinations[]`) as the intermediate format — `tro_extract.py` currently emits a
|
||||
TRO-focused JSON, not this full schema;
|
||||
`destinations[]`) as the intermediate format — `model.TroDocument` is a TRO-focused
|
||||
JSON, not this full schema yet;
|
||||
- the **other per-controller blocks** (`FB_CallSensors`, `FC_Direction`/`FC_Call_Jams` as
|
||||
standalone files) — only `FB_Main` is generated today;
|
||||
- **type-based timing defaults** from `doc/TRO_Typen.md` and the `OVERRIDE_TIMING_JSON` CAD
|
||||
attribute (timings are emitted as `TODO(E-Planung): pruefen` placeholders for now);
|
||||
- a **`--skip-json`** convenience path that would read DXF + CSV directly and emit SCL
|
||||
without an intermediate JSON file.
|
||||
- `Component` classes per part type (Separator, Switch, Scanner, VarioDrive, …) on the TRO,
|
||||
driving the per-part SCL emitters in `scl_skeleton.py`.
|
||||
|
||||
Note the `create_skel.py` / `guess_fbtype()` generator sketched in `doc/HundM/suggestion.md`
|
||||
and `doc/HundM/Json_Layout-Konzept.md` §14.5 is a **separate concept** and is not implemented
|
||||
@@ -203,12 +266,12 @@ sps_skel/
|
||||
domain-model documents listed above
|
||||
examples/ example files — empty for now
|
||||
lib/ Python source, importable via SKEL_LIB on PYTHONPATH — CLI tools and
|
||||
libraries that derive material flow / TRO lists / CAD annotations from a
|
||||
layout, read the TROs back into JSON, and emit an FB_Main SCL skeleton
|
||||
(see doc/Python_Scripts.md)
|
||||
libraries that derive material flow / TRO lists from a layout, write
|
||||
them as TRO-JSON (model.py), emit an FB_Main SCL skeleton, and draw
|
||||
the TROs into the CAD drawing (see doc/Python_Scripts.md)
|
||||
log/ gitignored
|
||||
results/ gitignored — CLI tool output (.dot/.svg/.md/.dxf/.scl/.json)
|
||||
tests/ unit tests — empty for now
|
||||
tests/ unit tests (pytest) — test_model.py for the TRO-JSON model
|
||||
```
|
||||
|
||||
When extending the pipeline (the remaining pieces in "Roadmap" above), put new modules under
|
||||
|
||||
@@ -9,61 +9,110 @@
|
||||
|
||||
## Stand des Projekts
|
||||
|
||||
Aus einem CSV-Export der mechanischen Planung (ILS 2.0) werden automatisch
|
||||
Die Kette **CSV → TRO-JSON → SCL** läuft in einer ersten Version durch. Aus
|
||||
einem CSV-Export der mechanischen Planung (ILS 2.0) entstehen automatisch:
|
||||
|
||||
- der **Materialfluss-Graph** der mechanischen Objekte (Strecken, Gefällestrecken,
|
||||
Kreisel),
|
||||
- der **Materialfluss-Graph** der mechanischen Objekte (Strecken,
|
||||
Gefällestrecken, Kreisel),
|
||||
- die daraus abgeleitete **TRO-Liste** samt eigenem Flussdiagramm,
|
||||
- eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem Marker-Symbol pro
|
||||
TRO (danach in BricsCAD von Hand nachbearbeitbar — TROs verschieben,
|
||||
ergänzen, Typ ändern),
|
||||
- daraus zurückgelesen ein **JSON-Modell der TROs** (liest nur die Zeichnung,
|
||||
erfasst also auch von Hand vorgenommene Korrekturen), und
|
||||
- die **TRO-JSON** `<name>_tro.json` (Schema `sps_skel-tro-v4`, Modell
|
||||
`lib/model.py`) — die Schnittstelle zwischen Ableitung und SCL-Erzeugung,
|
||||
- daraus ein **SCL-Gerüst für `FB_Main`** — ein `REGION` pro TRO mit
|
||||
Instanzaufruf und allen Parametern in der richtigen Reihenfolge, aber
|
||||
bewusst nicht lauffähig: jeder Wert, der aus der E-Planung stammen muss
|
||||
(Sensor-/Aktoradressen, Separator-/Weichennummern, Staubereiche, ungeprüfte
|
||||
Zeiten), ist als `TODO(E-Planung)` markiert statt geraten.
|
||||
Zeiten), ist als `TODO(E-Planung)` markiert statt geraten. Optional
|
||||
(`--templates`) kommen die REGIONs aus Jinja2-Templates
|
||||
(`lib/scl_templates/<fbType>.scl.j2`, für alle 7 Typen mit Referenz-Aufrufstelle),
|
||||
- zur Sichtkontrolle eine **annotierte Kopie der BricsCAD-Zeichnung** mit einem
|
||||
Marker-Symbol pro TRO. Die Zeichnung ist nur Ansicht, keine Datenquelle für
|
||||
die SCL.
|
||||
|
||||
Diese fünf Werkzeuge liegen als CLI-Tools in `lib/` und sind über
|
||||
`bin/`-Wrapper aufrufbar (siehe unten). Details, Domänenmodell und die
|
||||
verbleibende Roadmap stehen in `doc/`, allen voran `doc/Python_Scripts.md`.
|
||||
Details stehen in `doc/`, allen voran
|
||||
[`doc/Python_Scripts.md`](doc/Python_Scripts.md) (alle Tools und Schalter) und
|
||||
[`doc/Klassenhierarchie.md`](doc/Klassenhierarchie.md) (Klassen und wozu sie da
|
||||
sind).
|
||||
|
||||
## Roadmap
|
||||
## Ablauf
|
||||
|
||||
Offen ist vor allem die JSON-Layout-Datei nach dem in
|
||||
`doc/HundM/Json_Layout-Konzept.md` beschriebenen Schema (`plc`,
|
||||
`controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `loadingBooms[]`,
|
||||
`emptyCarrBuffers[]`, `routing`, `jamAreas[]`, `scanners[]`, `connections[]`,
|
||||
`destinations[]`) als von Hand editierbares Zwischenformat vor der
|
||||
SCL-Erzeugung, sowie die Generierung der übrigen Bausteine je Steuerung
|
||||
(`FB_CallSensors`, `FC_Direction`, `FC_Call_Jams`) — `scl_skeleton.py` deckt
|
||||
bisher nur `FB_Main` ab. Siehe `CLAUDE.md` (Abschnitt "Roadmap") für den
|
||||
aktuellen Stand der Planung dazu.
|
||||
```text
|
||||
data/<name>.csv (ILS-2.0-Export)
|
||||
│
|
||||
▼
|
||||
material_flow ──► <name>_material_flow.dot/.svg/.md Materialfluss der Mechanik
|
||||
│
|
||||
▼
|
||||
tro_flow --json ──► <name>_tro.json (+ .dot/.svg/.md) TROs ableiten
|
||||
│ │
|
||||
│ ▼
|
||||
│ scl_skeleton ──► <name>_FB_Main.scl SCL-Gerüst
|
||||
│
|
||||
└── gleiche Ableitung ──► tro_annotate ──► <name>_annotated.dxf (nur Ansicht)
|
||||
(braucht zusätzlich die DXF-Zeichnung)
|
||||
```
|
||||
|
||||
Alle Ausgaben landen in `results/` (`%SKEL_RESULTS%`).
|
||||
|
||||
## Welche Skripte gibt es und wofür?
|
||||
|
||||
### Umgebung (`bin/`, je `.bat` für Windows und `.sh` für Linux/macOS)
|
||||
|
||||
Diese vier Skripte verwalten nur Umgebung und venv, sie rufen kein
|
||||
Python-Programm auf.
|
||||
|
||||
| Skript | Wofür |
|
||||
| --- | --- |
|
||||
| `setenv` | Setzt `SPS_SKEL` und die `SKEL_*`-Variablen, hängt `SKEL_LIB` an `PYTHONPATH`, legt fehlende Ordner an und gibt alles zur Kontrolle aus. `.sh` muss gesourct werden. |
|
||||
| `install_py` | Einmalig: legt `.venv` an und installiert `requirements.txt`. Bricht ab, wenn `.venv` schon existiert. |
|
||||
| `activate_venv` | Je Sitzung: aktiviert `.venv` und zeigt Python-Version und Pakete. `.sh` muss gesourct werden. |
|
||||
| `get_cmd` | Öffnet eine neue Shell mit gesetzten Umgebungsvariablen. |
|
||||
|
||||
### CLI-Tools (`lib/<name>.py`, aufgerufen über `bin/<name>.bat`/`.sh`)
|
||||
|
||||
Jeder Wrapper ruft `setenv`, aktiviert `.venv` (falls vorhanden) und startet
|
||||
das Modul mit allen übergebenen Argumenten.
|
||||
|
||||
| Tool | Wofür | Wichtige Schalter |
|
||||
| --- | --- | --- |
|
||||
| `material_flow` | Liest den CSV-Export und baut den gerichteten **Materialfluss-Graphen** der mechanischen Objekte (Strecke, Gefällestrecke, Kreisel); Separatoren/Scanner hängen an ihrem Objekt. Grundlage für `tro_flow`. | `--file`, `--tosvg`, `--use-cords`, `--doc`, `--connect`, `--track`, `--show-bbs`, `--chain-graph` |
|
||||
| `tro_flow` | Leitet aus demselben Graphen die **TROs** ab (Typ aus dem Trägerobjekt des Separators, Weichen aus den Abgängen), verknüpft sie und zeichnet ihren Fluss. Mit `--json` entsteht die TRO-JSON — der Hauptweg zur SCL. | `--file`, `--json`, `--tosvg`, `--use-cords`, `--doc`, `--connect` |
|
||||
| `scl_skeleton` | Liest die TRO-JSON (geprüft gegen `lib/model.py`) und schreibt das **`FB_Main`-Gerüst** als SCL. Mit `--templates` aus den Jinja2-Templates, sonst generisch. | `--json`, `--out`, `--start`, `--templates [DIR]`, `--strict` |
|
||||
| `tro_annotate` | Zeichnet dieselben TROs als Marker (Farbe/Form je Typ, Attribute `ID`/`TYPE`/`FB_BLOCK`) in eine **Kopie** der BricsCAD-Zeichnung — zur Sichtkontrolle. Bestimmt dafür den CSV↔DXF-Lagebezug und legt ihn in `cfg/dxf_registration.json` ab. Erzeugt mit `--emit-lisp` die Typliste `cad/tro_types.lsp` für den `TROEDIT`-Dialog. | `--file`, `--dxf`, `--flow`, `--fb`, `--legend`, `--offset`, `--re-register`, `--check`, `--emit-lisp` |
|
||||
| `tro_extract` | **Geparkt.** Liest TROs aus der annotierten Zeichnung zurück in das alte JSON-Format `sps_skel-tro-extract-v1`, das `scl_skeleton` nicht mehr annimmt. Nicht gelöscht, nicht weiterentwickelt. | `--dxf` |
|
||||
|
||||
### Bibliotheken (`lib/`, ohne eigenen Aufruf)
|
||||
|
||||
| Modul | Wofür |
|
||||
| --- | --- |
|
||||
| `tro_catalog.py` | Katalog der TRO-**Typen**: Kurzname, Siemens-FB, Bauteile, Farbgruppe, CAD-Symbol. Einzige Quelle für FB-Typ und Bauteile. |
|
||||
| `model.py` | Gemeinsames Modell der TRO-**Instanzen** und der TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`, `TroDocument`). |
|
||||
| `scl_context.py` | Wandelt die TRO-JSON in den Render-Kontext für die SCL (Reihenfolge, Bauteilblöcke, Staubereiche, Weichenziele, offene Namen). |
|
||||
| `scl_templates/` | Jinja2-Templates je FB-Typ (`<fbType>.scl.j2`) plus gemeinsame Makros; nur mit `scl_skeleton --templates`. |
|
||||
| `dxf_registration.py` | Ermittelt, prüft und speichert die Transformation zwischen CSV- und DXF-Koordinaten. |
|
||||
| `tro_overrides.py` | Liest die Handkorrekturen aus `cfg/tro_overrides.ini`; heute nur von `material_flow --connect` genutzt. |
|
||||
|
||||
### Sonstige Skripte
|
||||
|
||||
| Skript | Wofür |
|
||||
| --- | --- |
|
||||
| `doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | Doku-Werkzeug, nicht Teil der Pipeline: erzeugt die Typ-Schaubilder und Topologie-Graphen für `doc/TRO_Katalog/TRO_Katalog.md` aus `connect.ini`. Aufruf: `py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` |
|
||||
| `cad/tro_types.lsp` | Generiertes BricsCAD-LISP (Typliste für den `TROEDIT`-Dialog), erzeugt von `tro_annotate --emit-lisp`. |
|
||||
| `tests/` | pytest-Tests: TRO-Modell, Render-Kontext, Golden-Files der SCL-Ausgabe (`tests/fixtures/scl/`), Templates gegen die Referenz-Aufrufstellen. |
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
sps_skel/
|
||||
bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool
|
||||
setenv.bat/.sh Umgebungsvariablen setzen
|
||||
install_py.bat/.sh venv erstellen + pip install
|
||||
activate_venv.bat/.sh venv aktivieren
|
||||
get_cmd.bat/.sh Shell mit Umgebung oeffnen
|
||||
material_flow.bat/.sh Wrapper fuer lib/material_flow.py
|
||||
tro_flow.bat/.sh Wrapper fuer lib/tro_flow.py
|
||||
tro_annotate.bat/.sh Wrapper fuer lib/tro_annotate.py
|
||||
tro_extract.bat/.sh Wrapper fuer lib/tro_extract.py
|
||||
scl_skeleton.bat/.sh Wrapper fuer lib/scl_skeleton.py
|
||||
cad/ generiertes BricsCAD-LISP (tro_types.lsp) fuer den TROEDIT-Dialog
|
||||
cfg/ Konfigurationsdateien (INI/JSON); dxf_registration.json
|
||||
data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git)
|
||||
doc/ Dokumentation, siehe doc/Python_Scripts.md
|
||||
examples/ Beispieldateien
|
||||
lib/ Python-Quellcode (CLI-Tools + Bibliotheken)
|
||||
log/ Log-Dateien (nicht im Git)
|
||||
results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.dxf, nicht im Git)
|
||||
tests/ Unit Tests
|
||||
bin/ Umgebungsskripte + ein .bat/.sh-Wrapper-Paar je CLI-Tool
|
||||
cad/ generiertes BricsCAD-LISP (tro_types.lsp) für den TROEDIT-Dialog
|
||||
cfg/ Konfiguration: dxf_registration.json, tro_overrides.ini
|
||||
data/ Eingabedaten - CSV-Exporte, DXF (nicht im Git)
|
||||
doc/ Dokumentation, siehe doc/Python_Scripts.md
|
||||
examples/ Beispieldateien (noch leer)
|
||||
lib/ Python-Quellcode (CLI-Tools, Bibliotheken, scl_templates/)
|
||||
log/ Log-Dateien (nicht im Git)
|
||||
results/ Ergebnisse der CLI-Tools (.dot/.svg/.md/.json/.scl/.dxf, nicht im Git)
|
||||
tests/ Unit Tests (pytest)
|
||||
.gitignore
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -72,94 +121,104 @@ sps_skel/
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
| Variable | Beschreibung |
|
||||
|----------------|---------------------------|
|
||||
| `SPS_SKEL` | Wurzelverzeichnis |
|
||||
| `SKEL_BIN` | Skriptverzeichnis |
|
||||
| `SKEL_LIB` | Python-Quellcode |
|
||||
| `SKEL_CFG` | Konfigurationsdateien |
|
||||
| `SKEL_DATA` | Eingabedaten |
|
||||
| `SKEL_DOC` | Dokumentation |
|
||||
| `SKEL_LOG` | Log-Dateien |
|
||||
| `SKEL_RESULTS` | Ergebnisse |
|
||||
| `SKEL_EXAMPLES` | Beispieldateien |
|
||||
| `SKEL_TESTS` | Unit Tests |
|
||||
| `PYTHONPATH` | Erweitert um `SKEL_LIB` |
|
||||
| Variable | Beschreibung |
|
||||
|-----------------|-------------------------|
|
||||
| `SPS_SKEL` | Wurzelverzeichnis |
|
||||
| `SKEL_BIN` | Skriptverzeichnis |
|
||||
| `SKEL_LIB` | Python-Quellcode |
|
||||
| `SKEL_CFG` | Konfigurationsdateien |
|
||||
| `SKEL_DATA` | Eingabedaten |
|
||||
| `SKEL_DOC` | Dokumentation |
|
||||
| `SKEL_LOG` | Log-Dateien |
|
||||
| `SKEL_RESULTS` | Ergebnisse |
|
||||
| `SKEL_EXAMPLES` | Beispieldateien |
|
||||
| `SKEL_TESTS` | Unit Tests |
|
||||
| `PYTHONPATH` | Erweitert um `SKEL_LIB` |
|
||||
|
||||
## Installation
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Python 3.10 oder hoeher
|
||||
- Graphviz (`dot`) auf `PATH` fuer `--tosvg`, optional aber empfohlen
|
||||
- fuer `tro_annotate.py`: `ezdxf` (wird per `requirements.txt` installiert)
|
||||
- Python 3.10 oder höher
|
||||
- Pakete aus `requirements.txt`: `pydantic`, `jinja2` (SCL-Templates), `ezdxf`
|
||||
(nur `tro_annotate`), `pytest`
|
||||
- Graphviz (`dot`, für `--use-cords` bei `tro_flow` auch `neato`) auf `PATH`
|
||||
für `--tosvg` — optional, aber empfohlen
|
||||
|
||||
### Setup (Windows)
|
||||
### Setup
|
||||
|
||||
```bat
|
||||
bin\install_py.bat
|
||||
bin\install_py.bat # Windows
|
||||
```
|
||||
|
||||
### Setup (Linux / macOS)
|
||||
|
||||
```sh
|
||||
bash bin/install_py.sh
|
||||
bash bin/install_py.sh # Linux / macOS
|
||||
```
|
||||
|
||||
## Nutzung
|
||||
|
||||
### Umgebung setzen
|
||||
### Umgebung
|
||||
|
||||
```bat
|
||||
bin\setenv.bat # Windows
|
||||
source bin/setenv.sh # Linux / macOS
|
||||
bin\setenv.bat # Windows: nur Variablen setzen
|
||||
bin\activate_venv.bat # Windows: venv aktivieren
|
||||
bin\get_cmd.bat # Windows: neue Shell mit Umgebung
|
||||
```
|
||||
|
||||
### Shell mit gesetzten Variablen oeffnen
|
||||
|
||||
```bat
|
||||
bin\get_cmd.bat # Windows
|
||||
source bin/get_cmd.sh # Linux / macOS
|
||||
```sh
|
||||
source bin/setenv.sh
|
||||
source bin/activate_venv.sh
|
||||
bash bin/get_cmd.sh
|
||||
```
|
||||
|
||||
### venv aktivieren
|
||||
### Typischer Durchlauf
|
||||
|
||||
`--file`/`--dxf` nehmen einen Dateinamen (aufgelöst gegen `%SKEL_DATA%`) oder
|
||||
einen vollen Pfad; `--json` sucht in `%SKEL_RESULTS%`.
|
||||
|
||||
```bat
|
||||
bin\activate_venv.bat # Windows
|
||||
source bin/activate_venv.sh # Linux / macOS
|
||||
```
|
||||
|
||||
### CLI-Tools ausführen
|
||||
|
||||
Jedes Tool wird über seinen `bin/<name>.bat`/`.sh`-Wrapper aufgerufen; dieser
|
||||
setzt die Umgebung, aktiviert `.venv` und ruft dann das Python-Modul auf.
|
||||
`--file`/`--dxf` akzeptieren entweder einen Dateinamen (wird gegen
|
||||
`%SKEL_DATA%` aufgelöst) oder einen vollen Pfad; Ausgaben landen in
|
||||
`%SKEL_RESULTS%`.
|
||||
|
||||
```bat
|
||||
REM 1) Materialfluss der mechanischen Objekte
|
||||
REM 1) Materialfluss der mechanischen Objekte (optional, zur Kontrolle)
|
||||
bin\material_flow.bat --file mubea.csv --tosvg --doc
|
||||
|
||||
REM 2) TRO-Liste + eigenes Flussdiagramm ableiten
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --doc
|
||||
REM 2) TROs ableiten und als TRO-JSON schreiben
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --doc --json
|
||||
|
||||
REM 3) TROs als Marker-Symbole in eine Kopie der CAD-Zeichnung einbrennen
|
||||
REM 3) FB_Main-SCL-Geruest erzeugen (generisch bzw. aus Templates)
|
||||
bin\scl_skeleton.bat --json mubea_tro.json
|
||||
bin\scl_skeleton.bat --json mubea_tro.json --templates
|
||||
|
||||
REM 4) Optional: TROs zur Sichtkontrolle in eine Kopie der Zeichnung
|
||||
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
|
||||
|
||||
REM (Zeichnung optional in BricsCAD von Hand nachbearbeiten)
|
||||
|
||||
REM 4) TROs aus der (ggf. bearbeiteten) Zeichnung zurueck in JSON lesen
|
||||
bin\tro_extract.bat --dxf export_annotated.dxf
|
||||
|
||||
REM 5) FB_Main-SCL-Geruest aus dem JSON erzeugen
|
||||
bin\scl_skeleton.bat --json export_tro.json --start TRO07
|
||||
```
|
||||
|
||||
Alle Switches, Ausgabedateien und Exit-Codes der drei Tools sind in
|
||||
[`doc/Python_Scripts.md`](doc/Python_Scripts.md) dokumentiert; das
|
||||
Domänenmodell (TRO-Typen, geplantes JSON-Layout, I/O-Listen-Analyse) findet
|
||||
sich in den übrigen Dokumenten unter `doc/`.
|
||||
`--start TRO07` bei `scl_skeleton` ist nur nötig, solange die Anlage ein
|
||||
geschlossener Kreis ohne Be-/Entladestation ist. Danach im erzeugten
|
||||
`*_FB_Main.scl` nach `TODO(E-Planung)` suchen — das sind die Stellen, die aus
|
||||
der E-Planung gefüllt werden müssen.
|
||||
|
||||
### Tests
|
||||
|
||||
```bat
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
mit aktivem `.venv`. Die Golden-Files in `tests/fixtures/scl/` legen die
|
||||
SCL-Ausgabe byteweise fest; neu schreiben (`SKEL_UPDATE_GOLDEN=1`) nur bei einer
|
||||
geprüften, gewollten Änderung.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Noch offen:
|
||||
|
||||
- das volle **JSON-Layout-Schema** aus `doc/HundM/Json_Layout-Konzept.md`
|
||||
(`plc`, `controlUnits`, `sensors[]`, `conveyors[]`, `tros[]`, `jamAreas[]`,
|
||||
`routing`, …) als Zwischenformat — die TRO-JSON ist bisher TRO-zentriert;
|
||||
- die übrigen Bausteine je Steuerung (`FB_CallSensors`, `FC_Direction`,
|
||||
`FC_Call_Jams` als eigene Dateien) — erzeugt wird bisher nur `FB_Main`;
|
||||
- Templates für die weiteren FB-Typen (siehe `doc/SCL_Templates.md`);
|
||||
- typabhängige Zeit-Defaults statt `TODO(E-Planung): pruefen`.
|
||||
|
||||
Details: Abschnitt "Roadmap" in `CLAUDE.md`.
|
||||
|
||||
## Lizenz
|
||||
|
||||
|
||||
@@ -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,33 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Hardwareplanung einlesen (TIA-Import + Positionsdatei)
|
||||
REM ================================================================
|
||||
REM Aufruf:
|
||||
REM bin\hw_import.bat --tia Mubea\mubea-1_TIA.xlsx --positions Mubea\Mubea_positionsconv.json
|
||||
REM bin\hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea
|
||||
REM
|
||||
REM TIA-Import und *_positionsconv.json bzw. *_positionsdraw.json werden in
|
||||
REM %SKEL_DATA% gesucht (auch mit Unterordner, oder als Pfad angegeben).
|
||||
REM Ergebnis <projekt>_hw.json und Bericht <projekt>_hw.md in %SKEL_RESULTS%.
|
||||
REM
|
||||
REM Weiter: bin\merge_hw_layout.bat --tro <csv>_tro.json --hw <projekt>_hw.json
|
||||
REM ================================================================
|
||||
|
||||
setlocal
|
||||
|
||||
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
call "%~dp0setenv.bat" >nul
|
||||
|
||||
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
|
||||
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
|
||||
)
|
||||
|
||||
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
|
||||
set "PY=py"
|
||||
py --version >nul 2>&1 || set "PY=python"
|
||||
|
||||
"%PY%" "%SKEL_LIB%\hw_import.py" %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# ================================================================
|
||||
# SPS_SKEL - Hardwareplanung einlesen (TIA-Import + Positionsdatei)
|
||||
# ================================================================
|
||||
# Aufruf:
|
||||
# bash bin/hw_import.sh --tia Mubea/mubea-1_TIA.xlsx --positions Mubea/Mubea_positionsconv.json
|
||||
# bash bin/hw_import.sh --tia mubea-1_TIA.xlsx --project Mubea
|
||||
#
|
||||
# TIA-Import und *_positionsconv.json bzw. *_positionsdraw.json werden in
|
||||
# $SKEL_DATA gesucht (auch mit Unterordner, oder als Pfad angegeben).
|
||||
# Ergebnis <projekt>_hw.json und Bericht <projekt>_hw.md in $SKEL_RESULTS.
|
||||
#
|
||||
# Weiter: bin/merge_hw_layout.sh --tro <csv>_tro.json --hw <projekt>_hw.json
|
||||
# ================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
# shellcheck source=/dev/null
|
||||
. "$SCRIPT_DIR/setenv.sh" >/dev/null
|
||||
|
||||
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$SPS_SKEL/.venv/bin/activate"
|
||||
fi
|
||||
|
||||
# Python-Interpreter bestimmen
|
||||
PY="python3"
|
||||
command -v python3 >/dev/null 2>&1 || PY="python"
|
||||
|
||||
exec "$PY" "$SKEL_LIB/hw_import.py" "$@"
|
||||
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Layout und Hardwareplanung zusammenfuehren
|
||||
REM ================================================================
|
||||
REM Aufruf:
|
||||
REM bin\merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json
|
||||
REM bin\merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json --re-register
|
||||
REM
|
||||
REM TRO-JSON (tro_flow --json) und Hardware-JSON (hw_import) werden in
|
||||
REM %SKEL_RESULTS% gesucht. Regeln: %SKEL_CFG%\e_planning.ini,
|
||||
REM Handkorrekturen: %SKEL_CFG%\hw_overrides.ini.
|
||||
REM Ergebnis <csv>_anlage.json und Bericht <csv>_anlage.md in %SKEL_RESULTS%.
|
||||
REM
|
||||
REM Vorher: bin\tro_flow.bat --file <csv> --json und bin\hw_import.bat
|
||||
REM ================================================================
|
||||
|
||||
setlocal
|
||||
|
||||
REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
call "%~dp0setenv.bat" >nul
|
||||
|
||||
REM Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" (
|
||||
call "%SPS_SKEL%\.venv\Scripts\activate.bat"
|
||||
)
|
||||
|
||||
REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python)
|
||||
set "PY=py"
|
||||
py --version >nul 2>&1 || set "PY=python"
|
||||
|
||||
"%PY%" "%SKEL_LIB%\merge_hw_layout.py" %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
endlocal & exit /b %RC%
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# ================================================================
|
||||
# SPS_SKEL - Layout und Hardwareplanung zusammenfuehren
|
||||
# ================================================================
|
||||
# Aufruf:
|
||||
# bash bin/merge_hw_layout.sh --tro mubea_tro.json --hw Mubea_hw.json
|
||||
# bash bin/merge_hw_layout.sh --tro mubea_tro.json --hw Mubea_hw.json --re-register
|
||||
#
|
||||
# TRO-JSON (tro_flow --json) und Hardware-JSON (hw_import) werden in
|
||||
# $SKEL_RESULTS gesucht. Regeln: $SKEL_CFG/e_planning.ini,
|
||||
# Handkorrekturen: $SKEL_CFG/hw_overrides.ini.
|
||||
# Ergebnis <csv>_anlage.json und Bericht <csv>_anlage.md in $SKEL_RESULTS.
|
||||
#
|
||||
# Vorher: bin/tro_flow.sh --file <csv> --json und bin/hw_import.sh
|
||||
# ================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_LIB, PYTHONPATH ...)
|
||||
# shellcheck source=/dev/null
|
||||
. "$SCRIPT_DIR/setenv.sh" >/dev/null
|
||||
|
||||
# Virtuelle Umgebung nutzen, falls vorhanden (pydantic wird benoetigt)
|
||||
if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$SPS_SKEL/.venv/bin/activate"
|
||||
fi
|
||||
|
||||
# Python-Interpreter bestimmen
|
||||
PY="python3"
|
||||
command -v python3 >/dev/null 2>&1 || PY="python"
|
||||
|
||||
exec "$PY" "$SKEL_LIB/merge_hw_layout.py" "$@"
|
||||
@@ -3,14 +3,14 @@ REM ================================================================
|
||||
REM SPS_SKEL - FB_Main-Geruest als SCL erzeugen
|
||||
REM ================================================================
|
||||
REM Aufruf:
|
||||
REM bin\tro_flow.bat --file mubea.csv
|
||||
REM bin\tro_flow.bat --file mubea.csv --tosvg
|
||||
REM bin\tro_flow.bat --file mubea.csv --tosvg --doc
|
||||
REM bin\scl_skeleton.bat
|
||||
REM bin\scl_skeleton.bat --json mubea_tro.json
|
||||
REM bin\scl_skeleton.bat --json mubea_tro.json --out FB_Main.scl
|
||||
REM
|
||||
REM Die JSON-Datei wird in %SKEL_RESULTS% gesucht, die SCL-Datei wird
|
||||
REM ebenfalls dort abgelegt.
|
||||
REM
|
||||
REM Schwesterprogramm: bin\material_flow.bat (Materialfluss der Objekte)
|
||||
REM Vorstufe: bin\tro_flow.bat --file <name>.csv --json (CSV -> TRO-JSON)
|
||||
REM ================================================================
|
||||
|
||||
setlocal
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
# Die JSON-Datei wird in $SKEL_RESULTS gesucht, die SCL-Datei wird
|
||||
# ebenfalls dort abgelegt.
|
||||
#
|
||||
# Vorstufe: bin/tro_extract.sh (Zeichnung -> JSON)
|
||||
# Vorstufe: bin/tro_flow.sh --file <name>.csv --json (CSV -> TRO-JSON)
|
||||
# ================================================================
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -6,6 +6,7 @@ REM Aufruf:
|
||||
REM bin\tro_flow.bat --file mubea.csv
|
||||
REM bin\tro_flow.bat --file mubea.csv --tosvg
|
||||
REM bin\tro_flow.bat --file mubea.csv --tosvg --doc
|
||||
REM bin\tro_flow.bat --file mubea.csv --json (TRO-JSON fuer scl_skeleton)
|
||||
REM
|
||||
REM Die CSV-Datei wird in %SKEL_DATA% gesucht, DOT, SVG, Doku und
|
||||
REM Fehlerdatei werden in %SKEL_RESULTS% abgelegt.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# bash bin/tro_flow.sh --file mubea.csv
|
||||
# bash bin/tro_flow.sh --file mubea.csv --tosvg
|
||||
# bash bin/tro_flow.sh --file mubea.csv --tosvg --doc
|
||||
# bash bin/tro_flow.sh --file mubea.csv --json (TRO-JSON fuer scl_skeleton)
|
||||
#
|
||||
# Die CSV-Datei wird in $SKEL_DATA gesucht, DOT, SVG, Doku und
|
||||
# Fehlerdatei werden in $SKEL_RESULTS abgelegt.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
;; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp
|
||||
;; Quelle: TRO_CATALOG in lib/tro_catalog.py - nicht manuell aendern.
|
||||
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi"))
|
||||
(setq *TRO-TYPES* (list "1Sep" "1Sep1Swi" "1Sep2Swi" "1Sep_SSCC" "Vario" "PinStore_Auto" "Vario_workStation" "EmptyCarrBuffer" "LoadingBoom" "2Sep1Swi" "2Sep2Swi" "3Sep2Swi"))
|
||||
(princ)
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"mubea.csv|500573_60_1.dxf": {
|
||||
"dx": 50905.678327433096,
|
||||
"dx": 50833.7283274331,
|
||||
"dy": 8414.490373798908,
|
||||
"rotation": 0.0,
|
||||
"source": "auto",
|
||||
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:4, Separator:31; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
|
||||
"matched": 34,
|
||||
"candidates": 35,
|
||||
"residual_max": 452.04557170439375,
|
||||
"residual_mean": 102.6006793267277,
|
||||
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:8, Separator:38; 22 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
|
||||
"matched": 36,
|
||||
"candidates": 46,
|
||||
"residual_max": 487.77757110283585,
|
||||
"residual_mean": 93.12692369578822,
|
||||
"csv_file": "mubea.csv",
|
||||
"dxf_file": "500573_60_1.dxf",
|
||||
"created": "2026-07-30 15:32"
|
||||
"created": "2026-10-02 12:45"
|
||||
},
|
||||
"export.csv|Mubea.dxf": {
|
||||
"dx": 59.950000000000045,
|
||||
@@ -26,5 +26,62 @@
|
||||
"csv_file": "export.csv",
|
||||
"dxf_file": "Mubea.dxf",
|
||||
"created": "2026-08-12 10:09"
|
||||
},
|
||||
"HundM05.csv|HundM05.dxf": {
|
||||
"dx": 0.0,
|
||||
"dy": 0.0,
|
||||
"rotation": 0.0,
|
||||
"source": "cli",
|
||||
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
|
||||
"matched": 14,
|
||||
"candidates": 26,
|
||||
"residual_max": 360.0,
|
||||
"residual_mean": 164.97465794329284,
|
||||
"csv_file": "HundM05.csv",
|
||||
"dxf_file": "HundM05.dxf",
|
||||
"created": "2026-10-02 13:40"
|
||||
},
|
||||
"mubea.csv|mubea.dxf": {
|
||||
"dx": 0.0,
|
||||
"dy": 0.0,
|
||||
"rotation": 0.0,
|
||||
"source": "cli",
|
||||
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
|
||||
"matched": 20,
|
||||
"candidates": 46,
|
||||
"residual_max": 360.0,
|
||||
"residual_mean": 256.77327549812964,
|
||||
"csv_file": "mubea.csv",
|
||||
"dxf_file": "mubea.dxf",
|
||||
"created": "2026-10-02 13:40"
|
||||
},
|
||||
"dematic.csv|dematic_TST.dxf": {
|
||||
"dx": 0.0,
|
||||
"dy": 360.0,
|
||||
"rotation": 0.0,
|
||||
"source": "auto",
|
||||
"anchor": "6 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:6, Separator:52; 6 Stimmen im 100-mm-Raster, 2 Kandidaten geprueft",
|
||||
"matched": 17,
|
||||
"candidates": 58,
|
||||
"residual_max": 419.9499999999998,
|
||||
"residual_mean": 235.56575392994918,
|
||||
"csv_file": "dematic.csv",
|
||||
"dxf_file": "dematic_TST.dxf",
|
||||
"created": "2026-10-06 14:19"
|
||||
},
|
||||
"mubea.csv|Mubea_positionsconv.json": {
|
||||
"dx": -151.4000000000001,
|
||||
"dy": -696.5699999999997,
|
||||
"rotation": 0.0,
|
||||
"source": "auto",
|
||||
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Separator:38; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
|
||||
"matched": 23,
|
||||
"candidates": 38,
|
||||
"residual_max": 541.020430390572,
|
||||
"residual_mean": 52.985147546665765,
|
||||
"csv_file": "mubea.csv",
|
||||
"dxf_file": "Mubea_positionsconv.json",
|
||||
"created": "2026-10-15 13:15",
|
||||
"fingerprint": "085df1af63e4b71d8a51301ea0c6e7812d415b5efd0c6b409c58cf2cb77ba4e2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
; ================================================================
|
||||
; e_planning.ini - Regeln fuer lib/e_planning.py
|
||||
; ================================================================
|
||||
; Ordnet die Sensoren/Ventile der E-Planung (*_positionsdraw.json des
|
||||
; Kabellaengen-Tools) den Bauteilen des Layouts (CSV) zu. Hintergrund und
|
||||
; Befunde: doc/E-Planung_Bruecke.md.
|
||||
;
|
||||
; [Registrierung]
|
||||
; anker VERW-Muster der E-Symbole, die auf einem Separator sitzen;
|
||||
; daraus wird der Lagebezug E-Zeichnung -> CSV ermittelt.
|
||||
;
|
||||
; Jeder Abschnitt "<Art>.<Rolle>" ist eine Regel; sie werden in dieser
|
||||
; Reihenfolge angewendet, ein E-Bauteil wird hoechstens einmal vergeben.
|
||||
; layout Muster fuer die Layout-Art (Separator, Ausschleus,
|
||||
; Einschleus, Weiche, Scanner, Beladung, Entladung).
|
||||
; Standard: der Teil vor dem Punkt im Abschnittsnamen.
|
||||
; verw Muster fuer das E-Attribut VERW (fnmatch, Komma-Liste,
|
||||
; Gross-/Kleinschreibung egal)
|
||||
; praefix BMK-Praefix (BG, MB ...), leer = egal
|
||||
; max_mm raeumlich: Hoechstabstand nach dem Lagebezug in mm
|
||||
; soll_mm raeumlich: Sollabstand - das Symbol liegt immer gleich weit
|
||||
; neben dem Bauteil; gewertet wird die Abweichung vom Soll
|
||||
; (nicht der kleinste Abstand), zulaessig bis toleranz_mm
|
||||
; toleranz_mm zulaessige Abweichung vom Sollabstand (Standard: cfg/parameter.ini
|
||||
; [merge_hw_layout] toleranz_mm)
|
||||
; nummer_von Nummernregel: BMK-Nummer = Nummer der genannten Rolle
|
||||
; nummer_versatz ... plus dieser Versatz (Mubea: Ventil = Sensor + 1)
|
||||
; Mit nummer_von wird zuerst die Nummer versucht, danach (wenn max_mm oder
|
||||
; soll_mm gesetzt) der Abstand fuer die uebrigen Bauteile.
|
||||
;
|
||||
; Lieber eine Luecke als ein falscher Name: was keine Regel sicher trifft,
|
||||
; bleibt in der SCL TODO(E-Planung) und steht im Bericht <csv>_eplan.md.
|
||||
;
|
||||
; Die VERW-Texte und die Nummernregel sind projektabhaengig. Die Werte hier
|
||||
; stammen aus Mubea (500573); in den Referenz-SCL (UH01..UH05) haben Sensor
|
||||
; und Stopper dieselbe Nummer (BG3242/MB3242) -> dort nummer_versatz = 0.
|
||||
; ================================================================
|
||||
|
||||
[Registrierung]
|
||||
anker = IN SEP
|
||||
|
||||
; Sensor im Stopper -> stInSeparator<n>.stSenInSep
|
||||
; sitzt in der Zeichnung auf dem Separator (Mubea Speicher: 0-1 mm).
|
||||
; An den Kreiseln liegen Sensoren 200-550 mm daneben, teils naeher an
|
||||
; Aus-/Einschleuselementen - unsicher, deshalb bewusst eng.
|
||||
[Separator.sensor]
|
||||
verw = IN SEP
|
||||
praefix = BG
|
||||
max_mm = 300
|
||||
|
||||
; Stopperventil -> xOutStopper<n>
|
||||
; liegt ~270 mm neben dem Separator, im 800-mm-Raster der Speicherlinien
|
||||
; naeher am Nachbarn als ueber die Nummer -> Nummernregel zuerst
|
||||
[Separator.stopper]
|
||||
verw = SEP
|
||||
praefix = MB
|
||||
nummer_von = Separator.sensor
|
||||
nummer_versatz = 1
|
||||
max_mm = 600
|
||||
soll_mm = 270
|
||||
|
||||
; Stausensor hinter dem Ausschleuselement (Mubea: konstant ~1390 mm)
|
||||
[Ausschleus.stau]
|
||||
verw = STAU*
|
||||
praefix = BG
|
||||
max_mm = 1600
|
||||
soll_mm = 1390
|
||||
|
||||
; Ausschleusventil (Mubea: konstant ~1414 mm, Nummer = Stausensor + 1)
|
||||
[Ausschleus.ventil]
|
||||
verw = AE ABZW*
|
||||
praefix = MB
|
||||
nummer_von = Ausschleus.stau
|
||||
nummer_versatz = 1
|
||||
max_mm = 1600
|
||||
soll_mm = 1414
|
||||
|
||||
; Weichenventil an einer Kreisel-Weiche
|
||||
[Weiche.ventil]
|
||||
verw = ABZWEIG-*, WEICHE*
|
||||
praefix = MB
|
||||
max_mm = 1000
|
||||
@@ -0,0 +1,67 @@
|
||||
; ============================================================
|
||||
; TRO-IDs zu export.csv - erzeugt von lib/tro_flow.py am 2026-10-14
|
||||
;
|
||||
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
|
||||
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
|
||||
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
|
||||
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
|
||||
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
|
||||
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
|
||||
; ============================================================
|
||||
|
||||
[tro]
|
||||
TRO01 = 6133.6, 302.6 ; 1Sep | Separator 0017 auf Kreisel4 Bahn L | Sep 0017
|
||||
TRO02 = 4540.0, -3169.7 ; 1Sep1Swi | Separator 0014 an ILS Weiche 0029 | Sep 0014
|
||||
TRO03 = 5340.0, -6534.2 ; 1Sep1Swi | Separator 0015 an ILS Weiche 0028 | Sep 0015
|
||||
TRO04 = 6933.6, -6618.2 ; 1Sep | Separator 0016 auf Kreisel4 Bahn R | Sep 0016
|
||||
TRO05 = 7149.2, 1886.0 ; 1Sep | Separator 0018 auf Kreisel6 Bahn R | Sep 0018
|
||||
TRO06 = 6933.6, 2908.5 ; 1Sep | Separator 0019 auf Kreisel6 Bahn R | Sep 0019
|
||||
TRO07 = 4540.6, -1493.5 ; 1Sep1Swi | Separator 0013 an ILS Weiche 0024 | Sep 0013
|
||||
TRO08 = 3699.4, 905.8 ; 1Sep | Separator 0012 auf Kreisel8 Bahn L | Sep 0012
|
||||
TRO09 = 1244.4, 105.8 ; 1Sep | Separator 0011 auf Kreisel8 Bahn R | Sep 0011
|
||||
TRO10 = -461.8, 1526.1 ; 1Sep | Separator 0010 auf Kreisel9 Bahn L | Sep 0010
|
||||
; neu aus export.csv am 2026-10-15
|
||||
TRO11 = 10310.0, -440.1 ; 1Sep | Separator 0042 auf Kreisel3 Bahn R | Sep 0042
|
||||
TRO12 = 10310.0, 159.9 ; 1Sep | Separator 0043 auf Kreisel3 Bahn R | Sep 0043
|
||||
TRO13 = 10310.0, -5013.1 ; 1Sep | Separator 0044 auf Kreisel3 Bahn R | Sep 0044
|
||||
TRO14 = 9510.0, 14217.0 ; 1Sep | Separator 0045 auf Kreisel3 Bahn L | Sep 0045
|
||||
TRO15 = 958.0, 12300.0 ; PinStore_Auto | Speicher (20 Schwerkraftlinien) | Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
|
||||
TRO16 = 3299.6, 13509.6 ; Vario | VF_1 | Sep 0054
|
||||
TRO17 = 8953.1, 13509.6 ; 1Sep | VF_1 | Sep 0055
|
||||
TRO18 = 400.0, -3978.1 ; 1Sep1Swi | Separator 0005 an Ausschleuselement 0053 (Strecke 0013, Anfang) | Sep 0005
|
||||
TRO19 = 3299.6, -4743.4 ; Vario | VF_2 | Sep 0058
|
||||
TRO20 = 8953.1, -4743.4 ; 1Sep | VF_2 | Sep 0059
|
||||
TRO21 = -400.0, -3554.9 ; 2Sep2Swi | Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125 | Sep 0012, 0036
|
||||
TRO22 = 5355.1, -5878.0 ; 1Sep | Separator 0040 auf Kreisel1 Bahn R | Sep 0040
|
||||
TRO23 = -400.0, 13920.0 ; 1Sep | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
|
||||
TRO24 = -400.0, -562.0 ; 1Sep | Separator 0011 auf Kreisel2 Bahn L | Sep 0011
|
||||
TRO25 = -898.9, 1134.6 ; Vario | VF_3 | Sep 0062
|
||||
TRO26 = -703.0, 12307.8 ; 1Sep | VF_3 | Sep 0063
|
||||
TRO27 = -1026.0, 10723.0 ; 1Sep | VF_3 | Sep 0037
|
||||
TRO28 = -1026.0, 5250.1 ; 1Sep | VF_3 | Sep 0038
|
||||
; neu aus export.csv am 2026-10-15
|
||||
TRO29 = 34716.5, 3406.7 ; Vario | VF_4 | Sep 0071
|
||||
TRO30 = 20161.7, 3406.7 ; 1Sep | VF_4 | Sep 0072
|
||||
TRO31 = 42164.0, 4329.5 ; 1Sep2Swi | Separator 0048 an Ausschleuselementen 0070, 0078 (Strecken 0008, 0010) | Sep 0048
|
||||
TRO32 = 40437.8, 2448.3 ; Vario | VF_6 | Sep 0079
|
||||
TRO33 = 33048.6, 2448.3 ; 1Sep | VF_6 | Sep 0080
|
||||
TRO34 = 26860.8, 2448.3 ; Vario | VF_6 | Sep 0081
|
||||
TRO35 = 20134.0, 2448.3 ; 1Sep | VF_6 | Sep 0082
|
||||
TRO36 = 20161.9, 6635.8 ; Vario | VF_5 | Sep 0075
|
||||
TRO37 = 20161.9, 11560.3 ; 1Sep | VF_5 | Sep 0076
|
||||
TRO38 = 19604.0, 4982.3 ; 1Sep1Swi | Separator 0043 an Ausschleuselement 0074 (Strecke 0009, Anfang) | Sep 0043
|
||||
TRO39 = 3098.7, 12119.7 ; 1Sep | Separator 0038 auf Kreisel2 Bahn L | Sep 0038
|
||||
TRO40 = 34990.4, 12919.7 ; 1Sep | Separator 0046 auf Kreisel2 Bahn R | Sep 0046
|
||||
TRO41 = 5034.1, 12919.7 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0040, 0085, 0088, 0091, 0094, 0097, 0100, 0103, 0106, 0109, 0112
|
||||
TRO42 = 2703.7, 21969.7 ; Vario | VF_1 | Sep 0059
|
||||
TRO43 = 2703.7, 13476.7 ; 1Sep | VF_1 | Sep 0060
|
||||
TRO44 = 3542.3, 24478.8 ; 1Sep1Swi | Separator 0039 an Ausschleuselement 0058 (Strecke 0005, Anfang) | Sep 0039
|
||||
TRO45 = 14674.7, 21969.7 ; Vario | VF_2 | Sep 0063
|
||||
TRO46 = 14674.7, 13476.7 ; 1Sep | VF_2 | Sep 0064
|
||||
TRO47 = 15472.4, 24478.8 ; 1Sep1Swi | Separator 0041 an Ausschleuselement 0062 (Strecke 0006, Anfang) | Sep 0041
|
||||
TRO48 = 23232.7, 21969.7 ; Vario | VF_3 | Sep 0067
|
||||
TRO49 = 23232.7, 13476.7 ; 1Sep | VF_3 | Sep 0068
|
||||
TRO50 = 24062.0, 24478.8 ; 1Sep1Swi | Separator 0044 an Ausschleuselement 0066 (Strecke 0007, Anfang) | Sep 0044
|
||||
TRO51 = 15934.1, 12919.8 ; PinStore_Auto | Speicher (7 Schwerkraftlinien) | Sep 0042, 0115, 0118, 0121, 0124, 0127, 0130, 0133
|
||||
TRO52 = 24734.2, 12919.8 ; PinStore_Auto | Speicher (10 Schwerkraftlinien) | Sep 0045, 0136, 0139, 0142, 0145, 0148, 0151, 0154, 0157, 0160, 0163
|
||||
TRO53 = 35122.8, 25278.8 ; 1Sep | Separator 0047 auf Kreisel3 Bahn R | Sep 0047
|
||||
@@ -0,0 +1,30 @@
|
||||
; ================================================================
|
||||
; hw_overrides.ini - Handkorrekturen fuer lib/merge_hw_layout.py
|
||||
; ================================================================
|
||||
; Was die Regeln (cfg/e_planning.ini) nicht oder falsch zuordnen, wird hier
|
||||
; von Hand gesetzt. Angewendet nach der automatischen Zuordnung; im Bericht
|
||||
; <csv>_anlage.md mit Regel "hand".
|
||||
;
|
||||
; Je Anlage (Name der CSV, aus der die TRO-JSON stammt) zwei Abschnitte:
|
||||
;
|
||||
; [<csv>.bindings]
|
||||
; <TRO-ID>.<Slot>.<Rolle> = <BMK> BMK setzen (muss im TIA-Import stehen)
|
||||
; <TRO-ID>.<Slot>.<Rolle> = - Zuordnung loeschen, Rolle bleibt offen
|
||||
; TRO-IDs aus cfg/<csv>_tro_ids.ini (stabil), Slots/Rollen aus der
|
||||
; TRO-JSON (separator1.sensor, separator1.stopper, line3.sensor,
|
||||
; drive.motor ...).
|
||||
;
|
||||
; [<csv>.conveyors]
|
||||
; <Foerderer E-Planung> = <Layout-ID> z. B. K0102 = 0010
|
||||
; <Foerderer E-Planung> = - Zuordnung loeschen
|
||||
; Achtung: die Layout-ID (TeileId) aendert sich mit jedem CSV-Export.
|
||||
;
|
||||
; Beispiel (auskommentiert):
|
||||
;
|
||||
; [mubea.csv.bindings]
|
||||
; TRO03.separator1.sensor = BG0532
|
||||
; TRO03.separator1.stopper = MB0533
|
||||
;
|
||||
; [mubea.csv.conveyors]
|
||||
; K0105 = 0001
|
||||
; ================================================================
|
||||
@@ -0,0 +1,30 @@
|
||||
; ============================================================
|
||||
; TRO-IDs zu mubea.csv - erzeugt von lib/tro_flow.py am 2026-10-15
|
||||
;
|
||||
; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.
|
||||
; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);
|
||||
; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden
|
||||
; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.
|
||||
; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).
|
||||
; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.
|
||||
; ============================================================
|
||||
|
||||
[tro]
|
||||
TRO01 = 10310.0, -440.1 ; 1Sep | Separator 0042 auf Kreisel3 Bahn R | Sep 0042
|
||||
TRO02 = 10310.0, 159.9 ; 1Sep | Separator 0043 auf Kreisel3 Bahn R | Sep 0043
|
||||
TRO03 = 10310.0, -5013.1 ; 1Sep | Separator 0044 auf Kreisel3 Bahn R | Sep 0044
|
||||
TRO04 = 958.0, 12300.0 ; PinStore_Auto | Speicher (20 Schwerkraftlinien) | Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
|
||||
TRO05 = -400.0, -3554.9 ; 1Sep | Separator 0012 auf Kreisel2 Bahn L | Sep 0012
|
||||
TRO06 = 5355.1, -5878.0 ; 1Sep | Separator 0040 auf Kreisel1 Bahn R | Sep 0040
|
||||
TRO07 = -898.9, 1134.6 ; Vario | VF_3 | Sep 0062
|
||||
TRO08 = -703.0, 12296.5 ; 1Sep | VF_3 | Sep 0063
|
||||
TRO09 = -400.0, -562.0 ; 1Sep1Swi | Separator 0011 an Ausschleuselement 0061 (Strecke 0015, Anfang) | Sep 0011
|
||||
TRO10 = -1026.0, 10723.0 ; 1Sep | VF_3 | Sep 0037
|
||||
TRO11 = -1026.0, 5250.1 ; 1Sep | VF_3 | Sep 0038
|
||||
TRO12 = 3299.6, 13509.6 ; Vario | VF_1 | Sep 0054
|
||||
TRO13 = 8953.1, 13509.6 ; 1Sep | VF_1 | Sep 0055
|
||||
TRO14 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 an Ausschleuselement 0053 (Strecke 0013, Anfang) | Sep 0003
|
||||
TRO15 = 9510.0, 14217.0 ; 1Sep | VF_1 | Sep 0045
|
||||
TRO16 = 3299.6, -4743.4 ; Vario | VF_2 | Sep 0058
|
||||
TRO17 = 8953.1, -4743.4 ; 1Sep | VF_2 | Sep 0059
|
||||
TRO18 = 400.0, -3978.1 ; 2Sep2Swi | Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125 | Sep 0005, 0036
|
||||
@@ -0,0 +1,192 @@
|
||||
; ================================================================
|
||||
; parameter.ini - Zahlenwerte der Bibliotheken in lib/
|
||||
; ================================================================
|
||||
; Gelesen von lib/parameter.py. Ein Abschnitt je Modul, der Schluessel ist
|
||||
; der Name der Konstanten im Modul in Kleinbuchstaben:
|
||||
; [kreisel_junctions] contact_tolerance -> CONTACT_TOLERANCE
|
||||
;
|
||||
; Diese Datei ist die versionierte Grunddatei und muss vollstaendig sein -
|
||||
; fehlt ein Schluessel, bricht das Programm ab (kein Ersatzwert im Code).
|
||||
; Eine weitere parameter.ini in %SKEL_CFG% (falls SKEL_CFG woanders hin
|
||||
; zeigt) ueberschreibt einzelne Werte fuer eine Anlage.
|
||||
;
|
||||
; Laengen in mm, Winkel in Grad, sofern nicht anders angegeben.
|
||||
; Werte, die das Ergebnis der Ableitung aendern (Toleranzen, Schwellen),
|
||||
; nur mit Begruendung und Vergleich der Ausgaben aller Anlagen aendern.
|
||||
; ================================================================
|
||||
|
||||
|
||||
[kreisel_junctions]
|
||||
; Hoechster Abstand zweier Kreisel-Umlaeufe, der noch als Beruehrung gilt.
|
||||
contact_tolerance = 50.0
|
||||
|
||||
; Hoechster Abstand eines Separators vom Umlauf, damit er auf ihm liegt.
|
||||
; Auch die Grenze fuer "liegt auf Kreisel" im Mouseover von
|
||||
; material_flow.py --track.
|
||||
separator_tolerance = 100.0
|
||||
|
||||
; Hoechster Abstand einer exportierten ILS Weiche von ihrem Beruehrpunkt.
|
||||
weiche_tolerance = 300.0
|
||||
|
||||
; Abtastschritt beim Suchen des Beruehrpunkts.
|
||||
sample_step = 10.0
|
||||
|
||||
; Mindestanzahl Abtastpunkte je Umlauf, auch bei sehr kleinen Kreiseln.
|
||||
min_samples = 8
|
||||
|
||||
; Bei tangentialer Beruehrung sind viele Abtastpunkte fast gleich nah: alle
|
||||
; bis zu diesem Mehrabstand ueber dem naechsten gehen in den Mittelpunkt ein.
|
||||
contact_tie = 1.0
|
||||
|
||||
; Ein Beruehrbereich, der laenger ist, liegt nicht an einem Punkt an (zwei
|
||||
; parallele Bahnen nebeneinander) - Hinweis statt Weiche.
|
||||
max_contact_length = 500.0
|
||||
|
||||
; Ein Punkt bis zu diesem Abstand vor dem Ende der Geraden zaehlt schon zur
|
||||
; Umlenkung. Zwei Kreisel, deren Umlenkungen sich beruehren, tun das oft genau
|
||||
; am Uebergang Gerade/Halbkreis (export.csv: Kreisel 5 an Kreisel 6 und 4).
|
||||
end_tolerance = 100.0
|
||||
|
||||
; Radius, wenn die Boundingbox ihn nicht liefert (schraeg liegender Kreisel).
|
||||
default_radius = 400.0
|
||||
|
||||
; Halbe Schrittweite fuer die Laufrichtung an einer Stelle des Umlaufs
|
||||
; (Differenz zweier Punkte davor und dahinter).
|
||||
direction_step = 1.0
|
||||
|
||||
; Gleichlauf am Beruehrpunkt: Mindest-Kosinus des Winkels zwischen beiden
|
||||
; Laufrichtungen (0.5 = hoechstens 60 Grad). Darunter "gegenlaeufig".
|
||||
gleichlauf_min_cos = 0.5
|
||||
|
||||
|
||||
[tro_flow]
|
||||
; Ab dieser Anzahl zusammenhaengender Schwerkraftlinien (Gefaellestrecke mit
|
||||
; je einem Separator) wird ein Speicherblock (PinStore_Auto) angenommen.
|
||||
min_store_lanes = 2
|
||||
|
||||
; Mindest-Hoehenverlust auf dem vorangehenden TrackIds-Segment, damit ein
|
||||
; Separator auf einer "Strecke" trotzdem als 1Sep statt Vario gilt (siehe
|
||||
; _incoming_drop). Muss nur "horizontal" gefuehrte, aber tatsaechlich
|
||||
; Gefaelle-Segmente erwischen (HundM05 Separator 0035: -104mm auf 3,8m), nicht
|
||||
; das Rauschen eines echten flachen VF-Laufs (HundM05 Separator 0036: +26mm
|
||||
; auf 0,5m). 30mm liegt bewusst weit unter dem Befund und ueber dem Rauschen.
|
||||
gefaelle_drop_mm = 30.0
|
||||
|
||||
; Zeichnungsmassstab fuer --use-cords in pt/mm: Graphviz rechnet in Punkt, die
|
||||
; Anlage in Millimeter. Seit die Symbole eine feste Groesse haben, reichen
|
||||
; 0.1 pt/mm: ~11 x 20 m Anlage ergeben etwa 1100 x 2000 pt. Vorher 0.25
|
||||
; (riesige Zeichnung, Symbole und Schrift verschwanden; Review 2026-10-01).
|
||||
coord_scale = 0.1
|
||||
|
||||
; Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
|
||||
; ab dem angehaengten TRO (siehe _station_positionen).
|
||||
coord_pfeil_laenge = 1300
|
||||
|
||||
; Groesste Luecke zwischen zwei benachbarten Knotenkoordinaten je Achse in der
|
||||
; lagerichtigen Zeichnung; groessere Leerstrecken werden auf diesen Wert
|
||||
; gekuerzt (Review 2026-10-01). --true-scale schaltet das ab.
|
||||
coord_max_luecke = 1500
|
||||
|
||||
; Abstand der Legende rechts neben der Anlage.
|
||||
legend_margin = 3000
|
||||
|
||||
; Oberkante der Legende so weit unter der Anlagenoberkante.
|
||||
legend_drop = 1000
|
||||
|
||||
|
||||
[tro_ids]
|
||||
; Abstand zwischen Eintrag der ID-Datei und Separator, bis zu dem ein TRO als
|
||||
; wiedergefunden gilt (Standard fuer --id-tolerance). Separatoren derselben
|
||||
; Bahn liegen deutlich weiter auseinander; ein verschobenes Bauteil faellt so
|
||||
; als "neu" auf.
|
||||
default_tolerance = 100.0
|
||||
|
||||
|
||||
[material_flow]
|
||||
; Max. Abstand, ab dem ein generisches Eingang/Ausgang-Symbol als
|
||||
; "deckungsgleich" mit einer BTMT-Station gilt und verschluckt wird. Die
|
||||
; beobachteten Paare liegen bei 256-431 mm, echte getrennte Stationen >1000 mm.
|
||||
station_merge_mm = 600.0
|
||||
|
||||
; Ab dieser Anzahl paralleler Gefaellestrecken zwischen denselben zwei
|
||||
; Kreisel-Bahnen werden sie im Materialfluss-Diagramm zu einem Block
|
||||
; zusammengefasst - sonst wird eine grosse Linienbank unlesbar breit.
|
||||
min_gefaelle_group = 2
|
||||
|
||||
; Zeichnungsmassstab fuer --use-cords in pt/mm. Achtung: tro_flow nutzt seit
|
||||
; Review 2026-10-01 0.1 - beide Graphen haben damit verschiedene Massstaebe.
|
||||
coord_scale = 0.25
|
||||
|
||||
; Die beiden Bahnen (L/R) eines Kreisels teilen sich eine Position; im
|
||||
; lagerichtigen Layout so weit versetzen, damit sie nicht uebereinander liegen.
|
||||
lane_nudge_mm = 350.0
|
||||
|
||||
; Legende im lagerichtigen Layout so weit unterhalb der Anlage.
|
||||
legend_offset_mm = 3000.0
|
||||
|
||||
; Legende (--show-bbs): Kaestchen- und Schriftgroesse.
|
||||
bbs_legend_box_mm = 250.0
|
||||
bbs_legend_font_mm = 180.0
|
||||
|
||||
; --show-bbs: Rand um die Anlage herum und minimale Schriftgroesse.
|
||||
bbs_margin_mm = 500.0
|
||||
bbs_min_font_mm = 40.0
|
||||
|
||||
; Anschlusspunkte K1-K4 (--show-kx): Groesse des Kreuzes und der Beschriftung.
|
||||
kx_cross_mm = 120.0
|
||||
kx_font_mm = 90.0
|
||||
|
||||
; --chain-graph: Hoechstgroesse der Zeichenflaeche, Rand und Titelhoehe (px).
|
||||
chain_max_w = 1400.0
|
||||
chain_max_h = 850.0
|
||||
chain_margin = 70.0
|
||||
chain_title_h = 60.0
|
||||
|
||||
|
||||
[dxf_registration]
|
||||
; Rastergroesse der Abstimmung.
|
||||
vote_grid = 100.0
|
||||
|
||||
; So viele Kandidaten der Abstimmung werden ausgeglichen und bewertet.
|
||||
vote_candidates = 6
|
||||
|
||||
; Nur Paare bis zu diesem Abstand gehen in den Ausgleich ein (exakte Treffer).
|
||||
fit_tol = 50.0
|
||||
|
||||
; Punkte gelten als zugeordnet, wenn sie nach der Verschiebung so nah liegen.
|
||||
default_match_tol = 600.0
|
||||
|
||||
; So viele exakte Treffer muessen es mindestens sein.
|
||||
min_inliers = 5
|
||||
|
||||
; Ab dieser Drehung (Grad) wird gewarnt.
|
||||
rotation_warn_deg = 0.05
|
||||
|
||||
|
||||
[tro_annotate]
|
||||
; Zeichnungsgroessen, wenn nicht per --marker-size/--text-height angegeben.
|
||||
default_marker = 300.0
|
||||
default_text = 220.0
|
||||
|
||||
; Restfehler des Lagebezugs, ab dem er abgelehnt wird.
|
||||
default_max_residual = 600.0
|
||||
|
||||
|
||||
[merge_hw_layout]
|
||||
; Zulaessige Abweichung vom Sollabstand (soll_mm), wenn eine Regel in
|
||||
; e_planning.ini kein toleranz_mm angibt (die Regeln liest merge_hw_layout, auch
|
||||
; fuer e_planning).
|
||||
toleranz_mm = 150.0
|
||||
; Foerderer der E-Planung (K0102) -> Layout-Foerderer (Kreisel3) per Mehrheit:
|
||||
; ein Feldgeraet, dessen Klartext den Foerderer nennt, stimmt fuer den
|
||||
; Layout-Foerderer, dessen Huellbox ihm am naechsten liegt - hoechstens so
|
||||
; weit entfernt (mm, nach dem Lagebezug).
|
||||
conveyor_max_mm = 1500.0
|
||||
; Zugeordnet wird erst ab so vielen Stimmen ...
|
||||
conveyor_min_votes = 2
|
||||
; ... und diesem Anteil an allen Geraeten, die den Foerderer nennen (0..1).
|
||||
conveyor_min_share = 0.6
|
||||
; Warnung, wenn der Lagebezug fuer weniger als diesen Anteil der
|
||||
; Layout-Separatoren einen Partner findet (0..1) - Hinweis auf verschiedene
|
||||
; Planungsstaende von Layout und E-Zeichnung.
|
||||
registration_min_share = 0.8
|
||||
@@ -0,0 +1,191 @@
|
||||
# Command Lines — Overview
|
||||
|
||||
**As of:** 2026-10-05
|
||||
|
||||
Every command this project provides, in one place. This is the quick reference;
|
||||
for what each tool derives, how, and the full description of every switch, see
|
||||
[`Python_Scripts.md`](Python_Scripts.md).
|
||||
|
||||
All commands live in `bin/` as a `.bat` (Windows) / `.sh` (Linux/macOS) pair.
|
||||
Examples below use the Windows form; on Linux/macOS replace `bin\x.bat` with
|
||||
`bash bin/x.sh` (or `source bin/x.sh` where noted).
|
||||
|
||||
## 1. Environment commands
|
||||
|
||||
These set up or enter the environment. They never run a Python tool.
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `bin\setenv.bat` / `source bin/setenv.sh` | Sets `SPS_SKEL` and `SKEL_BIN/LIB/CFG/DATA/DOC/LOG/RESULTS/EXAMPLES/TESTS`, prepends `SKEL_LIB` to `PYTHONPATH`, creates missing folders. Windows: `--keep-open` pauses at the end. |
|
||||
| `bin\install_py.bat` / `bash bin/install_py.sh` | One-time: creates `.venv` and installs `requirements.txt`. Does nothing if `.venv` already exists. |
|
||||
| `bin\activate_venv.bat` / `source bin/activate_venv.sh` | Each session: sets the environment and activates `.venv` (error if missing — run `install_py` first). |
|
||||
| `bin\get_cmd.bat` / `bash bin/get_cmd.sh` | Opens a new shell with the environment variables set. |
|
||||
|
||||
## 2. Pipeline tools
|
||||
|
||||
The five CLI tools, in pipeline order. Each wrapper sets the environment,
|
||||
activates `.venv` if present and runs `lib/<name>.py` with all arguments passed
|
||||
through. Every tool accepts `--help`.
|
||||
|
||||
```
|
||||
CSV ─► material_flow ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
|
||||
│ (model.py) (validates)
|
||||
└─► tro_annotate (+ DXF) ─► <name>_annotated.dxf viewer only
|
||||
|
||||
parked: tro_extract (DXF → JSON), see 2.4
|
||||
```
|
||||
|
||||
Shared conventions:
|
||||
|
||||
- Input names (`--file`, `--dxf`) are a bare filename resolved against
|
||||
`%SKEL_DATA%`, or a full path. `scl_skeleton` (and the parked `tro_extract`)
|
||||
look up their input in `%SKEL_RESULTS%` instead (it is the output of the
|
||||
previous step).
|
||||
- All output goes to `%SKEL_RESULTS%`.
|
||||
- `--tosvg` needs Graphviz `dot` (`neato` with `--use-cords`) on `PATH`, or
|
||||
`GRAPHVIZ_DOT` pointing at `dot`.
|
||||
|
||||
### 2.1 `material_flow` — material-flow graph of the mechanical objects
|
||||
|
||||
Input: CSV export. Details: [`Python_Scripts.md` §3](Python_Scripts.md#3-material_flowpy--material-flow-graph-of-the-mechanical-layout).
|
||||
|
||||
| Switch | Argument | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| `--file` | `NAME` | `export.csv` | CSV input. |
|
||||
| `--tosvg` | — | off | Also render `.svg` via Graphviz. |
|
||||
| `--use-cords` / `--use-coords` | — | off | Place nodes at plant coordinates (`neato -n`). Only with `--tosvg`. |
|
||||
| `--doc` | — | off | Write Markdown report `<name>_material_flow.md`. |
|
||||
| `--connect` | — | off | Write topology as `<name>_connect.ini`. |
|
||||
| `--show-bbs` | `flow` \| `all` | `flow` if given without value | Write top-down view `<name>_bbs.svg` (no Graphviz). `all` also draws VF/GF chain detail rows. |
|
||||
| `--show-kx` | — | off | Only with `--show-bbs`: draw connection points K1–K4. |
|
||||
| `--track` | — | off | Write fine-grained track graph `<name>_track.dot` (`.svg` with `--tosvg`). |
|
||||
| `--chain-graph` | — | off | Write chain/layout plan of the coarse objects (`<name>_chain.svg` / `.dot`). |
|
||||
| `--graphtype` | `coarsebox` \| `compound` \| `subgraph` | `coarsebox` | Only with `--chain-graph`: rendering method. |
|
||||
|
||||
Exit codes: `0` ok · `1` input/CLI error · `2` SVG rendering failed.
|
||||
|
||||
```bat
|
||||
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
|
||||
bin\material_flow.bat --file HundM05.csv --show-bbs all --show-kx
|
||||
bin\material_flow.bat --file HundM05.csv --chain-graph --graphtype subgraph --tosvg
|
||||
```
|
||||
|
||||
### 2.2 `tro_flow` — derive and draw the TROs
|
||||
|
||||
Input: CSV export. Details: [`Python_Scripts.md` §4](Python_Scripts.md#4-tro_flowpy--derive-and-draw-the-tros).
|
||||
|
||||
| Switch | Argument | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| `--file` | `NAME` | `export.csv` | CSV input. |
|
||||
| `--tosvg` | — | off | Also render the TRO flow diagram as `.svg`. |
|
||||
| `--use-cords` / `--use-coords` | — | off | Place TROs at plant coordinates (`neato -n`). Only with `--tosvg`. |
|
||||
| `--true-scale` | — | off | Only with `--use-cords`: draw to scale (default shortens empty stretches > 1500 mm). |
|
||||
| `--doc` | — | off | Write Markdown report `<name>_tro_doc.md`. |
|
||||
| `--connect` | — | off | Write TRO topology as `<name>_tro_connect.ini`. |
|
||||
| `--json` | — | off | Write the TRO-JSON `<name>_tro.json` (model `lib/model.py`) — input for `scl_skeleton`. |
|
||||
|
||||
Exit codes: `0` ok · `1` input/CLI error · `2` SVG rendering failed ·
|
||||
`3` TRO validation errors (`<name>_tro_errors.md` written).
|
||||
|
||||
```bat
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc
|
||||
bin\tro_flow.bat --file mubea.csv --json
|
||||
```
|
||||
|
||||
### 2.3 `tro_annotate` — burn the TROs into a copy of the drawing
|
||||
|
||||
Viewer only: shows the same TROs as `tro_flow` in the drawing for a visual
|
||||
check; the drawing is not read back as data.
|
||||
Input: CSV export + original BricsCAD DXF. The original is never modified.
|
||||
Details: [`Python_Scripts.md` §5](Python_Scripts.md#5-tro_annotatepy--burn-the-tros-into-a-copy-of-the-cad-drawing).
|
||||
|
||||
| Switch | Argument | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| `--file` | `NAME` | `export.csv` | CSV input. |
|
||||
| `--dxf` | `NAME` | — | Original drawing (DXF). Required unless `--emit-lisp` is used alone. |
|
||||
| `--out` | `NAME` | `<csv-stem>_annotated.dxf` | Output filename. |
|
||||
| `--flow` | — | off | Draw flow arrows between TROs. |
|
||||
| `--fb` | — | off | Show the Siemens FB block name on the marker. |
|
||||
| `--legend` | — | off | Draw a color-group legend. |
|
||||
| `--marker-size` | `MM` | `300.0` | Marker radius in mm. |
|
||||
| `--text-height` | `MM` | `220.0` | Text height in mm. |
|
||||
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand (`0,0` for dxfmakros drawings). |
|
||||
| `--re-register` | — | off | Re-derive the offset even if a cached one exists in `%SKEL_CFG%`. |
|
||||
| `--check` | — | off | Dry run: analyze and report, write nothing. |
|
||||
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` (or under `%DXFM_LISP%`) and exit. |
|
||||
|
||||
Exit codes: `0` ok · `1` input/CLI error · `4` coordinate registration not
|
||||
provable — use `--offset`.
|
||||
|
||||
```bat
|
||||
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
|
||||
bin\tro_annotate.bat --file mubea.csv --dxf Mubea.dxf --offset 0,0 --check
|
||||
bin\tro_annotate.bat --emit-lisp
|
||||
```
|
||||
|
||||
### 2.4 `tro_extract` — read the TROs back out of the drawing into JSON
|
||||
|
||||
**Parked** — kept, but not part of the main path. Its JSON (schema
|
||||
`sps_skel-tro-extract-v1`) is rejected by `scl_skeleton`; use
|
||||
`tro_flow --json` instead.
|
||||
|
||||
Input: the (possibly hand-edited) annotated DXF only, never the CSV.
|
||||
Details: [`Python_Scripts.md` §5a](Python_Scripts.md#5a-tro_extractpy--read-the-tros-back-out-of-the-drawing-into-json).
|
||||
|
||||
| Switch | Argument | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| `--dxf` | `NAME` | `export_annotated.dxf` | Annotated drawing (looked up in `%SKEL_RESULTS%`). |
|
||||
| `--out` | `NAME` | `<drawing>_tro.json` | Output JSON filename. |
|
||||
|
||||
Exit codes: `0` ok · `1` input/CLI error · `2` no TRO markers in the drawing.
|
||||
|
||||
```bat
|
||||
bin\tro_extract.bat --dxf mubea_annotated.dxf
|
||||
```
|
||||
|
||||
### 2.5 `scl_skeleton` — generate the `FB_Main` SCL skeleton
|
||||
|
||||
Input: TRO-JSON from `tro_flow --json`, validated against `lib/model.py` — a
|
||||
wrong or renamed key stops the run (exit `1`) instead of producing SCL. Output
|
||||
is a skeleton with `TODO(E-Planung)` gaps, not a compilable program.
|
||||
Details: [`Python_Scripts.md` §5b](Python_Scripts.md#5b-scl_skeletonpy--generate-the-fb_main-skeleton-as-scl).
|
||||
|
||||
| Switch | Argument | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| `--json` | `NAME` | `export_tro.json` | TRO-JSON input (looked up in `%SKEL_RESULTS%`). |
|
||||
| `--out` | `NAME` | `<source>_FB_Main.scl` | Output SCL filename. |
|
||||
| `--start` | `TRO` | — | TRO where the REGION order begins. Only needed while the flow is a closed loop without a loading/unloading station (`SYSn`). |
|
||||
| `--templates` | `[DIR]` | off | Build REGIONs from the FB templates (`lib/scl_templates` if `DIR` is omitted); FB types without a template stay generic. Without it: the previous output, unchanged. See [`SCL_Templates.md`](SCL_Templates.md). |
|
||||
| `--strict` | — | off | With `--templates`: exit `1` and write nothing if any FB type of the plant has no template. |
|
||||
|
||||
Exit codes: `0` ok (also when TROs without a template were built generically) · `1`
|
||||
input/CLI error, JSON does not match the model, template error, or a missing template
|
||||
with `--strict`.
|
||||
|
||||
```bat
|
||||
bin\scl_skeleton.bat --json mubea_tro.json
|
||||
bin\scl_skeleton.bat --json mubea_tro.json --start TRO01
|
||||
bin\scl_skeleton.bat --json mubea_tro.json --templates
|
||||
```
|
||||
|
||||
## 3. End-to-end example
|
||||
|
||||
```bat
|
||||
bin\activate_venv.bat
|
||||
bin\material_flow.bat --file mubea.csv --tosvg --doc
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json
|
||||
bin\scl_skeleton.bat --json mubea_tro.json
|
||||
REM optional, for a visual check in BricsCAD:
|
||||
bin\tro_annotate.bat --file mubea.csv --dxf Mubea.dxf --flow --legend
|
||||
```
|
||||
|
||||
Tests: `python -m pytest tests/` (with `.venv` active).
|
||||
|
||||
## 4. Documentation tooling (not part of the pipeline)
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `py doc/TRO_Katalog/tro_graphs/gen_tro_graphs.py` | Regenerates the per-type SVGs, the edge legend and the UH01–UH05 topology graphs for `TRO_Katalog.md` from `doc/TRO_Katalog/connect.ini`. No switches, no `bin/` wrapper. Needs Graphviz `dot`. |
|
||||
|
||||
The libraries `lib/tro_catalog.py`, `lib/model.py`, `lib/dxf_registration.py`
|
||||
and `lib/tro_overrides.py` have no command line of their own.
|
||||
@@ -0,0 +1,246 @@
|
||||
# Datenquellen für TROs und SCL: mechanisches Layout und Hardwareplanung
|
||||
|
||||
Stand 2026-10-11. Welche Angaben für die TRO-Liste und die erzeugten `.scl`-Bausteine
|
||||
aus dem **mechanischen Layout** kommen, welche aus der **Hardwareplanung** (EA-Liste
|
||||
und TIA-Import), und was als **Defaultvorbelegung** gesetzt wird. Die vollständige
|
||||
Liste der Rohangaben beider Quellen (Spalten, Merkmale, Attribute) steht in
|
||||
`doc/Klassenhierarchie.md`, Kapitel "Daten", Abschnitte "Angaben 1" und "Angaben 2";
|
||||
die Zuordnung jedes SCL-Parameters zu seiner Quelle in den Tabellen danach.
|
||||
Wie beide Quellen künftig über eine gemeinsame JSON zum SCL werden:
|
||||
`doc/Konzept_Layout_Hardware_SCL.md`.
|
||||
|
||||
## Die drei Quellen
|
||||
|
||||
| Quelle | Dateien | Liefert im Kern |
|
||||
|---|---|---|
|
||||
| **Mechanisches Layout** (Anlagen-Hardware) | CSV-Export ILS 2.0 (`data/*.csv`), aus der BricsCAD-Layoutzeichnung | **Struktur**: welche Bauteile, wer folgt auf wen, Drehrichtung, Längen und Höhen, Anzahl Separatoren/Scanner |
|
||||
| **Hardwareplanung** (E-Planung) | `<projekt>-<n>_EA.xlsx`, `<projekt>-<n>_TIA.xlsx` (PLC Tags + Constants), `<projekt>-<n>_WSCAD.xlsx`, `<zeichnung>_positionsdraw.json` | **Namen und Adressen**: jedes Signal mit BMK, Adresse, Index-Konstante, Rolle, Steuerung, Schaltschrank; mit `positionsdraw` auch die Lage |
|
||||
| **Defaultvorbelegung** | Typ-Katalog (`lib/tro_catalog.py`), Namensregeln (`scl_context.py`), Projektkonstanten | alles, was weder Layout noch E-Planung kennen: Zeiten, Konstanten-Namen nach Regel, Freigaben, WCS-Ziele |
|
||||
|
||||
Die Schlüssel zwischen den Quellen:
|
||||
|
||||
```text
|
||||
Layout-Bauteil (TeileId) --[Lage, e_planning.py]--> E-Symbol (BMK, positionsdraw)
|
||||
E-Symbol (BMK) --[Name]-----------------> TIA PLC Tag (Adresse, Kommentar)
|
||||
--[cIn<BMK>]--------------> TIA Constant (Index in DB_Inputs)
|
||||
E/A-Adresse --[Anschluss]-------------> WSCAD Bezug (Feldstation, Karte)
|
||||
```
|
||||
|
||||
EA-Liste und TIA-Import allein haben **keine Lage**, `positionsdraw` hat **keine
|
||||
Adresse**. Erst über das BMK als gemeinsamen Schlüssel wird aus "Sensor neben
|
||||
Separator 0008" ein vollständiger SCL-Eintrag mit Adresse und Indexkonstante.
|
||||
|
||||
## Was die Hardwareplanung liefert
|
||||
|
||||
### Für die TROs
|
||||
|
||||
| Angabe | aus | Regel |
|
||||
|---|---|---|
|
||||
| Signal gehört zu welchem Layout-Bauteil | `positionsdraw` `pos` | räumlich, `cfg/e_planning.ini` (heute umgesetzt) |
|
||||
| Rolle des Signals im TRO | `positionsdraw` `VERW`; ersatzweise Kommentar der EA/TIA-Liste | `IN SEP` → Sensor im Separator, `SEP` → Stopperventil, `STAU_REF-LT` → Stausensor, `AE ABZWEIG` → Ausschleusventil, `BT BELADEN` → Beladesensor, `MT in Position`/`ABFR.FINGER`/`conveyor full` → Vario |
|
||||
| Welche optionalen Sensoren verbaut sind | vorhanden / nicht vorhanden je Rolle | ergibt die `xPlugged*`-Schalter |
|
||||
| Fördereinheit (M-Nummer) | Kommentar `(ILS-CV M0108)`, `BEZEICHNUNG` `KR-K0102` | Instanzname des Antriebs (`#fbCarousel0102`), Name der Staubereiche (`DB_JamArea0102`) |
|
||||
| Steuerung, TRO-Nummernkreis | `KENNZEICHNUNG` `=A1+UH01-KF102`, Dateinummer `-<n>`, `mappings` | `UH01` → TRO 1xx |
|
||||
| Schaltschrank / Bedienfeld | `KENNZEICHNUNG`, WSCAD `Bezug`, `schaltschrank_elemente` | `stInOutMachineState`, Taster `SF…` |
|
||||
| Motor ↔ Motorschutz | `MA`/`FC` mit gleicher Nummer | `xOutVarioMotor`, `xInMotorProtection` |
|
||||
| Safety-Signale | WSCAD `Bezug` auf F-Karte (`KF1FDQ…`) | Not-Halt-Kreis, nur Hinweis |
|
||||
| Plausibilisierung des TRO-Typs | Anzahl "in Separator n", "AE Abzweig", "Pin Abfrage" je M-Nummer | Gegenprüfung des aus dem Layout abgeleiteten Typs |
|
||||
|
||||
### Für die SCL-Dateien
|
||||
|
||||
| Ziel | aus | Bemerkung |
|
||||
|---|---|---|
|
||||
| Namen in `FB_Main` (`=> "MB3306"`, `:= "FC0145"`, `"SF0002"`) | TIA PLC Tags `Name` | 1:1 |
|
||||
| Sensor-Indizes `"DB_Inputs".Sensors["cInBG3306"]` | TIA Constants `cIn<BMK>` | 1:1; jede Konstante, die der Generator schreibt, muss in diesem Blatt stehen → **Prüfung möglich** |
|
||||
| `FB_CallSensors`, Größe `DB_Inputs` | alle `cInBG…`/`cInFC…` mit `Value` | noch nicht generiert |
|
||||
| Variablen- und Konstantentabelle im TIA Portal | der TIA-Import selbst | wird nicht generiert, sondern importiert |
|
||||
| `stInOutMachineState`, `xInAutomatic`, `xInErrorQuit` | Schrankelemente `SF…`, `KENNZEICHNUNG` | heute `TODO(E-Planung)` |
|
||||
| `xInSftyOk` | Safety-Programm; WSCAD nur als Hinweis | heute `TODO(E-Planung)` |
|
||||
| Scanner `BX…`, Cognex-Variablen | **nicht** in EA/TIA-Tagliste (PROFINET) | TIA-Gerätekonfiguration oder `positionsdraw` mit `BX` |
|
||||
|
||||
## Was das mechanische Layout liefert
|
||||
|
||||
### Für die TROs
|
||||
|
||||
| Angabe | aus | Regel / Modul |
|
||||
|---|---|---|
|
||||
| Existenz und Grenzen der TROs | Separatoren (`ILS 2.0 Separator`) und ihr Host (`Zuordnung`, `TrackIds`) | `tro_flow.analyse_tros()` |
|
||||
| TRO-Typ und damit FB | Host Kreisel/Strecke/Gefälle, Weiche, Aus-/Einschleuselement, Scanner | `doc/500573_Mubea/TRO_Identifikation_500573.md`, `tro_catalog` |
|
||||
| Anzahl Separatoren, Weichen, Scanner je TRO | `Zuordnung`, `Naechster Separator`, Weichen-Merkmale | Parameterblöcke `stInSeparator<n>`, `stInSwitch<n>` |
|
||||
| Vorgänger/Nachfolger | `Nachbarn` (mit Bahn `-L`/`-R`), `TrackIds`, `Drehrichtung` | `material_flow` → Kanten, Ein-/Ausgangs-Staubereiche |
|
||||
| Systemgrenzen | `BTMT Beladung`/`Entladung`, `Eingang`/`Ausgang` | `SYSn`, `xIsBorner` |
|
||||
| Ziele je Weichenabgang | Erreichbarkeit im TRO-Graphen | `nTo1Destinations`, Zielliste |
|
||||
| Vorfahrt nötig | Einschleuselement / zwei Ströme in einen Staubereich | `xPrioDir`, `xReleaseOk` |
|
||||
| Tragende Fördertechnik | Host-Kreisel bzw. Gefälle | `xInCarouselRun1/2` |
|
||||
| angetriebene Strecke | `L_VF_m`, `Antriebfahrtrichtung` `Auf` | Vario |
|
||||
| Längen | `L_VF_m`, `L_GF_m`, Kreiselabstand | Grundlage für `tConvEmpty`, Staulängen |
|
||||
|
||||
### Für die SCL-Dateien
|
||||
|
||||
REGION-Reihenfolge, FB-Typ, Instanzname, Zahl und Reihenfolge der Parameterzeilen,
|
||||
Staubereichs-Verkettung (Exit von A = Entr von B), `FC_Direction`-Aufruf mit dem
|
||||
Carrier aus dem Eingangs-Staubereich, `nTo1Destinations`, Cognex-Dummy bei TROs ohne
|
||||
Scanner. Das ist heute der Teil, den `scl_skeleton.py` schon ohne E-Planung füllt.
|
||||
|
||||
## Überschneidungen: gegenseitige Prüfung
|
||||
|
||||
Beide Quellen beschreiben dieselbe Anlage, aber unabhängig voneinander. Wo sie sich
|
||||
überschneiden, prüft die eine die andere:
|
||||
|
||||
| Angabe | Layout | Hardwareplanung |
|
||||
|---|---|---|
|
||||
| Separatoren je Strecke/Kreisel | `Anzahl_Separator`, `Zuordnung` | Anzahl `IN SEP`/"in Separator n" je M-Nummer |
|
||||
| Weiche/Abzweig vorhanden | `ILS Weiche`, `ILS Ausschleuselement` | `AE ABZWEIG`, `ABZWEIG-K104`, `ES branch` |
|
||||
| Scanner | `ILS 2.0 Scanner` | `BX…` (nur in `positionsdraw` mancher Projekte) |
|
||||
| angetriebene Strecke | `L_VF_m`, `Auf` | `MA…`/`FC…` mit Bezeichnung der Strecke |
|
||||
| Lage | `Position` | `pos` (nach Lagebezug `dxf_registration`) |
|
||||
|
||||
Ein Widerspruch (z. B. Layout 6 Separatoren, E-Planung 5 `IN SEP`) heißt meist:
|
||||
unterschiedliche Planungsstände. Er gehört in den Bericht `<csv>_eplan.md`, nicht
|
||||
stillschweigend in die SCL.
|
||||
|
||||
## Beispielzeilen mit Herkunft
|
||||
|
||||
Zwei reale Aufrufstellen aus `doc/TRO_Katalog/scl_templates/` (UH01), Zeile für Zeile
|
||||
mit der Quelle als Kommentar:
|
||||
|
||||
- **Layout** = mechanisches Layout (CSV)
|
||||
- **HW-Planung** = Hardwareplanung (EA-Liste, TIA-Import, WSCAD, `positionsdraw`)
|
||||
- **Default** = Defaultvorbelegung (Typ-Default, Namensregel, Projektkonstante)
|
||||
|
||||
Bei zwei Quellen steht die führende zuerst: "HW-Planung + Layout" heißt, der Name
|
||||
kommt aus der Hardwareplanung, die Zuordnung zu diesem TRO bzw. Ausgang aus dem
|
||||
Layout. Der Status heute (Generator setzt / `TODO(E-Planung)`) steht in den
|
||||
Tabellen von `doc/Klassenhierarchie.md`.
|
||||
|
||||
Die Codeblöcke sind als `pascal` ausgezeichnet: Markdown-Renderer (VS Code,
|
||||
GitHub, Gitea) kennen kein `scl`, und SCL ist Pascal-basiert. So werden
|
||||
Schlüsselwörter, Zuweisungen, Zahlen und `//`-Kommentare hervorgehoben.
|
||||
|
||||
### `FB_ILS_MTRO_1Sep1Swi` (TRO 107, Separator mit Weiche am Kreisel)
|
||||
|
||||
```pascal
|
||||
REGION TRO 107(1Sep1Swi) // Layout: Typ aus Separator am Kreisel + Ausschleuselement; Nummer 107: Default (Nummernkreis UH01 aus HW-Planung)
|
||||
#TRO107.stInSeparator1.Settings.nSeparatorNo := "cSep107.1"; // Default: Namensregel cSep<TRO>.<n>; Anzahl Separatoren: Layout
|
||||
#TRO107.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short"; // Default; Short/Medium je Host (Kreisel/Strecke): Layout
|
||||
#TRO107.stInSeparator1.Settings.tTrailingTime := t#100ms; // Default: Typ-Default, bei IBN prüfen
|
||||
#TRO107.stInSeparator1.Settings.tHandlingTime := t#50ms; // Default: Typ-Default, bei IBN prüfen
|
||||
#TRO107.stInSeparator1.Settings.tJam := "TimeForSepJam"; // Default: Projektkonstante
|
||||
#TRO107.stInSeparator1.Settings.xPluggedJam := TRUE; // HW-Planung: Stausensor (VERW STAU_REF-LT) vorhanden
|
||||
#TRO107.stInSeparator1.Settings.xPluggedPart := FALSE; // HW-Planung: kein Beladesensor (VERW BT BELADEN)
|
||||
#TRO107.stInSeparator1.Settings.xPluggedScanner := FALSE; // Layout: kein ILS 2.0 Scanner mit "Naechster Separator" = dieser
|
||||
#TRO107.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG3306"]; // HW-Planung + Layout: BG mit VERW IN SEP am Separator; cInBG3306 aus TIA Constants
|
||||
#TRO107.stInSenJam2 := "DB_Inputs".Sensors["cInBG3305"]; // HW-Planung + Layout: Stausensor; Ausgang 2 = Weiterfahrt auf dem Kreisel
|
||||
#TRO107.stInSenJam3 := "DB_Inputs".Sensors["cInBG3330"]; // HW-Planung + Layout: Stausensor; Ausgang 3 = Abzweig am Ausschleuselement
|
||||
|
||||
#TRO107.stInSwitch1.Settings.nSwitchNo := "cSwi107.1"; // Default: Namensregel cSwi<TRO>.<n>; Anzahl Weichen: Layout
|
||||
#TRO107.stInSwitch1.Settings.xPluggedExSen3 := FALSE; // HW-Planung: Abgangssensor Exit 3 verbaut?
|
||||
#TRO107.stInSwitch1.Settings.xPluggedExSen4 := FALSE; // HW-Planung: Abgangssensor Exit 4 verbaut?
|
||||
#TRO107.stInSwitch1.Settings.nTo1Destinations := 3; // Layout: über Abgang 1 erreichbare Ziele (reachable_via)
|
||||
|
||||
#TRO107.stInSettings.xPrioDir2 := TRUE; // Layout: Zusammenführung vor Ausgang 2 -> Vorfahrt nötig
|
||||
#TRO107(stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.udt_TRO107.TroIn, // Default: Namensregel udt_TRO<n>; ob nötig: Layout
|
||||
stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.udt_TRO107.TroOut, // Default: Namensregel
|
||||
nInMainTroNo := "cMainTro107", // Default: Namensregel cMainTro<n>
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q, // HW-Planung: Not-Halt-Kreis OF1 (Safety-Programm, F-Karten im WSCAD)
|
||||
xInAllRdyToStart := TRUE, // Default
|
||||
xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed, // Layout: Host-Kreisel; Name 0102: HW-Planung (KR-K0102, MA/FC-Bezeichnung)
|
||||
xInRelease := TRUE, // Default: Freigabe/Testlogik
|
||||
stInOutMachineState := #stInOutControlUnitCabinetOF1, // HW-Planung: Schaltschrank OF1 (KENNZEICHNUNG, WSCAD Bezug)
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy, // Layout: kein Scanner am TRO
|
||||
arInOutJamEntr1 := "DB_JamArea0102".stJam0102_1.arCarrier, // Layout: Vorgänger-Staubereich; DB-Name 0102: HW-Planung (Kreisel K0102) + Default (Index _1)
|
||||
stInOutJamEntr1 := "DB_JamArea0102".stJam0102_1.stData, // wie oben
|
||||
arInOutJamExit2 := "DB_JamArea0103".stJam0103_1.arCarrier, // Layout: Nachfolger an Ausgang 2; DB-Name: HW-Planung + Default
|
||||
stInOutJamExit2 := "DB_JamArea0103".stJam0103_1.stData, // wie oben
|
||||
arInOutJamExit3 := "DB_JamArea0102".stJam0102_2.arCarrier, // Layout: Nachfolger an Ausgang 3; DB-Name: HW-Planung + Default
|
||||
stInOutJamExit3 := "DB_JamArea0102".stJam0102_2.stData, // wie oben
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO107, // Default: Namensregel
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro107"], // Default: Namensregel
|
||||
xOutStopper1 => "MB3306", // HW-Planung: MB mit VERW SEP, Nummernregel ab BG3306 (TIA PLC Tag)
|
||||
xOutSw1ExTo3 => "MB3304"); // HW-Planung + Layout: MB AE ABZWEIG am Ausschleuselement von Ausgang 3
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi107.1", // Default: Namensregel
|
||||
nOutDirection => #TRO107.nInDirection, // Default: fest
|
||||
InOutCarrier := "DB_JamArea0102".stJam0102_1.arCarrier[1]); // Layout: Carrier aus dem Eingangs-Staubereich
|
||||
|
||||
END_REGION
|
||||
END_REGION
|
||||
```
|
||||
|
||||
### `FB_ILS_MTRO_Vario` (TRO 115, angetriebene Strecke mit Separator)
|
||||
|
||||
```pascal
|
||||
REGION TRO 115 (Vario) // Layout: Strecke mit L_VF_m / Antriebfahrtrichtung "Auf"; HW-Planung bestätigt (MA, MT in Position)
|
||||
#TRO115.stInSeparator1.Settings.nSeparatorNo := "cSep115.1"; // Default: Namensregel; Anzahl Separatoren: Layout
|
||||
#TRO115.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short"; // Default; Wahl je Host: Layout
|
||||
#TRO115.stInSeparator1.Settings.tTrailingTime := t#800ms; // Default: Typ-Default Vario, bei IBN prüfen
|
||||
#TRO115.stInSeparator1.Settings.tHandlingTime := t#300ms; // Default: Typ-Default Vario, bei IBN prüfen
|
||||
#TRO115.stInSeparator1.Settings.tJam := "TimeForSepJam"; // Default: Projektkonstante
|
||||
#TRO115.stInSeparator1.Settings.xPluggedJam := FALSE; // HW-Planung: kein Stausensor am Separator
|
||||
#TRO115.stInSeparator1.Settings.xPluggedPart := FALSE; // HW-Planung: kein Beladesensor
|
||||
#TRO115.stInSeparator1.Settings.xPluggedScanner := FALSE; // Layout: kein Scanner
|
||||
#TRO115.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG3025"]; // HW-Planung + Layout: BG IN SEP am Separator; Index aus TIA Constants
|
||||
|
||||
#TRO115.stInSeparator1.Settings.tSenFree := t#2000ms; // Default: Typ-Default
|
||||
#TRO115.stInSeparator1.Settings.tSenWait := T#5000ms; // Default: Typ-Default
|
||||
|
||||
#TRO115.stInVario.Settings.xContinousEmptying := FALSE; // Default: Prozessentscheidung
|
||||
#TRO115.stInVario.Settings.tWaitForJog := T#300ms; // Default: Typ-Default
|
||||
#TRO115.stInVario.Settings.tConvEmpty := T#45000ms; // Layout: Förderlänge L_VF_m / Geschwindigkeit (Default) -> heute fest
|
||||
#TRO115.stInVario.Settings.nCountCarrier := 1; // Default: Prozessentscheidung
|
||||
#TRO115.stInVario.Settings.tWaitForStop := t#3s; // Default: Typ-Default
|
||||
|
||||
#TRO115.stInVario.Settings.tCarrierInPosDelay := t#1200ms; // Default: Typ-Default
|
||||
#TRO115.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms; // Default: Typ-Default
|
||||
|
||||
#TRO115.stInVario.Settings.tRasterHigh := t#5s; // Default: Typ-Default
|
||||
#TRO115.stInVario.Settings.tRasterLow := t#5s; // Default: Typ-Default
|
||||
|
||||
#TRO115.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG3026"]; // HW-Planung: VERW "MT in Position" an dieser Strecke
|
||||
#TRO115.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG3027"]; // HW-Planung: VERW "Finger in position" / ABFR.FINGER
|
||||
#TRO115.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG3000"]; // HW-Planung: VERW "conveyor full"
|
||||
#TRO115.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG3001"]; // HW-Planung: VERW "Jam detector" / STAU_REF-LT
|
||||
|
||||
#TRO115.stInPriorityManager.xReleaseOk := TRUE; // Layout: keine Zusammenführung -> keine Vorfahrt
|
||||
#TRO115.stInVario.Settings.xJamSenDelayActive := TRUE; // Default (nur sinnvoll, wenn HW-Planung einen Stausensor hat)
|
||||
#TRO115.stInVario.Settings.tJamSenDelay := T#2s; // Default: Typ-Default
|
||||
|
||||
#TRO115.stInSettings.xIsBorner := TRUE; // Layout: erster TRO nach Systemgrenze (Beladung/Eingang)
|
||||
|
||||
#TRO115(nInMainTroNo := "cMainTro115", // Default: Namensregel
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q, // HW-Planung: Not-Halt-Kreis
|
||||
xInMotorProtection := "FC0145", // HW-Planung: FC mit Nummer des Motors (schaltschrank_elemente, TIA "Meldekontakt Motorschutzschalter")
|
||||
xInAllRdyToStart := TRUE, // Default
|
||||
xInReset := "cTodoFalse", // Default
|
||||
xInErrorQuit := "SF0002", // HW-Planung: Quittiertaster (SF, Schrank)
|
||||
xInAutomatic := #t_Automatik_OF1 AND "SF0120", // HW-Planung: Schlüsselschalter SF0120; Verknüpfung: Default
|
||||
xInRelease := "DB_TRO_TEST".TRO115, // Default: Test-/IBN-Freigabe
|
||||
arInOutJamEntr := "DB_JamArea4104".stJam4104_1.arCarrier, // Layout: Vorgänger-Staubereich; Nummer: HW-Planung (M-Nummer) + Default
|
||||
stInOutJamEntr := "DB_JamArea4104".stJam4104_1.stData, // wie oben
|
||||
arInOutJamFinger := "DB_JamArea4103".stJam4103_1.arCarrier, // Layout: Staubereich auf der Vario-Strecke selbst; Nummer: HW-Planung + Default
|
||||
stInOutJamFinger := "DB_JamArea4103".stJam4103_1.stData, // wie oben
|
||||
stInOutMachineState := #stInOutControlUnitCabinetOF1, // HW-Planung: Schaltschrank OF1
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy, // Layout: kein Scanner
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO115, // Default: Namensregel
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro115"], // Default: Namensregel
|
||||
xOutStopper => "MB3025", // HW-Planung: MB VERW SEP, Nummernregel ab BG3025
|
||||
xOutVarioMotor => "MA0145"); // HW-Planung + Layout: MA an dieser angetriebenen Strecke (Lage bzw. Bezeichnung)
|
||||
END_REGION
|
||||
```
|
||||
|
||||
### Auszählung der beiden Beispiele
|
||||
|
||||
| Führende Quelle | 1Sep1Swi (39 Zeilen) | Vario (47 Zeilen) |
|
||||
|---|---|---|
|
||||
| Layout | 13 | 10 |
|
||||
| HW-Planung | 11 | 14 |
|
||||
| Default | 15 | 23 |
|
||||
|
||||
Gezählt sind alle kommentierten Zeilen (Zuweisungen und REGION-Kopf) nach der zuerst
|
||||
genannten Quelle; "wie oben" zählt wie die Zeile davor. Die Struktur (welche Zeilen es gibt, wie oft, in welcher Reihenfolge) kommt
|
||||
immer aus dem Layout; die Tabelle zählt, woher der **Wert** einer Zeile kommt. Der
|
||||
Großteil der Defaults sind Namensregeln und Zeiten, die nach dem Typ-Katalog
|
||||
vorbelegt und bei der Inbetriebnahme geprüft werden. Fast alle HW-Planung-Zeilen
|
||||
lassen sich aus TIA-Import und `positionsdraw` füllen; offen bleiben `xInSftyOk`
|
||||
(Safety-Programm) und die Scanner (PROFINET, nicht im Tag-Import).
|
||||
@@ -0,0 +1,290 @@
|
||||
# Brücke Layout ↔ E-Planung über `*_positionsdraw.json`
|
||||
|
||||
Stand 2026-10-15. Erst Machbarkeitsprüfung, inzwischen umgesetzt als
|
||||
`lib/e_planning.py` (Regeln `cfg/e_planning.ini`, Aufruf `bin/e_planning.bat`,
|
||||
`scl_skeleton --eplan`); Bedienung in `doc/Python_Scripts.md` §5c, Stand der
|
||||
Umsetzung unten. Am 2026-10-15 mit dem neuen Mubea-Datensatz in `data/Mubea/`
|
||||
(TIA-Import + Positionsdatei) nachgeprüft; Ergebnisse unter "Nachprüfung
|
||||
2026-10-15".
|
||||
|
||||
## Problem
|
||||
|
||||
Das Kapitel "Daten" in `doc/Klassenhierarchie.md` zeigt: das Hardwarelayout (CSV)
|
||||
liefert die **Struktur** (welche TROs, welche Separatoren/Weichen, wer mit wem), die
|
||||
E-Planung die **Namen und Adressen** (`BG…`, `MB…`, `MA…`, `FC…`). Die EA-Liste allein
|
||||
kennt aber keine Topologie. Es fehlt die Zuordnung **Layout-Bauteil (CSV-`TeileId`) →
|
||||
Betriebsmittelkennzeichen (BMK)**.
|
||||
|
||||
## Idee
|
||||
|
||||
Die E-Planung wird als Layer über der Hardwareplanung gezeichnet: Sensor- und
|
||||
Aktorsymbole liegen in der Zeichnung neben dem Bauteil, zu dem sie gehören. Das
|
||||
Kabellängen-Tool (`C:\10-Develop\gitrepos\kabellaengen`, `lib/getpositions.py`)
|
||||
schreibt diese Symbole mit Position und Attributen nach `<zeichnung>_positionsdraw.json`.
|
||||
Damit ist die Brücke eine **räumliche Zuordnung**: E-Symbol → nächstes Layout-Bauteil
|
||||
passender Art.
|
||||
|
||||
## Datenquelle `*_positionsdraw.json`
|
||||
|
||||
| Schlüssel | Inhalt | Nutzen für sps_skel |
|
||||
|---|---|---|
|
||||
| `sensors` | je BMK (`BG0642@UNKNOWN`): `pos` [x, y], `VERW` (Verwendung), `BEZEICHNUNG` (Klartext), `BESCHR`, `ARTINR`, `KENNZEICHNUNG` (`=A1+UH01-KF107`) | Hauptquelle: Sensoren/Ventile/Motoren im Feld |
|
||||
| `schaltschrank_elemente` | Elemente im Schrank (Motorschutz `FC…`, Taster, Leuchten, `QA…`) mit `VERW`, `BEZEICHNUNG`, `KENNZEICHNUNG`, ohne Feldposition | Motorschutz ↔ Motor, Bedienelemente |
|
||||
| `mappings` | Steuerung → Liste ihrer BMK (`UH01: [...]`) | Steuerungsaufteilung, Nummernkreis |
|
||||
| `racks`, `distributors`, `tunnels` | Kabeltrassen, Verteiler | für sps_skel ohne Belang |
|
||||
| `double_ids`, `warnings` | doppelte BMK, fehlende Attribute | Qualitätsprüfung der E-Daten |
|
||||
|
||||
Die Erdungs-Beispiele (`PR500501_…_Erdung_…`, `ST500652_…`) enthalten nur
|
||||
Potentialausgleich (`POT…`) und taugen nicht für die Brücke. Brauchbar sind
|
||||
Dateien mit `BG`/`MB`/`MA`: `Mubea_positionsdraw.json` (158 Feld-BMK, davon 157
|
||||
in `mappings` unter UH01 — `BG0514` hat eine leere `KENNZEICHNUNG`),
|
||||
`ST500592_ILS_positionsdraw.json` (~3100), `ST500592_10_5-10_Omniflo_…` (~900),
|
||||
`ST-Fortna_positions_.json` (mit 38 Scannern `BX`).
|
||||
|
||||
`*_positionsconv.json` und `*_positionsdraw.json` sind bei Mubea byte-gleich;
|
||||
eine von beiden genügt.
|
||||
|
||||
## Datenquelle TIA-Import `*_TIA.xlsx`
|
||||
|
||||
Die zweite nötige Quelle. Blatt **PLC Tags**: je Signal Name (`BG0542`),
|
||||
Absolutadresse (`%E…`/`%A…`), Kommentar. Blatt **Constants**: je Signal eine
|
||||
Index-Konstante `cIn<BMK>`/`cOut<BMK>` (laufender Index, **nicht** aus der
|
||||
Adresse berechenbar).
|
||||
|
||||
| Nutzen | Warum |
|
||||
|---|---|
|
||||
| SCL übersetzbar | Sensoren stehen im SCL als `"DB_Inputs".Sensors["cInBG…"]`, Ventile als `"MB…"`; beides muss im TIA-Projekt existieren — der TIA-Import liefert genau diese Tags und Konstanten |
|
||||
| Prüfliste | jeder Name im erzeugten SCL muss im TIA-Import stehen |
|
||||
| `FB_CallSensors`, Größe `DB_Inputs` | alle `cIn…`-Konstanten mit Wert (noch nicht generiert) |
|
||||
| Schrank- und Antriebssignale | `SF…` (Taster, Not-Halt), `PF…` (Leuchten), `MA…`/`FC…` (Motor/Motorschutz), `GF…` (Geber), `BP…`, `DI…` |
|
||||
|
||||
Mubea: 237 Tags (105 BG, 64 MB, 26 SF, 17 PF, 11 MA, 11 FC, je 1 BP/DI/GF) und
|
||||
237 Konstanten (144 `cIn`, 93 `cOut`). Alle 158 Feld-BMK und 80 von 81
|
||||
Schrankelementen (fehlt: `QA7000`) der Positionsdatei stehen im TIA-Import, die
|
||||
Kommentare sind 1:1 gleich der `BEZEICHNUNG`.
|
||||
|
||||
**Nicht nötig** (Mubea): `*_EA.xlsx` (dieselben 237 Signale, nur Adresse und
|
||||
Kommentar, keine Namen), `*_WSCAD.xlsx` (zusätzlich nur die E/A-Baugruppe je
|
||||
Kanal, die `KENNZEICHNUNG` der Positionsdatei schon trägt, und 259 freie Kanäle),
|
||||
`*_BOM.xlsx`, `*_cables.*`, `*_todraw.json`, `*_errors.json` (Kabel, Stückliste,
|
||||
Trassen; die Qualitätswarnungen stehen auch in der Positionsdatei). Eingaben für
|
||||
die Brücke also: **CSV + TIA-Import + eine Positionsdatei**.
|
||||
|
||||
## Test an Mubea
|
||||
|
||||
Eingaben: `data/mubea.csv` (Layout, 38 Separatoren) und
|
||||
`kabellaengen/work/Mubea_positionsdraw.json` (E-Planung, alter Stand). Die Zahlen
|
||||
in diesem Abschnitt stammen aus der ersten Untersuchung (Umkreis 700 mm, ohne
|
||||
Regeln); das Ergebnis der Umsetzung und die Nachprüfung mit dem neuen Stand
|
||||
`data/Mubea/Mubea_positionsdraw.json` stehen unten.
|
||||
|
||||
### 1. Lagebezug
|
||||
|
||||
Die Ermittlung aus `lib/dxf_registration.py` (`derive_transform`) funktioniert
|
||||
unverändert, wenn man die E-Symbole mit `VERW` `SEP`/`IN SEP` als Rolle "Separator"
|
||||
einsetzt:
|
||||
|
||||
```text
|
||||
mubea.csv <- Mubea_positionsdraw.json: dx +50613.6 mm, dy +8555.8 mm, Drehung 0
|
||||
26/38 Separatoren im Toleranzband
|
||||
```
|
||||
|
||||
Das entspricht dem schon gespeicherten Bezug `mubea.csv|500573_60_1.dxf`
|
||||
(dx +50833.7, dy +8414.5) bis auf ~250 mm: diese (alte) E-Zeichnung lag im selben
|
||||
Koordinatensystem wie die Layoutzeichnung. Zum Prüfen genügt also der
|
||||
bestehende Mechanismus; als Schlüssel in `cfg/dxf_registration.json` dient
|
||||
`<csv>|<positionsdraw.json>`.
|
||||
|
||||
Das gilt **nicht allgemein**: der neue Stand in `data/Mubea/` liegt in einem
|
||||
anderen Koordinatensystem (dx −151.4, dy −696.6, siehe "Nachprüfung
|
||||
2026-10-15"). Der Lagebezug ist je Datei neu zu ermitteln, nicht von der
|
||||
Layoutzeichnung zu übernehmen.
|
||||
|
||||
### 2. Was sich zuordnen lässt
|
||||
|
||||
| E-Symbol (`VERW`) | Layout-Partner | Abstand | Ergebnis |
|
||||
|---|---|---|---|
|
||||
| `IN SEP` (BG, Sensor im Separator) | Separator | **0–1 mm** im Pin-Speicher (20/20), sonst 200–500 mm | Anker: das Symbol sitzt auf dem Separator |
|
||||
| `SEP` (MB, Stopperventil) | Separator | konstant **~270 mm** daneben | eindeutig über Nummernregel (unten), nicht über Abstand |
|
||||
| `AE ABZWEIG` (MB, Ausschleusventil) | Ausschleuselement | konstant **1414 mm** (20/20, Versatz (−1405, −159)) | eindeutig, wenn der Versatz je Symbolart bekannt ist |
|
||||
| `STAU_REF-LT` (BG, Stausensor) | Ausschleuselement | konstant **1391 mm** (20/20) | wie oben |
|
||||
| `ABZWEIG-K104` (MB, Weiche) | Weiche | 783 mm | eindeutig (nur eine Weiche) |
|
||||
| `BANDBRUCH`, `KETTENBRUCH`, `ÜBERGABE`/`ÜBERNAHME`, `SYNC P1..4`, `ZAPPLER` | Kreisel/Strecke/Station | uneinheitlich, teils > 50 m (Symbol an Konvejoren außerhalb des CSV-Ausschnitts) | nicht über Abstand, nur über `BEZEICHNUNG` |
|
||||
|
||||
Nebeneinander liegende Separatoren (Pin-Speicher, Raster 800 mm) machen den
|
||||
Abstand allein mehrdeutig: das MB von Strecke n liegt 270 mm von Separator n und
|
||||
530 mm von Separator n+1. Eindeutig wird es erst mit der Nummernregel.
|
||||
|
||||
### 3. Nummernregel der E-Planung
|
||||
|
||||
Ventil = Sensor + 1, gleiche Funktionsgruppe:
|
||||
|
||||
| Paar | Treffer (von 58 MB) |
|
||||
|---|---|
|
||||
| `MB(n)` `SEP` ↔ `BG(n−1)` `IN SEP` (z. B. BG0642 / MB0643) | 31 |
|
||||
| `MB(n)` `AE ABZWEIG` ↔ `BG(n−1)` `STAU_REF-LT` (z. B. BG0542 / MB0543) | 21 |
|
||||
| sonstige Paare (`VEREINZ(CP)`, `STOPPER (CP)`, `AE ABZW.`) | 3 |
|
||||
| ohne BG-Partner | 3 |
|
||||
|
||||
Damit reicht es, den **Sensor** räumlich zuzuordnen (der `IN SEP` liegt auf dem
|
||||
Separator); das Ventil folgt aus der Nummer. Die SCL-Referenzen zeigen dasselbe
|
||||
Muster (`stSenInSep := …["cInBG3242"]`, `xOutStopper1 => "MB3242"`) — dort sogar
|
||||
mit gleicher Nummer; die Regel ist also projektabhängig und gehört in eine
|
||||
Konfiguration, nicht in den Code.
|
||||
|
||||
Gleiches gilt für Motor ↔ Motorschutz: `MA0200` "PIN CONVEYOR#100" ↔
|
||||
`FC0200` "MOTORSCHUTZ CONVEYER #100" (gleiche Nummer, `schaltschrank_elemente`),
|
||||
wie in der SCL `xOutVarioMotor => "MA0145"`, `xInMotorProtection := "FC0145"`.
|
||||
|
||||
### 4. Was nicht passt
|
||||
|
||||
12 von 38 Layout-Separatoren (vor allem an den Kreiseln K0102/K0104/K0105) haben im
|
||||
Umkreis von 700 mm kein `IN SEP`/`SEP`; umgekehrt liegen mehrere `IN SEP` des
|
||||
Kreisels K0102 neben Aus-/Einschleuselementen statt neben einem Separator. Vermutlich
|
||||
ist die CSV (aus `500573_60_1.dxf`) ein anderer Planungsstand als die E-Zeichnung
|
||||
`Mubea.dxf` des Kabellängen-Tools, oder Kreisel-Separatoren sind im CSV anders
|
||||
modelliert. Das muss mit zueinander passenden Ständen geprüft werden, bevor man
|
||||
eine Quote angibt.
|
||||
|
||||
`Mubea_positionsdraw.json` enthält **keine Scanner** (`BX`). Für Scanner-Zuordnung
|
||||
ist ein anderes Projekt nötig (`ST-Fortna_positions_.json`, `ILS-Baugruppe`).
|
||||
|
||||
## Was die Brücke für das Kapitel "Daten" liefert
|
||||
|
||||
| SCL-Datum | aus `positionsdraw.json` | Regel |
|
||||
|---|---|---|
|
||||
| `stInSeparator<n>.stSenInSep` | BG mit `VERW` `IN SEP` | räumlich am Separator (Anker) |
|
||||
| `xOutStopper<n>` | MB mit `VERW` `SEP` | Nummernregel ab dem BG |
|
||||
| `stInSenJam<n>`, `Settings.xPluggedJam` | BG `STAU_REF-LT` | räumlich, Versatz je Symbolart; vorhanden ⇒ `xPluggedJam := TRUE` |
|
||||
| `xOutSw<n>ExTo3/4` | MB `AE ABZWEIG` / Weichen-MB | Nummernregel ab dem Stausensor bzw. räumlich an Weiche/Ausschleuselement |
|
||||
| `Settings.xPluggedPart`, `stSenPart` | BG `BT BELADEN` | räumlich; vorhanden ⇒ `TRUE` |
|
||||
| `xInMotorProtection`, `xOutVarioMotor` | MA im Feld + FC in `schaltschrank_elemente` | gleiche Nummer |
|
||||
| `xInCarouselRun` (welcher Antrieb) | FC/MA-`BEZEICHNUNG` "KREISEL-K0102", "CONVEYOR #100" | **räumlich**, nicht über Text: das CSV nennt die Kreisel `Kreisel1/2/3`, die E-Planung `K0102/K0104/K0105`. Alle Symbole, deren `BEZEICHNUNG` einen Kreisel nennt, per Mehrheit dem nächsten CSV-Kreisel zuordnen (siehe Nachprüfung) |
|
||||
| Steuerung, TRO-Nummernkreis, `stInOutMachineState` | `KENNZEICHNUNG` `=A1+UH01-KF107`, `mappings` | `UH01` = Steuerung, `KF107` = E/A-Station |
|
||||
| Plausibilisierung | `BEZEICHNUNG` "SEPARATOR STR. 14", "KR-K0102 VOR BT-BELADUNG" | Strecken-/Kreiselnummer gegen Layout prüfen |
|
||||
|
||||
Nicht aus dieser Quelle: Scanner-Hardwarekennungen, Weichenbauart (`xNewSwitch`).
|
||||
|
||||
Auch nicht aus den E/A-Daten, aber **je Projekt nur ein Wert**: `xInSftyOk`,
|
||||
`stInOutMachineState`, `xInAutomatic`, `xInErrorQuit`. Der TIA-Import enthält
|
||||
zwar die Not-Halt-Kanäle (`SF5000…SF6006`) und `SF0001` "ANLAGE EIN" / `SF0002`
|
||||
"START NACH STÖRUNG", die Referenz-SCL verdrahtet diese Parameter aber nicht mit
|
||||
E/A-Signalen, sondern mit dem Sicherheitsprogramm
|
||||
(`"DB_InterfaceSafety".stFromSafety.EStopMain….Q`) bzw. der Schrank-Struktur
|
||||
(`#stInOutControlUnitCabinet…`). Sie gehören als Projektkonstanten in eine
|
||||
Konfiguration (bei Mubea ~39 der offenen Stellen), nicht in die Brücke.
|
||||
|
||||
## Umsetzung (Stand 2026-10-10)
|
||||
|
||||
1. **`lib/e_planning.py`**: liest `*_positionsdraw.json` in `EDevice` (BMK, VERW,
|
||||
Bezeichnung, Kennzeichnung → Steuerung/E/A-Station, Position).
|
||||
2. **Lagebezug** über `dxf_registration.derive_transform` mit den `IN SEP`-Sensoren
|
||||
als Rolle "Separator"; abgelegt in `cfg/dxf_registration.json` unter
|
||||
`<csv>|<positionsdraw.json>`.
|
||||
3. **Regeln in `cfg/e_planning.ini`** (projektabhängig): je Rolle Layout-Art, VERW,
|
||||
Höchstabstand, **Sollabstand** (für die konstanten Versätze von Stausensor und
|
||||
Ausschleusventil — sonst gewinnt bei 0080/0084 ein näherer, fremder Stausensor
|
||||
des Kreisels K0102) und die Nummernregel (`MB = BG + 1`).
|
||||
4. **Ausgabe** `<csv>_eplan.json` (`EplanDocument`) und Bericht `<csv>_eplan.md`.
|
||||
`scl_skeleton --eplan` setzt Separator-Sensor und Stopperventil ein
|
||||
(`SclName.todo = False`); alles andere bleibt `TODO(E-Planung)`.
|
||||
|
||||
Ergebnis Mubea (alter Stand) mit den ausgelieferten Regeln: Separator-Sensor 21/38,
|
||||
Stopper 22/38, Stausensor 20/23, Ausschleusventil 20/23, Weichenventil 1/1. Den
|
||||
Separator-Sensor nimmt die Regel nur bis 300 mm; die Kreisel-Fälle mit 470–540 mm
|
||||
(z. B. BG0532 an 0044) bleiben bewusst offen.
|
||||
|
||||
## Nachprüfung 2026-10-15 (neuer Stand `data/Mubea/`)
|
||||
|
||||
Lauf: `e_planning --file mubea.csv --eplan <voller Pfad>\data\Mubea\Mubea_positionsdraw.json`,
|
||||
dann `tro_flow --json` und `scl_skeleton --eplan --templates`.
|
||||
|
||||
| | alter Stand | neuer Stand |
|
||||
|---|---|---|
|
||||
| Lagebezug | dx +50613.6, dy +8555.8 | **dx −151.4, dy −696.6**, Drehung 0, 23/38 Punkte, Restfehler max 541 mm |
|
||||
| Separator-Sensor | 21/38 | 21/38 |
|
||||
| Stopper | 22/38 | 22/38 |
|
||||
| Stausensor | 20/23 | **21/23** |
|
||||
| Ausschleusventil | 20/23 | **19/23** |
|
||||
| Weichenventil | 1/1 | 1/1 |
|
||||
| Antriebe | — | 11, alle mit Motorschutz |
|
||||
|
||||
Im erzeugten `FB_Main` (18 TROs): 43 Namen aus der E-Planung, 440 Stellen
|
||||
`TODO(E-Planung)`. Alle 21 Konstanten und 22 Tags im SCL stehen im TIA-Import.
|
||||
|
||||
Befunde:
|
||||
|
||||
- **Pfad:** `--eplan Mubea/Mubea_positionsdraw.json` (Unterordner relativ zu
|
||||
`%SKEL_DATA%`) wird nicht gefunden; nur ein Dateiname direkt in `%SKEL_DATA%`
|
||||
oder ein voller Pfad geht.
|
||||
- **Gespeicherter Lagebezug:** der Schlüssel `<csv>|<positionsdraw.json>` in
|
||||
`cfg/dxf_registration.json` enthält nur den Dateinamen. Ein neuer Stand mit
|
||||
gleichem Namen übernimmt ohne `--re-register` still den alten Bezug — beim
|
||||
Mubea-Wechsel wären das ~51 m Versatz gewesen.
|
||||
- **Kreisel-Zuordnung räumlich möglich:** die Symbole, deren `BEZEICHNUNG` einen
|
||||
Kreisel nennt (nach dem Lagebezug, Abstand zur Boundingbox), liegen eindeutig
|
||||
auf einem CSV-Kreisel — **K0102 = Kreisel3 (0010)**, **K0104 = Kreisel2 (0002)**,
|
||||
**K0105 = Kreisel1 (0001)**; ein Ausreißer (`BG0725` "BANDBRUCHÜBERWACHUNG
|
||||
KR-K0105" an Kreisel2). Damit erhält jeder Kreisel seinen Antrieb
|
||||
(`MA0202`/`FC0202`, `MA0205`/`FC0205`, `MA0206`/`FC0206`) und K0102 seinen Geber
|
||||
`GF20000`.
|
||||
|
||||
Was sich mit TIA-Import + Positionsdatei noch schließen lässt:
|
||||
|
||||
| Offene Stellen (Mubea) | Anzahl | Weg |
|
||||
|---|---|---|
|
||||
| `stInSenJam<n>` | ~17 | schon zugeordnet; fehlt nur TRO-Ausgang → Ausschleuselement (siehe "Offen") |
|
||||
| `xInCarouselRun` | ~13 | Kreisel-Zuordnung oben + Motorpaar |
|
||||
| Vario-Sensoren (`stSenLastPos`, `stSenJam`, `stSenFinger`, `stSenCarrInPos`), `xOutVarioMotor`, `xInMotorProtection` | ~18 | wahrscheinlich, neue Regeln über `VERW` (`SYNC P1..4`, `ZAPPLER`, `ABFR.FINGER`, `BT BELADEN`) und die Motorpaare; zu prüfen |
|
||||
| `xInSftyOk`, `stInOutMachineState`, `xInAutomatic`, `xInErrorQuit` | ~39 | Projektkonstante (oben), nicht aus diesen Dateien |
|
||||
| `nSeparatorNo`/`nSwitchNo`, Zeiten, Staubereichs-Verdrahtung | Rest | weder TIA noch Positionsdatei |
|
||||
|
||||
Die 17 nicht zugeordneten Separatoren an den Kreiseln sind ein Standunterschied
|
||||
Layout ↔ E-Zeichnung, keine fehlenden Daten.
|
||||
|
||||
Offen:
|
||||
|
||||
- Stausensoren und Ausschleus-/Weichenventile stehen in JSON und Bericht, kommen
|
||||
aber noch nicht in die SCL: die TRO-JSON sagt nicht, welches Ausschleuselement
|
||||
bzw. welche Weiche zu welchem TRO-Ausgang gehört.
|
||||
- Die Lagerlinien eines `PinStore`-TRO (`arInSep[n]`) sind zugeordnet, der
|
||||
Generator erzeugt die Linienparameter aber noch nicht.
|
||||
- Scanner (`BX`) fehlen in den Mubea-Daten; Regel und Test mit
|
||||
`ST-Fortna_positions_.json` stehen aus.
|
||||
- Ein Abgleich mit passenden Ständen von CSV und E-Zeichnung für die Kreisel.
|
||||
- ~~TIA-Import einlesen~~ — **erledigt** 2026-10-15: `lib/hw_import.py`
|
||||
(`<projekt>_hw.json`); `FB_CallSensors` daraus steht noch aus.
|
||||
- ~~Kreisel ↔ E-Kreisel (`K01xx`) räumlich zuordnen~~ — **erledigt** in
|
||||
`lib/merge_hw_layout.py` (Mehrheit, nur Förderer der genannten Art); in die SCL
|
||||
(`xInCarouselRun`) kommt es mit der Umstellung von `scl_skeleton` auf die
|
||||
Anlagen-JSON.
|
||||
- ~~Gespeicherten Lagebezug absichern~~ — **erledigt**: SHA-256 der
|
||||
Positionsdatei im Eintrag von `cfg/dxf_registration.json`.
|
||||
- ~~Unterordner in `--eplan`~~ — **erledigt** (`material_flow.resolve_input`).
|
||||
|
||||
## Abgleich Layoutzeichnung ↔ CSV ↔ E-Planung (2026-10-15)
|
||||
|
||||
Zeichnung `data/mubea_layout.dxf` (aus `mubea_layout.dwg`), gelesen über alle
|
||||
Blockebenen mit vollständiger Blocktransformation (die `Staustrecke…`-Blöcke
|
||||
der Gefällestrecken sind mit gekippter Extrusionsrichtung eingefügt; ohne
|
||||
OCS liegen ihre Separatoren falsch).
|
||||
|
||||
| Vergleich | Ergebnis |
|
||||
|---|---|
|
||||
| DXF ↔ `mubea.csv` | **gleiches Koordinatensystem, kein Versatz.** 38/38 Separatoren je 60–70 mm (Einfügepunkt `Separator_SP_3D` gegen CSV-Punkt, in Blockrichtung), 8/8 Scanner je 360 mm, Aus-/Einschleuselemente 22/23 bzw. 22/23 auf 0 mm (`0061`, `0064` und die Weiche `0125` sind keine eigenen Blöcke) |
|
||||
| E-Planung ↔ DXF | (−211, −697) mm; Speicherlinien 20/20 auf **1 mm** |
|
||||
| E-Planung ↔ CSV | (−151, −697) mm; 23/38 im Toleranzband |
|
||||
|
||||
Die 20 Separatoren der Gefällestrecken passen mit dem Versatz exakt (ohne ihn
|
||||
liegt jeder Sensor eine Speicherlinie daneben, 235 mm). Die 12 Separatoren an
|
||||
Kreiseln/Strecken und die 6 Vario-Separatoren passen in **keiner** Lage
|
||||
(155–3438 mm, mit und ohne Versatz). Es gibt also keine starre Transformation
|
||||
für die ganze Anlage: der Speicher ist in der E-Zeichnung als Ganzes
|
||||
verschoben, der Bereich um die Kreisel ist ein anderer Planungsstand.
|
||||
Folge: die Kreisel-Separatoren bleiben offen, bis E-Zeichnung und Layout zum
|
||||
selben Stand gehören; `merge_hw_layout` warnt deshalb (23/38 = 61 %).
|
||||
|
||||
Nebenbefund für `tro_annotate` (Kontrollansicht): es liest nur die
|
||||
Modellbereichs-Blöcke (12 der 38 Separatoren, die übrigen stecken in
|
||||
`GF_n`/`VF_n`), und der Lagebezug übernimmt den 60-mm-Einfügeversatz als
|
||||
scheinbare Verschiebung (dx +59.8).
|
||||
@@ -0,0 +1,741 @@
|
||||
# Klassenhierarchie
|
||||
|
||||
Bestandsaufnahme aller Python-Klassen in `lib/` (Stand 2026-10-10). Kurzes
|
||||
Ergebnis vorweg: es gibt praktisch **keine Vererbung** in diesem Code (eine
|
||||
Ausnahme: `DerivedTro(TroBase)`, plus die Standard-Exception
|
||||
`RegistrationError(RuntimeError)`). Alles andere sind flache, eigenständige
|
||||
Klassen — entweder `@dataclass` (veränderlicher Laufzeit-/Analysezustand) oder
|
||||
Pydantic `BaseModel` (validierte Daten, die auch als JSON ein-/ausgehen). Eine
|
||||
klassische "ist-ein"-Hierarchie zu suchen wäre hier der falsche Blickwinkel;
|
||||
interessanter ist, **wer wen enthält/referenziert** und in welchem Modul-Layer
|
||||
eine Klasse sitzt (siehe Layer-Kommentar oben in `model.py`).
|
||||
|
||||
## Modul-Layer (wer darf wen importieren)
|
||||
|
||||
```
|
||||
tro_catalog.py (Typ-Ebene: was ein TRO-Typ ist)
|
||||
|
|
||||
v
|
||||
model.py (Instanz-Ebene: ein konkreter TRO, die TRO-JSON)
|
||||
|
|
||||
v
|
||||
tro_flow.py (Ableitung: CSV-Graph -> Analysis -> TroDocument)
|
||||
|
|
||||
+----------------------+
|
||||
v v
|
||||
scl_context.py tro_annotate.py (Ausgabe: annotierte Zeichnung)
|
||||
(TroDocument -> SclFile, Render-Kontext)
|
||||
|
|
||||
v
|
||||
scl_skeleton.py (Ausgabe: SclFile -> FB_Main.scl, generisch oder per Jinja-Template)
|
||||
|
||||
material_flow.py dxf_registration.py (eigenständig, ohne Abhängigkeit auf obige)
|
||||
tro_overrides.py (importiert model.Connection / model.TroCandidate)
|
||||
e_planning.py (Brücke zur E-Planung; importiert dxf_registration, liest das
|
||||
CSV über material_flow; scl_context kennt nur EplanDocument)
|
||||
```
|
||||
|
||||
`tro_flow.py` und `tro_annotate.py` definieren keine eigenen Klassen (nur
|
||||
Funktionen). `scl_skeleton.py` hat genau eine (`SclTemplates`, Jinja-Anbindung).
|
||||
`material_flow.py` hat Graph-Dataclasses — das ist CSV-Rohmodell, nicht
|
||||
TRO-Modell.
|
||||
|
||||
## Vererbung (ASCII)
|
||||
|
||||
```
|
||||
RuntimeError (stdlib) BaseModel (pydantic, stdlib: pydantic)
|
||||
| |
|
||||
+-- RegistrationError +-- _Positioned (model.py, Mixin)
|
||||
(dxf_registration.py) |
|
||||
+-- TroBase
|
||||
| +-- DerivedTro
|
||||
+-- TroCandidate
|
||||
```
|
||||
|
||||
Das ist die gesamte `class X(Y)`-Liste im Projekt — alle anderen Klassen unten
|
||||
haben `object`/`BaseModel` direkt als einzige Basis und erscheinen hier nur
|
||||
noch als Gruppen nach Datei.
|
||||
|
||||
## Klassen nach Datei, mit Komposition ("enthält")
|
||||
|
||||
```
|
||||
lib/tro_catalog.py [Typ-Katalog, keine Instanzdaten]
|
||||
├── TroItem (Konstanten-Klasse: Bauteilarten wie SEPARATOR, SWITCH)
|
||||
├── TroSymbol (Konstanten-Klasse: Zeichensymbole wie CIRCLE, TRIANGLE)
|
||||
├── TroStyle(BaseModel) Farbe/Darstellung einer Typgruppe
|
||||
└── TroDefinition(BaseModel)
|
||||
└─ style: TroStyle (1 TroDefinition hat genau 1 TroStyle)
|
||||
|
||||
lib/model.py [Instanz-Ebene / TRO-JSON]
|
||||
├── TroBase(BaseModel)
|
||||
│ └─ definition() --> TroDefinition (gelesen aus tro_catalog, nicht gespeichert)
|
||||
│ └── DerivedTro(TroBase) [einzige echte Vererbung im Projekt]
|
||||
│ └─ slots: list[Slot] (Stellen fuer die Hardwareplanung)
|
||||
├── _Positioned(BaseModel) (Mixin: position {x,y,z}, describe_position)
|
||||
│ ├── TroBase, TroCandidate, JamArea
|
||||
├── Slot(BaseModel) (separator1/switch1/scanner1/drive + Rollen)
|
||||
├── JamArea(_Positioned) (Staubereich J<Sep>: Eingang eines TRO)
|
||||
├── Connection(BaseModel) (from/to/kind zwischen zwei TRO-IDs)
|
||||
├── TroCandidate(_Positioned) (gemeldete Stelle, an der ein TRO erwartet wird)
|
||||
├── Analysis(BaseModel)
|
||||
│ ├─ tros: list[DerivedTro]
|
||||
│ ├─ isolated: list[DerivedTro]
|
||||
│ └─ boundaries: list[DerivedTro]
|
||||
└── TroDocument(BaseModel) (= <csv>_tro.json)
|
||||
├─ tros: list[DerivedTro]
|
||||
├─ boundaries: list[DerivedTro]
|
||||
├─ connections: list[Connection]
|
||||
├─ candidates: list[TroCandidate]
|
||||
└─ jam_areas: list[JamArea] (JSON: jamAreas)
|
||||
|
||||
lib/material_flow.py [CSV-Rohmodell -> Graph, @dataclass]
|
||||
├── Element (1 CSV-Zeile)
|
||||
├── Node
|
||||
│ └─ element: Element
|
||||
├── Edge
|
||||
├── Graph
|
||||
│ ├─ nodes: dict[str, Node]
|
||||
│ ├─ edges: list[Edge]
|
||||
│ └─ unassigned: list[Element]
|
||||
├── TrackNode
|
||||
│ └─ element: Element | None
|
||||
├── TrackEdge
|
||||
└── TrackGraph
|
||||
├─ nodes: dict[str, TrackNode]
|
||||
└─ edges: list[TrackEdge]
|
||||
|
||||
lib/tro_overrides.py [manuelle Korrekturen aus cfg/tro_overrides.ini, @dataclass]
|
||||
├── TroOverride (Typkorrektur an einem abgeleiteten TRO: tro_id ODER separator)
|
||||
├── SplitSpec (TRO aus einem bestehenden heraustrennen)
|
||||
├── MergeSpec (mehrere TROs zu einem zusammenfassen)
|
||||
└── Overrides (Sammelcontainer für eine CSV-Datei)
|
||||
├─ tros: dict[str, TroOverride]
|
||||
├─ splits: list[SplitSpec]
|
||||
├─ merges: list[MergeSpec]
|
||||
├─ connections: list[model.Connection]
|
||||
└─ candidates: list[model.TroCandidate]
|
||||
|
||||
lib/dxf_registration.py [CSV<->DXF Koordinatentransform]
|
||||
├── RegistrationError(RuntimeError)
|
||||
└── Transform(BaseModel)
|
||||
|
||||
lib/scl_context.py [Render-Kontext für die SCL, BaseModel]
|
||||
├── SclName (Wert + Merker todo: noch aus der E-Planung offen?)
|
||||
├── SclSeparator (ein stInSeparator<n>-Block)
|
||||
├── SclSwitch (ein stInSwitch<n>-Block + FC_Direction-Aufruf)
|
||||
├── SclSwitchExit (Weichenabgang + darüber erreichbare TROs)
|
||||
├── SclScanner (ein stInBarcodeReader-Block)
|
||||
├── SclJam (Staubereich an Ein-/Ausgang)
|
||||
├── SclTro (alles für eine REGION)
|
||||
│ ├─ separators: list[SclSeparator]
|
||||
│ ├─ switches: list[SclSwitch]
|
||||
│ ├─ switch_exits: list[SclSwitchExit]
|
||||
│ ├─ scanners: list[SclScanner]
|
||||
│ ├─ jam_entries / jam_exits: list[SclJam]
|
||||
│ └─ main_tro, safety, ... : SclName
|
||||
└── SclFile (die ganze Datei)
|
||||
└─ tros: list[SclTro] (in Fliessrichtung)
|
||||
|
||||
lib/scl_skeleton.py [SCL-Ausgabe]
|
||||
└── SclTemplates (Ordner mit <fbType>.scl.j2, hält ein jinja2.Environment)
|
||||
|
||||
lib/e_planning.py [Brücke Layout <-> E-Planung]
|
||||
├── EDevice(BaseModel) (ein BMK aus *_positionsdraw.json: VERW, Bezeichnung, Position)
|
||||
├── LayoutPoint (@dataclass: punktartiges Layout-Bauteil in CSV-Koordinaten)
|
||||
├── Rule (@dataclass: eine Regel aus cfg/e_planning.ini)
|
||||
├── Assignment(BaseModel) (BMK in einer Rolle an einem Layout-Bauteil, mit Abstand)
|
||||
├── Drive(BaseModel) (Motor MA ↔ Motorschutz FC)
|
||||
└── EplanDocument(BaseModel) (= <csv>_eplan.json)
|
||||
├─ transform: Transform (aus dxf_registration)
|
||||
├─ assignments: list[Assignment]
|
||||
└─ drives: list[Drive]
|
||||
```
|
||||
|
||||
## Auffälligkeiten für ein mögliches Refactoring
|
||||
|
||||
1. ~~`OpenPoint` existiert zweimal~~ — erledigt 2026-10-10: zu
|
||||
`model.TroCandidate` zusammengefasst (`expect` → `type_name`, `x/y/z` →
|
||||
`position`); `ConnectionSpec` durch `model.Connection` ersetzt;
|
||||
`tro_overrides.py` importiert beide aus `model.py`.
|
||||
2. **Zwei parallele Graph-Hierarchien** in `material_flow.py`
|
||||
(`Node/Edge/Graph` vs. `TrackNode/TrackEdge/TrackGraph`) teilen sich keine
|
||||
Basisklasse, obwohl Struktur und Feldnamen sich stark ähneln (`node_id,
|
||||
kind, ...` / `src, dst, ...`). Eine gemeinsame leichte Basis (z. B. ein
|
||||
generisches `GraphBase[NodeT, EdgeT]`) wäre möglich, aber angesichts der
|
||||
wenigen Stellen, die beide Typen anfassen, vermutlich mehr Abstraktion als
|
||||
Nutzen.
|
||||
3. **`TroItem` und `TroSymbol` sind reine Konstanten-Namespaces** (keine
|
||||
Instanzen, keine Methoden außer `TroItem.all()`), kein Pydantic-Modell wie
|
||||
der Rest von `tro_catalog.py`. Das ist bewusst so (siehe Docstring: "damit
|
||||
Tippfehler auffallen") und funktioniert wie ein `Enum`-Ersatz — ein `StrEnum`
|
||||
wäre eine nahe liegende, idiomatischere Alternative, falls ein Refactoring
|
||||
ansteht.
|
||||
4. Die einzige echte Vererbung (`DerivedTro(TroBase)`) ist sauber: `TroBase`
|
||||
ist bereits so geschnitten, dass es für Systemgrenzen (`boundaries`)
|
||||
wiederverwendet wird, `DerivedTro` ergänzt nur das, was die CSV-Ableitung
|
||||
zusätzlich weiß. Kein Handlungsbedarf.
|
||||
|
||||
**Fazit:** Die Code-Basis nutzt Vererbung bewusst kaum — die Architektur trennt
|
||||
stattdessen über Module (Typ-Katalog vs. Instanz vs. Ableitung vs. Ausgabe,
|
||||
siehe `model.py`-Kopfkommentar) und über Komposition. Ein Refactoring in
|
||||
Richtung "mehr Vererbung" wäre hier eher kontraproduktiv. Die doppelte
|
||||
`OpenPoint`-Definition (1) ist behoben; offen bleibt nur optional `StrEnum`
|
||||
statt Konstanten-Klassen (3).
|
||||
|
||||
## Die komplexen Klassen im Einzelnen
|
||||
|
||||
Die folgenden Abschnitte beschreiben je Klasse, **wozu** sie da ist, **was
|
||||
darin passiert** und **wer sie erzeugt bzw. liest**. Einfache Datenhalter
|
||||
ohne eigene Logik — `Connection`, `TroCandidate`, `Edge`, `TrackEdge`,
|
||||
`TroItem`/`TroSymbol` und die kleinen Blöcke `SclSeparator`, `SclSwitch`,
|
||||
`SclSwitchExit`, `SclScanner`, `SclJam` — sind im Baum oben ausreichend
|
||||
beschrieben und bekommen keinen eigenen Abschnitt.
|
||||
|
||||
### `TroDefinition` (+ `TroStyle`) — `lib/tro_catalog.py`
|
||||
|
||||
**Wozu:** Beschreibt einen TRO-**Typ**, nicht eine Stelle in der Anlage: Kurzname
|
||||
(`1Sep1Swi`), Siemens-FB (`FB_ILS_MTRO_1Sep1Swi`), Bauteile
|
||||
(`{"Separator": 1, "Switch": 1}`), Farbgruppe und CAD-Symbol. Der Katalog
|
||||
aller Definitionen ist die ausführbare Form von `doc/TRO_Typen.md`.
|
||||
|
||||
**Was passiert darin:** Pydantic mit `validate_assignment` und `extra="forbid"` —
|
||||
eine Definition kann nie ungültig werden, auch nicht über die `set_*`-Methoden.
|
||||
`_clean_items` wirft Bauteile mit Anzahl ≤ 0 heraus. Ein eigener `__init__`
|
||||
erlaubt die alte Positionsform `TroDefinition("1Sep", "FB_...", {...})`. Dazu
|
||||
kommen Abfragen (`get_items`, `has_item`, `get_item_fb`, `get_total_items`) und
|
||||
Darstellungshelfer (`get_dot_attrs`, `mermaid_class/_classdef`,
|
||||
`describe_items`). `TroStyle` liefert nur die Farben einer Gruppe für Graphviz,
|
||||
Mermaid und CAD.
|
||||
|
||||
**Wer nutzt sie:** Über `get_tro(name)`/`catalog()` jeder, der vom Typnamen auf
|
||||
FB oder Bauteile kommen muss — vor allem `TroBase.definition()`; außerdem
|
||||
`tro_flow` (Typprüfung), `tro_annotate` (Farbe/Symbol, `--emit-lisp`) und die
|
||||
Doku-Grafiken.
|
||||
|
||||
### `TroBase` / `DerivedTro` — `lib/model.py`
|
||||
|
||||
**Wozu:** Ein konkreter TRO in einer Anlage (Instanz). `TroBase` ist der Kern, den
|
||||
jeder TRO hat, egal woher er stammt: `tro_id`, `type_name`, `separators`,
|
||||
`predecessors`/`successors`, `position`. `DerivedTro` ergänzt, was `tro_flow`
|
||||
beim Ableiten aus dem CSV zusätzlich weiß: `hosts`, `scanners`, `reason`,
|
||||
`colocated`, `position_from`/`position_points`. Systemgrenzen (`SYSn`) sind
|
||||
ebenfalls `DerivedTro`.
|
||||
|
||||
**Was passiert darin:**
|
||||
|
||||
- `fb_block` und `items` sind **berechnete** Felder: bei jedem Zugriff aus dem
|
||||
Katalog gelesen (`definition()` → `get_tro(type_name)`), nie gespeichert. In der
|
||||
JSON stehen sie trotzdem (`fbType`, `items`), damit sie lesbar ist.
|
||||
- `_check_catalog_fields` (Validator vor dem Einlesen): steht in einer JSON ein
|
||||
`items`/`fbType`, das nicht zum Typ passt, ist das ein Fehler — "den Typ ändern,
|
||||
nicht die Bauteile". Danach werden die Felder verworfen.
|
||||
- Aliase: in der JSON `id`/`type`, im Code `tro_id`/`type_name`
|
||||
(`populate_by_name`).
|
||||
- `_Positioned` (Mixin) wandelt `position` zwischen Tupel und
|
||||
`{"x","y","z"}` und liefert `x`/`y`/`z` und `describe_position()`.
|
||||
- `DerivedTro.connected`: hat der TRO überhaupt Nachbarn?
|
||||
|
||||
**Wer nutzt sie:** `tro_flow.derive_tros`/`derive_boundaries` erzeugen sie,
|
||||
`scl_context` liest sie (als `TroBase`), `tro_annotate` zeichnet sie.
|
||||
|
||||
### `Analysis` — `lib/model.py`
|
||||
|
||||
**Wozu:** Das Arbeitsergebnis eines Ableitungslaufs, bevor daraus eine Datei wird.
|
||||
Enthält mehr als die JSON: Kanten als Tupel, Kantenarten, isolierte TROs,
|
||||
unbekannte Typen, Befunde, die verwendete Methode.
|
||||
|
||||
**Was passiert darin:** Fast nur Daten; `by_id()` sucht über TROs und
|
||||
Systemgrenzen. Befüllt wird sie in `tro_flow.analyse_tros` in fester
|
||||
Reihenfolge: `derive_tros` → `derive_boundaries` → `locate_tros` (Position,
|
||||
nötig für die Reihenfolge auf einer Kreiselbahn) → `link_tros` (Kanten und
|
||||
Kantenarten) → Prüfung gegen den Katalog.
|
||||
|
||||
**Wer nutzt sie:** `tro_flow` (Bericht, Diagramm, `--json`) und `tro_annotate`
|
||||
(zeichnet direkt aus der `Analysis`, ohne Umweg über die JSON).
|
||||
|
||||
### `TroDocument` — `lib/model.py`
|
||||
|
||||
**Wozu:** Die TRO-JSON `<csv>_tro.json` (Schema `sps_skel-tro-v4`) — die
|
||||
Schnittstelle zwischen Ableitung (`tro_flow --json`) und SCL-Ausgabe
|
||||
(`scl_skeleton`). Weil beide Seiten dieselbe Klasse benutzen, kann das Format
|
||||
nicht unbemerkt auseinanderlaufen.
|
||||
|
||||
**Was passiert darin:** `from_analysis()` macht aus einer `Analysis` ein Dokument
|
||||
(Kanten + Kantenarten → `Connection`-Liste, je TRO die `slots` aus
|
||||
`derive_slots()`, die Staubereiche aus `derive_jam_areas()`). `to_json()` schreibt mit den
|
||||
JSON-Aliasen, `from_json()` liest und prüft. `extra="forbid"` sorgt dafür, dass ein
|
||||
umbenannter oder falscher Schlüssel als Fehler auffällt statt still einen
|
||||
Standardwert zu bekommen; `scl_skeleton.main` meldet bei falschem `schema`
|
||||
gezielt "mit tro_flow neu erzeugen".
|
||||
|
||||
**Wer nutzt sie:** geschrieben von `tro_flow --json`, gelesen von
|
||||
`scl_skeleton.load_document` → `scl_context.build_context`.
|
||||
|
||||
### `Element`, `Graph`, `TrackGraph` — `lib/material_flow.py`
|
||||
|
||||
**Wozu:** Das Rohmodell aus dem CSV-Export (ILS 2.0), noch ohne TRO-Begriff.
|
||||
|
||||
- `Element` = eine CSV-Zeile: Art, ID, Name, Nachbarn, `track_ids`,
|
||||
Merkmale, Position, Bounding-Box, Anschlusspunkte K1..K4.
|
||||
- `Graph` (`Node`/`Edge`) = grober **Materialfluss-Graph**: Transportobjekte
|
||||
(Strecke, Gefällestrecke, Kreisel-Bahn) als Knoten, Geräte (Separatoren,
|
||||
Weichen, Scanner …) an die Knoten gehängt (`Node.devices`), Stationen und
|
||||
Übergaben als Kanten.
|
||||
- `TrackGraph` (`TrackNode`/`TrackEdge`) = feiner **Bahnzusammenhang**: jedes
|
||||
Bahnelement (AS, SEP, ES, Weiche, BTMT …) ist ein Knoten, verkettet in
|
||||
Bau- bzw. Drehrichtung (siehe `doc/track.md`).
|
||||
|
||||
**Was passiert darin:** `Element` hat viele abgeleitete Properties: Klassifizierung
|
||||
(`is_transport`, `is_circle`, `is_device`, `is_station`, `is_transfer`,
|
||||
`is_entry`/`is_exit`), Geometrie (`x/y/z`, `bbox_x/y`, `height`,
|
||||
`height_from/to`, `rise`), Antrieb (`drive_dir`, `rotation`). Die Graphklassen
|
||||
selbst sind reine Container; die Logik steckt in den Funktionen
|
||||
`build_graph()` (Knoten bilden, Geräte zuordnen, Kanten sammeln, Fliessrichtung
|
||||
aus Gefälle/Antrieb, Stationen und Übergaben einhängen) und
|
||||
`build_track_graph()` (Ketten aus der Spalte `TrackIds`; ein Element in zwei
|
||||
Containern verbindet diese automatisch). `TrackNode` kann ohne `element`
|
||||
existieren (Platzhalter für eine nicht auflösbare Bahn, generisches
|
||||
Ein-/Ausgang-Symbol).
|
||||
|
||||
**Wer nutzt sie:** `material_flow` selbst (DOT/SVG/connect.ini/Bericht) und
|
||||
`tro_flow.analyse_tros(graph, elements)` als Eingang der TRO-Ableitung;
|
||||
`tro_annotate` für Ankerpunkte des Lagebezugs.
|
||||
|
||||
### `Overrides` (+ `TroOverride`, `SplitSpec`, `MergeSpec`) — `lib/tro_overrides.py`
|
||||
|
||||
**Wozu:** Von Hand gepflegte, vor Ort bestätigte Korrekturen aus
|
||||
`cfg/tro_overrides.ini` für das, was die Ableitung nicht erkennen kann.
|
||||
|
||||
- `TroOverride`: Typ eines TRO korrigieren. Adressiert über `tro_id` **oder**
|
||||
— stabiler — über einen `separator`, weil TRO-Nummern sich zwischen Läufen
|
||||
verschieben, Separatornummern aus dem Export nicht (`matches()`).
|
||||
- `SplitSpec`: Separatoren/Scanner aus einem TRO herauslösen und einen neuen TRO
|
||||
bilden (Übergabestelle, deren Zuordnung auf die Strecke statt auf die
|
||||
Kreiselbahn zeigt).
|
||||
- `MergeSpec`: mehrere TROs zu einem Steuerobjekt zusammenfassen (z. B. zwei
|
||||
Sperren beiderseits eines Kreisel-Berührpunkts).
|
||||
- `Overrides`: Sammelcontainer je CSV-Datei, dazu gesetzte Topologie
|
||||
(`connections`, ersetzt die abgeleiteten Kanten vollständig), erwartete
|
||||
TROs (`candidates`), Protokoll (`applied`) und `warnings`. `bool(overrides)`
|
||||
ist True, sobald irgendeine Korrektur vorhanden ist.
|
||||
|
||||
**Was passiert darin:** `load_overrides(csv_name)` liest den INI-Abschnitt der
|
||||
CSV-Datei und baut daraus die Objekte; Lesefehler landen in `warnings` statt
|
||||
den Lauf abzubrechen.
|
||||
|
||||
**Wer nutzt sie:** Stand heute **nur** `material_flow --connect`
|
||||
(`render_connect_ini` schreibt sie als `[<Anlage>.manual]`-Abschnitt). `tro_flow`
|
||||
wendet sie (noch) nicht an — sie fliessen also nicht in die TRO-JSON und nicht
|
||||
in die SCL.
|
||||
|
||||
### `Transform` (+ `RegistrationError`) — `lib/dxf_registration.py`
|
||||
|
||||
**Wozu:** Der Lagebezug zwischen CSV-Export und BricsCAD-Zeichnung: eine starre
|
||||
Transformation `csv = R(rotation) · dxf + (dx, dy)`. Ohne sie landen die
|
||||
TRO-Marker in `tro_annotate` an der falschen Stelle.
|
||||
|
||||
**Was passiert darin:** `to_csv()`/`to_dxf()` rechnen Punkte hin und zurück,
|
||||
`describe()` fasst Verschiebung, Drehung, Punktzahl und Restfehler zusammen.
|
||||
Die Ermittlung steckt in Funktionen: `derive_transform()` ordnet punktartige
|
||||
Objekte gleicher Rolle (Separator, Scanner) aus CSV und DXF zu, stimmt über
|
||||
Verschiebungen ab, gleicht aus und wählt den besten Kandidaten;
|
||||
`verify_transform()` rechnet die Restfehler nach und schreibt sie ins Objekt;
|
||||
`load_transform()`/`save_transform()` lesen/schreiben
|
||||
`cfg/dxf_registration.json`. `RegistrationError` heisst: der Bezug liess sich
|
||||
nicht **nachweisbar** bestimmen — dann lieber abbrechen als falsch zeichnen.
|
||||
|
||||
**Wer nutzt sie:** `tro_annotate.resolve_transform` (Reihenfolge: `--offset` →
|
||||
abgelegter Bezug → neu ermitteln, jeweils mit Prüfung), dazu das geparkte
|
||||
`tro_extract`.
|
||||
|
||||
### `SclFile`, `SclTro`, `SclName` — `lib/scl_context.py`
|
||||
|
||||
**Wozu:** Der **Render-Kontext** für das FB_Main-Gerüst. Alles, was aus der
|
||||
TRO-JSON *abgeleitet* wird, steht hier fertig drin; die Ausgabe
|
||||
(`emit_*`-Funktionen oder Jinja-Templates) rechnet nichts mehr selbst, sie
|
||||
liest nur noch. So können generischer Aufbau und Templates nicht
|
||||
unterschiedlich ableiten.
|
||||
|
||||
**Was passiert darin:**
|
||||
|
||||
- `build_context(document, source, start_id)` baut aus einem `TroDocument` ein
|
||||
`SclFile`: Kopfdaten plus die TROs in Fliessrichtung.
|
||||
- `flow_order()` bestimmt die Reihenfolge der REGIONs: Start bei `--start`, sonst
|
||||
beim TRO ohne Vorgänger, sonst beim TRO, den eine Station (`SYSn`) speist,
|
||||
sonst wird der Kreislauf willkürlich aufgetrennt — der Grund steht im
|
||||
SCL-Kopf (`start_reason`). Danach Breitensuche; nicht erreichte TROs werden
|
||||
hinten angehängt, damit keiner verloren geht.
|
||||
- `_tro_context()` baut je TRO ein `SclTro`: so viele Separator-, Weichen- und
|
||||
Scannerblöcke, wie der Katalogtyp Bauteile hat (`items`); je Vorgänger ein
|
||||
Staubereich am Eingang, je Nachfolger einer am Ausgang; `to1_destinations`
|
||||
nur bei genau einer Weiche (bei mehreren ist die Aufteilung eine
|
||||
Planungsentscheidung); die Zielliste je Weichenabgang über
|
||||
`reachable_via()` (Erreichbarkeit im TRO-Graphen, ohne über die Weiche
|
||||
zurückzulaufen).
|
||||
- `SclName` = Wert + Merker `todo`. Alles, was aus der E-Planung kommen muss
|
||||
(Separator-/Weichennummern, Sensor-/Aktoradressen, MainTro, Staubereichs-DB …),
|
||||
ist heute ein Platzhalter mit `todo=True`; die Ausgabe hängt daran den
|
||||
Kommentar `TODO(E-Planung)`. Spätere Namensregeln ersetzen nur den Wert und
|
||||
löschen den Merker — die Ausgabe ändert sich dafür nicht.
|
||||
|
||||
**Wer nutzt sie:** erzeugt in `scl_skeleton.render()`/`main()`, gelesen von
|
||||
`render_context()`, den `emit_*`-Funktionen und den Templates (als `t`).
|
||||
|
||||
### `SclTemplates` — `lib/scl_skeleton.py`
|
||||
|
||||
**Wozu:** Die Anbindung der parametrisierten SCL-Aufrufstellen
|
||||
(`lib/scl_templates/*.scl.j2`) an die Ausgabe. Ein Objekt steht für einen
|
||||
Template-Ordner.
|
||||
|
||||
**Was passiert darin:** Der Konstruktor baut ein `jinja2.Environment` mit
|
||||
`StrictUndefined` (ein Tippfehler im Template wird zum Fehler statt zu einer
|
||||
leeren SCL-Stelle), `trim_blocks`/`lstrip_blocks` und der globalen Variable
|
||||
`TODO`. `has(fb)` prüft, ob `<fb>.scl.j2` existiert; `render(tro)` rendert das
|
||||
Template mit `t = SclTro` und fasst danach mehrfache Leerzeilen zusammen (statt
|
||||
das mit `{%- -%}` im Template zu tun, was die Einrückung bricht).
|
||||
|
||||
#### Template ↔ TRO: Zuordnung und Einstieg in die Produktion
|
||||
|
||||
**Zuordnung — es gibt keine Tabelle, nur eine Namenskonvention:
|
||||
Dateiname = FB-Typ.** Die Kette:
|
||||
|
||||
```text
|
||||
DerivedTro.type_name "1Sep1Swi" (TRO-JSON, Feld "type")
|
||||
-> tro_catalog.get_tro() TroDefinition
|
||||
-> TroDefinition.fb_block "FB_ILS_MTRO_1Sep1Swi"
|
||||
-> TroBase.fb_block (berechnet, nicht gespeichert)
|
||||
-> SclTro.fb (scl_context._tro_context)
|
||||
-> SclTemplates.has/render lib/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl.j2
|
||||
```
|
||||
|
||||
Ein neues Template für einen weiteren Typ heisst also einfach
|
||||
`<fbType>.scl.j2` und wird automatisch für alle TROs dieses Typs verwendet.
|
||||
Heute gibt es nur den Piloten `FB_ILS_MTRO_1Sep1Swi.scl.j2` (plus
|
||||
`_macros.scl.j2` mit gemeinsamen Makros). Alle anderen Typen laufen über den
|
||||
generischen Aufbau (`emit_region()`).
|
||||
|
||||
**Einstieg — nur per Schalter (opt-in):**
|
||||
|
||||
- `scl_skeleton --templates [DIR]` (ohne DIR: `lib/scl_templates`). `main()`
|
||||
legt dann ein `SclTemplates` an; `render_context()` nimmt pro TRO
|
||||
`templates.render(tro)`, wenn `templates.has(tro.fb)`, sonst `emit_region()`.
|
||||
Der SCL-Kopf listet, welche FB-Typen aus Template bzw. generisch kamen.
|
||||
- `--strict` bricht ab (Exit 1, keine SCL), wenn ein FB-Typ der Anlage kein
|
||||
Template hat. Ein `TemplateError` (fehlende Variable, Syntaxfehler) bricht
|
||||
ebenfalls ab — lieber keine SCL als eine mit stillen Lücken.
|
||||
- Ohne `--templates` werden Templates gar nicht geladen; die Ausgabe ist die
|
||||
bisherige (Golden-Files `tests/fixtures/scl/*_FB_Main.scl`). Mit Templates
|
||||
pinnen `*_FB_Main_templates.scl` das Ergebnis.
|
||||
- `bin/scl_skeleton.bat`/`.sh` reichen die Schalter nur durch; der Standardaufruf
|
||||
ohne `--templates` nutzt also **keine** Templates.
|
||||
|
||||
**Nicht Produktion:** `doc/TRO_Katalog/scl_templates/*.scl` sind die realen
|
||||
Referenz-Aufrufstellen. Gegen sie prüft `tests/test_scl_templates.py` jedes
|
||||
Template im Round-Trip; in die erzeugte SCL fliessen sie nicht ein.
|
||||
|
||||
## Daten
|
||||
|
||||
Bestandsaufnahme aller Daten, die in den Referenz-Aufrufstellen und FB-Schnittstellen
|
||||
unter `doc/TRO_Katalog/scl_templates/*.scl` vorkommen (Stand 2026-10-10), und woher
|
||||
der Generator sie bekommen muss. Grundlage: die sieben Aufrufstellen
|
||||
(`FB_ILS_MTRO_1Sep`, `_1Sep1Swi`, `_1Sep2Swi`, `_1Sep_SSCC`, `_PinStore_Auto`,
|
||||
`_Vario`, `_Vario_workStation_CallSite`) und die `VAR_INPUT/OUTPUT/IN_OUT`-Abschnitte der
|
||||
vier vollständig vorliegenden FBs (`FB_ILS_MTRO_2Sep1Swi`, `FB_ILS_MTRO_Vario_workStation`,
|
||||
`FB_EmptyCarrBuffer`, `FB_LoadingBoom_INBOUND`). Die Beispielwerte stammen aus diesen
|
||||
Dateien.
|
||||
|
||||
**Quellen** (Spalte "Quelle"):
|
||||
|
||||
| Kürzel | Quelle | Was sie liefert |
|
||||
|---|---|---|
|
||||
| **L** | Anlagen-Hardware = mechanisches Layout (CSV-Export ILS 2.0, `data/*.csv`) | Bauteile (`TeileArt`: Separator, Scanner, Weiche, Kreisel, Strecke, Gefällestrecke, BTMT Be-/Entladung, Ein-/Ausschleuselement), `Nachbarn`/`TrackIds`, `Merkmale` (Drehrichtung, Antriebfahrtrichtung, Höhen, Längen, Anzahl Separatoren/Scanner, Zuordnung). Daraus abgeleitet: TRO-Typ, Topologie, Reihenfolge, Ziele. |
|
||||
| **E** | Hardwareplanung = Elektroplanung (EA-Liste, TIA-Import, WSCAD-Import, `*_positionsdraw.json`; siehe `doc/HundM/EA-Listen-Analyse.md`) | Betriebsmittelkennzeichen und Variablennamen (`BG…` Sensoren, `MB…` Ventile, `MA…` Motoren, `FC…` Motorschutz, `SF…` Taster, `BX…` Scanner), Hardware-Kennungen, welche optionalen Sensoren verbaut sind, Zuordnung zu Schaltschrank und Not-Halt-Kreis. |
|
||||
| **K** | Namenskonvention | Name folgt aus TRO-Nummer bzw. Staubereichs-/Weichennummer (`cMainTro<n>`, `cSep<n>.<i>`, `"DB_Interface_HMI".stTRO.TRO<n>` …). Keine externe Quelle nötig, sobald Nummernkreis und Regel feststehen. |
|
||||
| **P** | Parameter / Projektstandard | Weder Layout noch E-Planung: Zeiten, Schwellwerte, Projektkonstanten, WCS-/Inbetriebnahme-Logik. Kandidaten für Typ-Defaults (`doc/TRO_Typen.md`). |
|
||||
|
||||
`L + E` heisst: das Layout sagt, **dass/wie viele** es gibt und **wo**, die
|
||||
E-Planung liefert den **Namen/die Adresse**. Das ist der häufigste Fall.
|
||||
|
||||
Die zwei folgenden Abschnitte listen **alle Angaben**, die die beiden externen
|
||||
Quellen heute enthalten (Stand 2026-10-11, geprüft an `data/*.csv` und an den
|
||||
Exporten unter `kabellaengen/work/`: `easy_sps-1_EA/_TIA/_WSCAD.xlsx`,
|
||||
`Mubea_positionsdraw.json`). Was sich daraus für TROs und SCL gewinnen lässt und
|
||||
zwei kommentierte SCL-Beispiele: `doc/Datenquellen_Layout_Hardwareplanung.md`.
|
||||
|
||||
### Angaben 1: Anlagen-Hardware (mechanisches Layout, CSV)
|
||||
|
||||
Eine Zeile je Bauteil, Trennzeichen `;`, `utf-8-sig`. Gelesen von
|
||||
`material_flow.load_elements()`.
|
||||
|
||||
#### Spalten (alle Bauteilarten)
|
||||
|
||||
| Spalte | Beispiel | Bedeutung | Nutzen für TRO/SCL |
|
||||
|---|---|---|---|
|
||||
| `Elementnummer` | `1` | laufende Zeilennummer des Exports | keiner (nicht stabil) |
|
||||
| `TeileArt` | `ILS 2.0 Separator` | Bauteilart (Liste unten) | Knotenart, TRO-Typ |
|
||||
| `TeileId` | `0008` | stabile ID im Layout | Schlüssel für Topologie, Overrides, E-Brücke |
|
||||
| `Bezeichnung` | `VF_1`, `Kreisel :3` | Klartext aus der Zeichnung | Anzeige, REGION-Kommentar |
|
||||
| `Planquadrat` | `B/1-C/4` | Raster der Zeichnung | Bericht/Suche |
|
||||
| `Anzahl` | `1` | Stückzahl | keiner |
|
||||
| `Position` | `5878.63, 4330.96, 2128.70` | Einfügepunkt x, y, z in mm | Lagebezug zur E-Planung (`dxf_registration`), Abstände |
|
||||
| `Boundingbox` | `2837.82, 6404.92, 731.58` | Ausdehnung x, y, z in mm | Lage von Weichen/Bahnen |
|
||||
| `Insertpoint` | `TV0UEkxg…` | kodierter Einfügepunkt (Base64) | intern |
|
||||
| `K1`–`K4` | `AK41AC7XAFQk` | Anschluss-Koordinatensysteme (Base64, je x/y/z) | Anschlussseite, Lage der AS/ES (`decode_kpoint`) |
|
||||
| `Nachbarn` | `0006-L, 0014` | berührende Bauteile, bei Kreiseln mit Bahn `-L`/`-R` | Topologie (Kanten) |
|
||||
| `TrackIds` | `0044, 0027, 0026` | Anbauteile eines Containers in Baureihenfolge bzw. Umlaufrichtung | Reihenfolge Separator/AS/ES auf Strecke und Kreisel |
|
||||
| `Fehler`, `Warnungen` | `nur ein Partner`, `unverbunden` | Prüfmeldungen des Exports | Qualitätsprüfung |
|
||||
| `Merkmale` | JSON | artspezifische Eigenschaften (unten) | siehe unten |
|
||||
|
||||
#### Merkmale je `TeileArt`
|
||||
|
||||
| TeileArt | Merkmale | Nutzen für TRO/SCL |
|
||||
|---|---|---|
|
||||
| `ILS 2.0 Kreisel` | `Name`, `Kreiselart` (`STANDARD`), `Drehrichtung` (`UZS`/`GUZ`), `Drehung`, `Höhe in m`, `Abstand (Kreiselachse A - Kreiselachse) in Meter`, `Anzahl der Separatoren`, `Anzahl der Scanner`, `Anzahl der Rampen`, `Geruest fuer Einzelmodul` | Host eines TRO (`xInCarouselRun1/2`), Umlaufrichtung → Reihenfolge, Länge → Staulänge; Anzahlen zur Plausibilisierung |
|
||||
| `ILS 2.0 Strecke` | `Typ` (`Streckengruppe`), `Seite_AS`, `Seite_ES`, `Winkel`, `VF_Winkel`, `DeltaH_mm`, `DeltaL_mm`, `L_VF_m` (angetriebene Teilstücke), `L_GF_m` (Gefälleteilstücke), `Montagehoehe_m`, `Hoehe_Von_mm`, `Hoehe_Bis_mm`, `Antriebfahrtrichtung` (`horizontal`/`Auf`), `Anzahl_Separator`, `Anzahl_Scanner`, `Separatorlist` | angetrieben (`L_VF_m`, `Auf`) → Vario/Antrieb; Längen → `tConvEmpty`, Staulänge; Anzahlen zur Plausibilisierung |
|
||||
| `ILS 2.0 Gefaellestrecke` | wie Strecke, `L_VF_m` und `Antriebfahrtrichtung` leer | Host "Gefälle" → `1Sep`, `xInCarouselRun1 := TRUE` |
|
||||
| `ILS 2.0 Separator` | `Zuordnung` (Host, ggf. mit Bahn `-L`/`-R`), `ARTINR`, `Höhe`, `Drehung`, `Typ` | **TRO-Anker**: jeder Separator ist ein TRO oder Teil davon; Host → TRO-Typ |
|
||||
| `ILS 2.0 Scanner` | `Zuordnung`, `Naechster Separator`, `ARTINR`, `Höhe`, `Drehung`, `Typ` | Scanner am TRO (`xPluggedScanner`, `stInBarcodeReader`, REGION-Titel) |
|
||||
| `ILS Weiche` | `Kreisel A`, `Kreisel B`, `Achse von Kreisel`, `Beruehrung` (`Kreis/Kreis`), `Erzeugt aus` | Kreisel-Kreisel-Weiche → `1SepNSwi`, `xCar2NecRight/Left`, zweiter Kreisel `xInCarouselRun2` |
|
||||
| `ILS Ausschleuselement` | `Kreisel`, `Strecke`/`Förderer`, `Streckenende` (`Anfang`), `Element` (`AS_Element_90_links_3D`), `ARTINR`, `Erzeugt aus` | Abgang Kreisel → Strecke: Weichenausgang `Exit3/4`, Ziel je Abgang |
|
||||
| `ILS Einschleuselement` | wie Ausschleuselement, `Streckenende` `Ende` | Zusammenführung Strecke → Kreisel: Vorfahrt (`xPrioDir`, PriorityManager) |
|
||||
| `ILS 2.0 BTMT Beladung` / `Entladung` | `ARTINR`, `Höhe`, `Drehung` | Systemgrenze `SYSn`, `xIsBorner`, Ziele; Ladeschleife |
|
||||
| `Eingang` / `Ausgang` | `ARTINR`, `Höhe`, `Drehung` | Systemgrenze zu Nachbaranlage, Übergabe-Staubereich |
|
||||
|
||||
`ARTINR` (Artikelnummer) unterscheidet Bauarten, z. B. Separator `200000077`,
|
||||
Scanner `829534306`; daran ließe sich die Weichenbauart (`xNewSwitch`) prüfen.
|
||||
|
||||
Zusätzlich gibt es den **Sivas-Stücklisten-Export** (`data/*_sivas.csv`:
|
||||
`SivasNummer`, `Dispogruppe`, `Laenge[mm]`, `IDs`, `details` mit
|
||||
Förder-/Gefällegruppen, Kurven, `MotorSeite`). Er ist eine Stückliste, keine
|
||||
Topologie, und wird nicht gelesen; brauchbar wäre `MotorSeite` und die
|
||||
Gruppenlängen.
|
||||
|
||||
**Nicht im Layout:** Betriebsmittelkennzeichen, E/A-Adressen, welche optionalen
|
||||
Sensoren verbaut sind, Schaltschrank/Steuerung, Zeiten, Ziel-Nummern des WCS.
|
||||
|
||||
### Angaben 2: Hardwareplanung (EA-Liste, TIA-Import, WSCAD-Import, positionsdraw)
|
||||
|
||||
Vier Exporte derselben E-Planung. Gemeinsamer Schlüssel ist das
|
||||
**Betriebsmittelkennzeichen (BMK)** `BG3306`, `MB3306` …; nur `positionsdraw`
|
||||
kennt eine Lage, nur EA/TIA/WSCAD kennen die E/A-Adresse.
|
||||
|
||||
**`<projekt>-<n>_EA.xlsx`** — Blatt "Ein-, Ausgaenge", je SPS `n` eine Datei
|
||||
|
||||
| Spalte | Beispiel | Bedeutung |
|
||||
|---|---|---|
|
||||
| `Name` | `E324.0` | E/A-Adresse |
|
||||
| `Kommentar` | `Stausensor 1 (ILS-CV M0108)` | Funktionsrolle, lfd. Nr., Fördereinheit (M-Nummer), Bereich |
|
||||
| `Zeile` | `2` | Verweis Schaltplan |
|
||||
|
||||
**`<projekt>-<n>_TIA.xlsx`** — Importdatei für das TIA Portal
|
||||
|
||||
| Blatt / Spalte | Beispiel | Bedeutung | Nutzen für SCL |
|
||||
|---|---|---|---|
|
||||
| PLC Tags `Name` | `BG3240`, `MB3306`, `MA0062`, `FC0062` | BMK = Variablenname | direkt die Namen in `FB_Main` (`=> "MB3306"`, `:= "FC0145"`) |
|
||||
| PLC Tags `Path` | `Import` | Variablentabelle | Ordner der Variablentabelle |
|
||||
| PLC Tags `Data Type` | `Bool` | Datentyp (nur Bool) | – |
|
||||
| PLC Tags `Logical Address` | `%E324.0`, `%A006.2` | Absolutadresse | Prüfung, `FB_CallSensors` |
|
||||
| PLC Tags `Comment` | `Stausensor 1 (ILS-CV M0108)` | wie EA-Kommentar | Rolle, M-Nummer |
|
||||
| PLC Tags `Hmi Visible/Accessible/Writeable` | `True` | HMI-Flags | – |
|
||||
| PLC Tags `Typeobject ID`, `Version ID` | leer | – | – |
|
||||
| Constants `Name` | `cInBG3240`, `cInFC0062`, `cOutMA0062` | Index-Konstante je Signal | genau die Indizes in `"DB_Inputs".Sensors["cInBG3306"]` |
|
||||
| Constants `Data Type`, `Value` | `Int`, `1` | Array-Index | Dimensionierung `DB_Inputs` |
|
||||
|
||||
**`<projekt>-<n>_WSCAD.xlsx`** — Importdatei für das E-CAD
|
||||
|
||||
| Spalte | Beispiel | Bedeutung | Nutzen für SCL |
|
||||
|---|---|---|---|
|
||||
| `Anschluss` | `E324.0` | E/A-Adresse | Schlüssel zu EA/TIA |
|
||||
| `Kommentar` | wie EA | | |
|
||||
| `Bezug` | `=A01+UC0101-KF1DI1` | Anlage, Feldstation (`UC…`), Baugruppe (`KF1DI1`, `KF1FDQ2` = F-Karte) | Steuerungs-/Schaltschrankzuordnung, Safety-Signale |
|
||||
| `Symboltext`, `Sondertext 1..8` | meist leer | | |
|
||||
|
||||
**`<zeichnung>_positionsdraw.json`** — Kabellängen-Tool, E-Symbole aus der Zeichnung
|
||||
|
||||
| Schlüssel / Attribut | Beispiel | Bedeutung | Nutzen für TRO/SCL |
|
||||
|---|---|---|---|
|
||||
| `sensors{BMK@SPS}` | `BG0542@UNKNOWN` | Feldgeräte (BG, MB, MA …) | Hauptquelle der Brücke |
|
||||
| `.IO` / `.B` | `BG0542` | BMK | Schlüssel zu TIA |
|
||||
| `.pos` | `[-42369.8, -11437.2]` | Lage in mm | **Zuordnung zum Layout-Bauteil** (`e_planning.py`) |
|
||||
| `.VERW` | `IN SEP`, `SEP`, `STAU_REF-LT`, `AE ABZWEIG`, `ABZWEIG-K104`, `BT BELADEN`, `SYNC P1..4`, `ABFR.FINGER`, `ÜBERGABE`, `ÜBERNAHME`, `BANDBRUCH`, `KETTENBRUCH`, `ZAPPLER P1`, `MOT_0,37kW` | Verwendung (kontrolliertes Vokabular) | **Rolle** des Signals im TRO |
|
||||
| `.BEZEICHNUNG` | `STAUSENSOR STR. 01`, `PRÜFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)` | Klartext | Strecken-/Kreiselnummer, Plausibilisierung |
|
||||
| `.BESCHR` | `STAUSENSOR` | Gerätebeschreibung | – |
|
||||
| `.KENNZEICHNUNG` | `=A1+UH01-KF102` | Steuerung `UH01`, E/A-Station `KF102` | Steuerung, TRO-Nummernkreis, Schaltschrank |
|
||||
| `.ARTINR` | `829434300` | Artikelnummer Gerät | Gerätetyp |
|
||||
| `.SPS` | `''`, `A/B` | SPS-Nummer (oft leer) | Steuerung |
|
||||
| `.A`, `.C`, `.ID`, `.MENGE`, `.POSITION`, `.GRUPPE`, `.ETIKETTE`, `.AUFLOESE`, `.TEXT-D/E/ES/F` | | Blockattribute des Symbols | – |
|
||||
| `schaltschrank_elemente{…}` | `FC0200` `MOTORSCHUTZ CONVEYER #100`, `SF…` Taster, `PF…` Leuchten, `QA7000` Hauptschütze, `GF` Encoder, `BP` Druckschalter | Elemente im Schrank, ohne Feldlage | Motorschutz ↔ Motor, Bedienelemente, Encoder |
|
||||
| `mappings` | `UH01: [BMK …]` bzw. Verteiler → BMK | Zuordnung zur Steuerung/Verteiler aus der Verkabelung | Steuerungsaufteilung |
|
||||
| `distributors`, `racks`, `tunnels` | | Verteiler, Trassen | – |
|
||||
| `warnings`, `not_found`, `double_ids` | | Prüfprotokoll | Qualitätsprüfung |
|
||||
|
||||
**BMK-Präfixe:** `BG` Sensor, `SF` Taster/Schalter, `DI` Diagnose, `BP`
|
||||
Druckschalter, `GF` Encoder, `BX` Scanner (nur PROFINET, nicht in den Digitallisten),
|
||||
`MB` Ventil (Stopper, Weiche, Ausschleuser), `MA`/`QA` Motor/Schütz, `DQ`
|
||||
Sonderausgang, `FC` Motorschutz, `PF` Leuchtmelder.
|
||||
|
||||
**Nicht in der Hardwareplanung:** Topologie (wer auf wen folgt), TRO-Grenzen,
|
||||
Ziele/Weichenrichtungen, Zeiten, Analogwerte und PROFINET-Geräte (Cognex-Variablen
|
||||
stehen in der TIA-Gerätekonfiguration, nicht im Tag-Import).
|
||||
|
||||
### TRO-Kopf und Instanzaufruf
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Existenz, Anzahl, Reihenfolge der TROs | `REGION TRO 100 …` | L | `tro_flow` → `DerivedTro`; REGION-Reihenfolge `scl_context.flow_order()` |
|
||||
| TRO-Typ / FB-Typ | `FB_ILS_MTRO_1Sep1Swi` | L (→ Katalog) | Typ aus Separator-Host (`doc/500573_Mubea/TRO_Identifikation_500573.md`), FB aus `tro_catalog` |
|
||||
| TRO-Nummer, Instanzname | `#TRO107` | L + E | Anzahl aus L; Nummernkreis je Steuerung (100er = UH01, 200er = UH02 …) ist Projektvorgabe |
|
||||
| REGION-Titel | `TRO 267 (1Sep2Switch)+Scanner(BX2100)` | L + E | Typ aus L, Scanner-BMK aus E |
|
||||
| `nInMainTroNo` | `"cMainTro100"` | K | Wert der Konstante in der PLC-Konstantentabelle |
|
||||
| `nOutStateLast` | `"DB_GLOB_TroState".arTroState["cMainTro100"]` | K | |
|
||||
| `stInOutHMI` | `"DB_Interface_HMI".stTRO.TRO100` | K | PinStore/Puffer: eigener UDT `UDT_HMI_TRO_PinStore` |
|
||||
| `xInRelease` | `"DB_TRO_TEST".TRO100`, `TRUE`, WCS-Verbindung aktiv | P | Freigabe-/Testlogik, Inbetriebnahme |
|
||||
| `xInAllRdyToStart`, `xInCancel`, `xInReset` | `TRUE`, `"cTodoFalse"` | P | meist Konstante |
|
||||
| `stInSettings.xIsBorner` (`xIsBorner1/2`) | `TRUE` | L | erster TRO nach Einfahrt in die Anlage (Beladestation / Systemgrenze `SYSn`) |
|
||||
| `stInSettings.nBornerDest(1/2)` | – | P | Standardziel für neu angelegte Carrier (WCS) |
|
||||
| `stInSettings.bSepType` (`bSep1Type/2Type`) | `.%X2 := TRUE` | E + P | X0 erstes Bein halten, X1 Beladekontrolle, X2 Beladeerfassung — hängt an verbauter Sensorik und Prozess |
|
||||
| `stInSettings.nTroStopper1/2`, `nTroSwitchExit2/3` | – | L + K | Nummern der Nachbar-TROs |
|
||||
| `stInSettings.sJamSrc` / `sJamDest` | – | L + K | Namen der benachbarten Staubereiche |
|
||||
| `stInSettings.xCar2NecRight/Left` | – | L | zweiter Kreisel nötig für Richtung rechts/links (Topologie an einer Kreisel-Weiche) |
|
||||
| `tPartSenHigh`, `tInSenHigh`, `tDummyTimeNormal`, `tDummyTimeBorner` | FB-Defaults 500 ms / 3 s / 0 ms | P | Typ-Defaults |
|
||||
|
||||
### Separator (`stInSeparator<n>`, `xOutStopper<n>`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Separatoren je TRO | `stInSeparator1`, `stInSeparator2` | L | `ILS 2.0 Separator` + `Zuordnung`, Typ-Katalog `items` |
|
||||
| `Settings.nSeparatorNo` | `"cSep107.1"` | K | Layout-Separator-ID (`TeileId`) ist **nicht** diese Nummer |
|
||||
| Sensor im Stopper `stSenInSep` | `"DB_Inputs".Sensors["cInBG3242"]` | E | |
|
||||
| Sensor nach Stopper `stSenSepLeft`, Beladesensor `stSenPart`, Scan-Start `stSenStartScan` | `["cInBG4465"]` | E | nur wenn verbaut |
|
||||
| Stopperventil `xOutStopper<n>` / `xOutStopper` | `=> "MB3242"` | E | BG- und MB-Nummer sind in der E-Planung meist gleich (BG3242/MB3242) |
|
||||
| `Settings.xPluggedJam` | `TRUE` | E | Stausensor verbaut? |
|
||||
| `Settings.xPluggedPart` | `FALSE` | E | Beladesensor verbaut? |
|
||||
| `Settings.xPluggedConfirm`, `xPluggedNullBT` | `FALSE` | E | |
|
||||
| `Settings.xPluggedScanner` | `TRUE` | L | Scanner am Separator (`DerivedTro.scanners`) |
|
||||
| `Settings.tDelayToNextItem` | `"TimeForSepWaiting_Short"` / `_Medium` | P (+ L) | Wahl Short/Medium hängt am Host (Kreisel/Strecke/Gefälle) |
|
||||
| `Settings.tTrailingTime`, `tHandlingTime` | `t#600ms`, `t#200ms`, `"cTimeSepTrailing"` | P | Typ-Default, IBN |
|
||||
| `Settings.tJam` | `"TimeForSepJam"` | P | Projektkonstante |
|
||||
| `Settings.tSenFree`, `tSenWait` | `T#1500ms`, `T#4000ms` | P | |
|
||||
|
||||
### Staubereiche (Jam)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Ein-/Ausgänge und Zuordnung Exit-Nr ↔ Nachbar | `arInOutJamEntr1`, `arInOutJamExit2..4` | L | ein Eingang je eigenem Staubereich (`TroDocument.jamAreas`, Kennung `J<Separator>`; mehrere Vorgänger fahren in denselben ein), je Nachfolger/Weichenabgang ein Ausgang (`SclJam`) |
|
||||
| Gleicher Staubereich auf beiden Seiten einer Kante | Exit von A = Entr von B | L | Identität aus der Topologie |
|
||||
| Staubereichs-DB und -Index | `"DB_JamArea0102".stJam0102_1.arCarrier` / `.stData` | K (+ E) | Nummerierung der Staubereiche ist Projektvorgabe |
|
||||
| Stausensor je Ausgang | `stInSenJam2 := "DB_Inputs".Sensors["cInBG3243"]` | E | |
|
||||
| Übergabe-Staubereich an Steuerungsgrenze | `#arCarrierHandoverJam5001`, `#stJamDataHandoverJam5001` | L + E | Steuerungsaufteilung aus der E-Planung |
|
||||
| `xInJamAtExit2/3` | – | P | NIU |
|
||||
|
||||
### Weiche (`stInSwitch<n>`, `FC_Direction`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Weichen je TRO | `stInSwitch1`, `stInSwitch2` | L | `ILS Weiche`, Ausschleuselement, Typ-Katalog |
|
||||
| `Settings.nSwitchNo` | `"cSwi107.1"` | K | |
|
||||
| `Settings.nTo1Destinations` | `3` | L | Erreichbarkeit im TRO-Graphen (`reachable_via()`); bei mehreren Weichen Planungsentscheidung |
|
||||
| Zielliste je Abgang | `cDest…` | L + P | welche Ziele: L; Zielnummern: Projekt/WCS |
|
||||
| `Settings.xPluggedExSen3/4`, `stSenExit3/4` | `FALSE` | E | Abgangssensoren verbaut? |
|
||||
| `Settings.xNewSwitch` | `TRUE` | E | Weichenbauart (ggf. über `ARTINR` im CSV prüfbar) |
|
||||
| Weichenventile `xOutSw<n>ExTo3/4`, `xOutSw1EntrTo1/2` | `=> "MB3304"` | E | |
|
||||
| `FC_Direction`-Aufruf | `nInSwitchNo := "cSwi107.1"`, `InOutCarrier := …stJam0102_1.arCarrier[1]` | K + L | Carrier aus dem Eingangs-Staubereich |
|
||||
| Vorfahrt `stInSettings.xPrioDir<n>`, `stInPriorityManagerDir<n>` | `"DB_Prozessdaten".stPriorityManager.udt_TRO107.TroIn` | L + K | Vorfahrt nötig, wo zwei Ströme zusammenlaufen (Kreuzung/Zusammenführung) |
|
||||
| `stInPriorityManager.xReleaseOk` | `TRUE` ("braucht keine Vorfahrtsregelung") | L | TRUE, wenn keine Zusammenführung |
|
||||
|
||||
### Scanner / Barcode / SSCC
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Scanner vorhanden, Anzahl | `stInBarcodeReader`, `stInBarcodeReader1/2` | L | `ILS 2.0 Scanner`, `Anzahl_Scanner` |
|
||||
| Scanner-BMK und Index | `#arCognexInterface["cScannerBX2100"]` | E | |
|
||||
| Cognex-Variablen | `"bx2100-TriggerReady"`, `-TriggerAck`, `-Results Available`, `-General Fault`, `-TriggerEnable`, `-Trigger`, `-ResultsAck` | E | aus dem BMK nach Regel bildbar, wenn die TIA-Variablentabelle dem Muster folgt |
|
||||
| Hardware-Kennung Ergebnisse | `"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1"` | E | |
|
||||
| Kein Scanner | `#stCognexInterfaceDummy` | L | |
|
||||
| `Settings.xActivateCheckASCI`, `tTimeout` | `TRUE`, `T#5s` | P | |
|
||||
| `Settings.nStation`, `nScannerPurpose` | `111`, `0` | P | WCS |
|
||||
| Zweck SSCC-Scanner, `xInPluggedSSCC` | `#stCognexIntfTro105` | E + P | Layout kennt den zweiten Scanner, nicht seinen Zweck |
|
||||
| `stSenEndMeasurement` | `["cInBG3281"]` | E | |
|
||||
| `FC_CARR_GLOB_CORRECTION` | `nInStation := 0`, Carrier aus Eingangs-Staubereich | P + L | |
|
||||
| WCS-Telegramm, HMI-SSCC-Ausgabe | `FC_Send_GOHDReqM`, `"DB_Interface_HMI".stSSCC.SSCC1107` | P | WCS-Schnittstelle |
|
||||
|
||||
### Vario (`stInVario`) und Arbeitsplatz
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Vario vorhanden | `FB_ILS_MTRO_Vario` | L | angetriebene Strecke / Variofoerderer |
|
||||
| Sensoren `stSenCarrInPos`, `stSenFinger`, `stSenLastPos`, `stSenJam`, `stSenNextSep`, `stSenSepVario` | `["cInBG3026"]` … | E | |
|
||||
| Motor `xOutVarioMotor`, Motorschutz `xInMotorProtection` | `"MA0145"`, `"FC0145"` | E | |
|
||||
| Finger-Staubereich `arInOutJamFinger` | `"DB_JamArea4103".stJam4103_1` | L + K | |
|
||||
| `Settings.tConvEmpty` | `T#45000ms` | L + P | aus Förderlänge (`L_VF_m`) und Geschwindigkeit ableitbar |
|
||||
| `Settings.nCountCarrier`, `tWaitForJog`, `tWaitForStop`, `tCarrierInPosDelay`, `tCarrierInPosEdgeDelay`, `tRasterHigh/Low`, `xJamSenDelayActive`, `tJamSenDelay`, `xContinousEmptying`, `SettingsVarioFingerMonitoring` | `1`, `T#300ms` … | P | |
|
||||
| Arbeitsplatz `stInStationSensor`, `xInReleaseStation`, `fbConvVario.xBeladenError` | `["cInBG3314"]`, `#"rTrig_Workstation 1".CLK` | E | Taster/Sensor am Arbeitsplatz |
|
||||
|
||||
### Antrieb, Sicherheit, Betriebsart
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Fördertechnik, die den TRO trägt | `xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed`; Gefälle: `TRUE` | L | Host-Kreisel aus `DerivedTro.hosts`; Instanzname nach K |
|
||||
| `xInCarouselRun2` | zweiter Kreisel | L | TRO an der Berührung zweier Kreisel |
|
||||
| `xInConveyorRunIn/Out` (PinStore) | `#fbSortConv1101`, `#fbExitConv1103` | L + K | |
|
||||
| `xInSftyOk` | `"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q` | E | Not-Halt-Kreis |
|
||||
| `stInOutMachineState` | `#stInOutControlUnitCabinetOF1` | E | Schaltschrank (OF1/OF2) |
|
||||
| `xInAutomatic`, `xInErrorQuit`, `xInQuitLocal` | `#t_Automatik_OF1 AND "SF0120"`, `"SF0002"`, `"SF0121" OR "SF0117"` | E | Bedienelemente |
|
||||
| `nInTestTarget` | `"IBN".nTestTarget111` | P | |
|
||||
|
||||
### Pin-Lager (`FB_ILS_MTRO_PinStore_Auto`) und Leerträgerpuffer (`FB_EmptyCarrBuffer`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Anzahl Lagerlinien | `nNoOfLines := 19`, `arInSep[1..5]` | L | Separatoren auf Kreisel mit Pinband / `Separatorlist` |
|
||||
| Positionen je Linie, Staulänge | `nNoOfPositions := 70`, `nJamLength := 4` / `"cStorage111ExitJam2"` | L + P | aus Linienlänge und Teilung |
|
||||
| Liniensensoren `arInSep[n].stSenTrolley`, `arInJamSensor[n]`, `stInSenJamExit` | `["cInBG3270"]` | E | |
|
||||
| Linienventile `arOutSeparatorStore[n]` | `"MB3270"` | E | |
|
||||
| Pin-Separator `stInSeparatorPin`, `xOutSeparatorPin` | `["cInBG3000"]`, `"MB3000"` | E | |
|
||||
| Referenzsensor "Zappler" `stInSensorReference` | `["cInBG3001"]` | E | |
|
||||
| Encoder `stInEncoderParameter` | `#fbEncoderGF2200.nOutPosActDif` | E | |
|
||||
| `nDestGeneral`, `nConvStations`, `nStationsMax`, `nStorageThreshold` | `"cDest11100"`, `"cHooksSorterMaxPin111"` | P | WCS/Projekt |
|
||||
| `nInBufferNr`, `xInBufferReq/Release/ReleaseNext`, `xInPause` | `101`/`102` | P | Pufferlogik |
|
||||
| Lager-/Stations-DBs | `"DB_Storage1.1.1".arLine`, `"DB_ConveyorStations"`, `"DB_HandshakeStorage"` | K + P | |
|
||||
| Linien-Zeiten, Simulation | `"cTimeSepHandlingStorageLine"`, `stSimu.*` | P | |
|
||||
|
||||
### Ladeschleife (`FB_LoadingBoom_INBOUND`)
|
||||
|
||||
| Daten | SCL-Stelle (Beispiel) | Quelle | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| Ladeschleife vorhanden, Lage | – | L | Beladestation (`ILS 2.0 BTMT Beladung`), prüfen |
|
||||
| Bedienelemente `stInControl.*` | Taster, Knebel-/Schlüsselschalter | E | |
|
||||
| Ausleger/Stützfuß `stInBoom.*`, `stInFoot.*` | Endlagen, Motorschutz, Bumper | E | |
|
||||
| Analogwerte `stInTilt.wTilt`, `stInDistance.wDistance` | Neigung, Abstand | E | |
|
||||
| Sensoren `stInSen2mThreshold`, `stInSenJamEntr/Exit1/Exit2`, `stInSenStopper1/2`, `stInSenGrid` | | E | |
|
||||
| Ausgänge `stOutStopper.*`, `stOutBoom.*`, `stOutFoot.*`, `xOutLedMovement`, `xOutLight`, `xOutLedBumper`, `xOutCarousel` | | E | |
|
||||
| Schwellwerte `nDiff`, `nZeroOk`, `nStartSlow/Fast`, `nDistanceZero`, `tDiff`, `tZero`, `nTiltComp` | | P | Inbetriebnahme |
|
||||
| `stInSettings.*` Zeiten | `tBoomInput := T#3500ms` … | P | |
|
||||
| Exit-Staubereich `arInOutCarrierExit`, `stInOutCarrierExit` | | L + K | |
|
||||
|
||||
### Zusammenfassung
|
||||
|
||||
- **Aus dem Hardwarelayout (L)** kommt die **Struktur**: welche TROs es gibt, ihr Typ
|
||||
und damit FB und Anzahl der Parameterblöcke (Separatoren, Weichen, Scanner, Vario,
|
||||
Lagerlinien), Vorgänger/Nachfolger und damit Ein-/Ausgangs-Staubereiche,
|
||||
REGION-Reihenfolge, Ziele je Weichenabgang (`nTo1Destinations`), Vorfahrt ja/nein,
|
||||
Systemgrenzen (`xIsBorner`), die tragende Fördertechnik (`xInCarouselRun`, Gefälle →
|
||||
`TRUE`), ob ein Scanner am Separator sitzt (`xPluggedScanner`) und Längen als
|
||||
Grundlage für Zeiten/Staulängen. Das meiste davon ist heute in `DerivedTro` →
|
||||
`SclTro` abgebildet; Vorfahrt, `xIsBorner` und Längen noch nicht.
|
||||
- **Aus der Elektroplanung (E)** kommen **alle Namen und Adressen**: jeder Sensor
|
||||
(`BG`), jedes Ventil (`MB`), Motor (`MA`), Motorschutz (`FC`), Taster (`SF`), Scanner
|
||||
(`BX`) samt Cognex-Variablen und Hardware-Kennung, die `xPlugged*`-Schalter (was ist
|
||||
tatsächlich verbaut), Weichenbauart, Not-Halt-Kreis, Schaltschrank und
|
||||
Steuerungsgrenzen. Im Render-Kontext sind das die `SclName` mit `todo=True`
|
||||
(`TODO(E-Planung)`). Offen ist die Brücke: die EA-Liste kennt keine Topologie; es
|
||||
braucht eine Zuordnung Layout-Bauteil (Separator-`TeileId`) ↔ BMK (`BG3242`/`MB3242`).
|
||||
Diese Brücke baut `lib/e_planning.py` über die Symbolpositionen in
|
||||
`*_positionsdraw.json` des Kabellängen-Tools (`doc/E-Planung_Bruecke.md`);
|
||||
heute kommen damit Separator-Sensor und Stopperventil in die SCL
|
||||
(`scl_skeleton --eplan`).
|
||||
- **Namenskonvention (K)**: `cMainTro`, `cSep`, `cSwi`, Staubereichs-DBs, HMI- und
|
||||
State-Pfade — sobald Nummernkreise feststehen, ohne weitere Quelle erzeugbar.
|
||||
- **Parameter (P)**: Zeiten, Projektkonstanten, WCS-Ziele/Stationen, Test-/IBN-Signale —
|
||||
weder L noch E; gehören als Typ-Defaults in den Katalog bzw. in eine
|
||||
Projektkonfiguration.
|
||||
@@ -0,0 +1,416 @@
|
||||
# Konzept: Layout und Hardwareplanung → Anlagen-JSON → SCL
|
||||
|
||||
Stand 2026-10-11. Zielbild für den Ausbau der Pipeline, damit die beiden
|
||||
Fachabteilungen unabhängig voneinander und zeitversetzt liefern können. Welche
|
||||
Angaben aus welcher Quelle kommen, steht in `doc/Datenquellen_Layout_Hardwareplanung.md`;
|
||||
die Rohangaben beider Quellen in `doc/Klassenhierarchie.md`, Kapitel "Daten".
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
Die Produktion läuft über zwei Abteilungen:
|
||||
|
||||
1. **Anlagenlayout** (aus dem Angebotswesen, detaillierter aus der Projektierung).
|
||||
Liefert alles für die TROs: Bauteile, Abfolge, Weichen, Staubereiche,
|
||||
Systemgrenzen.
|
||||
2. **Hardwareplanung** (Karten, Kabel, Sensoren, Aktoren). Liefert die globalen
|
||||
Variablen und die tatsächlichen Sensoren und Aktoren mit Namen und Adressen.
|
||||
|
||||
Die Hardwareplanung kommt **Tage bis Wochen später** als das Layout. Das System
|
||||
muss deshalb mit dem Layout allein ein brauchbares SCL-Skelett erzeugen und die
|
||||
Hardware später nachtragen können, ohne Layout-Arbeit zu wiederholen.
|
||||
|
||||
## Zielbild
|
||||
|
||||
```text
|
||||
Abteilung 1: Anlagenlayout Abteilung 2: Hardwareplanung (später)
|
||||
CSV ──tro_flow──> <csv>_tro.json TIA.xlsx, positionsdraw.json
|
||||
(TROs, Abfolge, ──hw_import──> <projekt>_hw.json
|
||||
Staubereiche, Slots) (globale Variablen, Geräte)
|
||||
│ │
|
||||
└──────────── merge_hw_layout ─────────────────┘
|
||||
│
|
||||
<csv>_anlage.json (Layout + Zuordnungen)
|
||||
│
|
||||
scl_skeleton ──> FB_Main.scl, FB_CallSensors.scl, …
|
||||
```
|
||||
|
||||
| Stufe | Skript | Eingabe | Ausgabe | Status |
|
||||
|---|---|---|---|---|
|
||||
| 1 Layout | `tro_flow.py --json` | CSV, `cfg/tro_overrides.ini` | `<csv>_tro.json` | vorhanden, wird erweitert |
|
||||
| 2 Hardware | `hw_import.py` | `*_TIA.xlsx`, `*_positionsdraw.json` (optional `*_EA.xlsx`/`*_WSCAD.xlsx` ohne Positionsdatei) | `<projekt>_hw.json` | vorhanden (2026-10-15), ohne EA/WSCAD |
|
||||
| 3 Zuordnung | `merge_hw_layout.py` | `<csv>_tro.json`, `<projekt>_hw.json`, Regeln, `cfg/hw_overrides.ini` | `<csv>_anlage.json`, Bericht `.md` | vorhanden (2026-10-15, Logik aus `e_planning.py`) |
|
||||
| 4 SCL | `scl_skeleton.py` | `<csv>_anlage.json` (oder nur `<csv>_tro.json`) | `.scl` | vorhanden, wird umgestellt |
|
||||
|
||||
Grundsätze:
|
||||
|
||||
- **Stufe 1 allein reicht für ein Skelett.** Ohne Hardware wird jeder offene Wert
|
||||
wie heute `TODO(E-Planung)`.
|
||||
- **Die Stufen verändern sich nicht gegenseitig.** Die Zuordnung legt ihre
|
||||
Ergebnisse neben das Layout, statt es zu überschreiben. Ein neuer Layoutstand
|
||||
oder ein neuer Hardwarestand heißt: die betroffene Stufe und alles danach neu
|
||||
ausführen.
|
||||
- **Lieber eine Lücke als ein falscher Name** (wie heute in `cfg/e_planning.ini`):
|
||||
was keine Regel sicher trifft, bleibt offen und steht im Bericht.
|
||||
- **Drei Quellen, jede sichtbar:** Layout, Hardwareplanung und Defaults
|
||||
(`cfg/defaults.ini`, Typ-Katalog). Jede SCL-Zeile lässt sich einer davon zuordnen.
|
||||
|
||||
## Umbauten
|
||||
|
||||
### 1. TRO-JSON zur Layout-JSON ausbauen (Stufe 1)
|
||||
|
||||
Heute fehlt der Zuordnung ein Ziel: die TRO-JSON sagt nicht, welche Stellen ein
|
||||
TRO hat, an die Hardware gehört. Daran scheitern heute Stausensoren und
|
||||
Weichenventile (`doc/E-Planung_Bruecke.md`, "Offen").
|
||||
|
||||
- **Slots je TRO:** je Separator, Weiche, Ausgang und Antrieb ein Eintrag mit der
|
||||
Layout-ID des Bauteils und den Rollen, die die Hardwareplanung dort füllen muss.
|
||||
|
||||
```json
|
||||
"slots": [
|
||||
{"slot": "separator1", "layoutId": "0008", "roles": ["sensor", "stopper", "jamSensor?", "partSensor?"]},
|
||||
{"slot": "switch1", "layoutId": "0037", "exit": 3, "roles": ["valve", "exitSensor?"]},
|
||||
{"slot": "drive", "layoutId": "0002", "roles": ["carouselRun"]}
|
||||
]
|
||||
```
|
||||
|
||||
Die Slots ergeben sich aus dem Katalogtyp (`items`) und der Topologie; `?` =
|
||||
optional, fehlt das Gerät, wird der zugehörige `xPlugged*`-Schalter `FALSE`.
|
||||
- **Staubereiche als eigene Liste `jamAreas[]`:** ein Staubereich je TRO-Eingang,
|
||||
mit allen Vorgängern, dem Förderer, auf dem er liegt, und seiner Lage auf dem
|
||||
Förderer (siehe "Staubereiche" unten). Heute nur abgeleitet beim Rendern
|
||||
(`scl_context.SclJam`). Dabei korrigieren: `scl_skeleton` erzeugt heute je
|
||||
Vorgänger einen eigenen Eingang (`arInOutJamEntr1`, `…Entr2`); in den Referenzen
|
||||
laufen mehrere Vorgänger in **einem** Staubereich zusammen, der TRO hat einen
|
||||
Eingang (Ausnahme `2Sep1Swi`, PinStore).
|
||||
- **Weitere Layout-Angaben:** `xIsBorner`, Vorfahrt ja/nein, tragender Förderer
|
||||
mit Rolle (heute `hosts` ohne Rolle), Längen für `tConvEmpty` und Staulängen.
|
||||
- **Ableitungen aus `scl_context` nach `tro_flow`/`model` verschieben,** damit die
|
||||
JSON sie trägt und nicht erst das Rendern sie kennt.
|
||||
- Schema `sps_skel-tro-v4`.
|
||||
|
||||
**Stand (2026-10-11), erster Teil umgesetzt:**
|
||||
|
||||
- `model.Slot` und `DerivedTro.slots`: `separator<n>`, `switch<n>`,
|
||||
`scanner<n>`, `drive` mit Rollen, Anzahl aus dem Katalog (`derive_slots`).
|
||||
Weichen-Slots haben noch keine `layoutId`: `tro_flow` hält das Weichen- bzw.
|
||||
Ausschleuselement nicht am TRO fest.
|
||||
- `model.JamArea` und `TroDocument.jamAreas`: je Separator laut Katalog ein
|
||||
Eingangs-Staubereich `J<Separator>` mit allen Vorgängern, dem Host und der
|
||||
Lage (`derive_jam_areas`). Bei 2Sep-Typen ist die Aufteilung der Vorgänger
|
||||
auf die zwei Eingänge offen (Hinweis im Feld `note`); beim PinStore heißt
|
||||
der Eingang vorläufig nach dem ersten Linien-Separator.
|
||||
- `scl_context` liest die Staubereiche aus der JSON: **ein** `JamEntr` je
|
||||
eigenem Staubereich statt je Vorgänger (Abweichung zu UH01–UH05 behoben),
|
||||
jeder Ausgang nennt im Kommentar den Staubereich des Nachfolgers. Golden
|
||||
Files neu erzeugt. Eine v3-Datei ohne `jamAreas` wird weiter gelesen.
|
||||
- **Noch offen aus Umbau 1:** Layout-ID der Weichen, `xIsBorner`, Vorfahrt,
|
||||
Host-Rolle, Längen (`tConvEmpty`, Staulängen), `<NNNN>`/`<k>` für den
|
||||
SCL-Namen der Staubereiche.
|
||||
|
||||
### 2. Neues Skript `hw_import.py` (Stufe 2)
|
||||
|
||||
- Liest TIA-Import (PLC Tags + Constants) und `positionsdraw.json`. Das genügt:
|
||||
bei Mubea stehen alle BMK der Positionsdatei im TIA-Import, die Kommentare sind
|
||||
gleich; EA-Liste und WSCAD-Import bringen nichts dazu (EA ohne Namen, WSCAD nur
|
||||
die E/A-Baugruppe, die `KENNZEICHNUNG` schon trägt). EA/WSCAD nur als Ersatz,
|
||||
wenn keine Positionsdatei vorliegt (Weg "nur EA-Liste" in Stufe 3). Befund in
|
||||
`doc/E-Planung_Bruecke.md`, "Nachprüfung 2026-10-15".
|
||||
- Schreibt ein neues Modell `HardwareDocument`:
|
||||
- **Geräte** je BMK: Adresse, Index-Konstante (`cInBG3306`), Kommentar, VERW,
|
||||
Steuerung, Station/Schrank, Position (falls vorhanden);
|
||||
- **globale Variablen**: alle Tags und Konstanten, Motor↔Motorschutz-Paare,
|
||||
Taster, Not-Halt.
|
||||
- Prüft die Exporte gegeneinander: BMK fehlt in einer Liste, doppelte BMK,
|
||||
Adresse ohne Tag.
|
||||
- Übernimmt aus `e_planning.py`: `load_positionsdraw`, `EDevice`, `pair_drives`.
|
||||
- Erste Quelle: der TIA-Import, weil er Namen und Konstanten 1:1 liefert und die
|
||||
spätere Prüfung des SCL ermöglicht.
|
||||
|
||||
### 3. Neues Skript `merge_hw_layout.py` (Stufe 3)
|
||||
|
||||
- Eingabe: `<csv>_tro.json` + `<projekt>_hw.json`; das CSV wird nicht mehr gelesen.
|
||||
- Zuordnungswege, als Regeln in einer INI (heute `cfg/e_planning.ini`):
|
||||
- **räumlich** über `positionsdraw`: die bestehende Logik aus `e_planning.py`
|
||||
(Lagebezug über `dxf_registration`, Höchst-/Sollabstand), jetzt gegen die Slots;
|
||||
- **Nummernregel**: `MB = BG + 1` bzw. gleiche Nummer, Motor = Motorschutz;
|
||||
- **nur EA-Liste, ohne Positionen**: über die Kommentare
|
||||
("in Separator 1 (ILS-CV M0108)") und eine von Hand gepflegte Tabelle
|
||||
M-Nummer → Layout-Förderer.
|
||||
- Handkorrekturen in `cfg/hw_overrides.ini` (analog `cfg/tro_overrides.ini`).
|
||||
- Ausgabe `<csv>_anlage.json`: Layoutteil unverändert, daneben
|
||||
|
||||
```json
|
||||
"bindings": [
|
||||
{"layoutId": "0008", "role": "sensor", "bmk": "BG3306", "constant": "cInBG3306",
|
||||
"address": "%E330.6", "source": "positionsdraw", "rule": "raeumlich",
|
||||
"distance": 12.0, "status": "zugeordnet"}
|
||||
]
|
||||
```
|
||||
|
||||
`status`: `zugeordnet` | `offen` | `widerspruch`.
|
||||
- **Stabile Schlüssel:** Zuordnungen hängen an TRO-ID + Slot + Rolle. Die TRO-IDs
|
||||
sind ab der Festlegung durch die SPS-Programmierer stabil (siehe "TRO-IDs"
|
||||
unten). Die Layout-`TeileId` taugt dafür **nicht**: sie ändert sich zwischen
|
||||
zwei CSV-Exporten (siehe dort). `layoutId` in `bindings` dient nur zur
|
||||
Nachverfolgung im aktuellen Stand.
|
||||
- **Gegenprüfung** beider Quellen (Anzahl Separatoren vs. `IN SEP`, Weichen vs.
|
||||
`AE ABZWEIG`, angetriebene Strecken vs. `MA`) in den Bericht.
|
||||
|
||||
**Stand Umbau 2/3 (2026-10-15), umgesetzt:**
|
||||
|
||||
- `lib/hw_import.py` (`bin/hw_import.bat`): TIA-Import (Blätter an der
|
||||
Kopfzeile erkannt, xlsx ohne openpyxl gelesen) + Positionsdatei →
|
||||
`<projekt>_hw.json` (`HardwareDocument`, `sps_skel-hw-v1`): je BMK Adresse,
|
||||
Richtung, Index-Konstante, VERW, Station, Lage, Förderer; Antriebe mit
|
||||
Motorschutz und Geber; Gegenprüfung beider Quellen. EA-Liste/WSCAD nicht
|
||||
gelesen (Weg "nur EA-Liste" bleibt offen).
|
||||
- `lib/merge_hw_layout.py` (`bin/merge_hw_layout.bat`): `<csv>_tro.json` +
|
||||
`<projekt>_hw.json` → `<csv>_anlage.json` (`model.AnlageDocument`,
|
||||
`sps_skel-anlage-v1`): Layout unverändert unter `layout`, daneben
|
||||
`bindings[]` (Schlüssel TRO-ID + Slot + Rolle, mit Konstante und Adresse aus
|
||||
dem TIA-Import, Status `zugeordnet`/`offen`/`widerspruch`/`ohne_slot`) und
|
||||
`conveyors[]`. Handkorrekturen `cfg/hw_overrides.ini`, Zahlenwerte
|
||||
`cfg/parameter.ini [merge_hw_layout]`.
|
||||
- Damit `merge_hw_layout` kein CSV liest, trägt die TRO-JSON jetzt `parts` (Lage und
|
||||
Hüllbox der Layout-Bauteile, von `tro_flow --json`; Schema bleibt v4, eine
|
||||
Datei ohne `parts` liest sich weiter). Ein `PinStore_Auto` hat je Lagerlinie
|
||||
einen Slot `line<n>`.
|
||||
- **Förderernummer:** die E-Planung nennt Förderer im Klartext als `K0102` bzw.
|
||||
`#106` (beides dieselbe Nummer, `CONVEYOR-#103` = `CONVEYOR-K0103`).
|
||||
`merge_hw_layout` ordnet sie per Mehrheit der Feldgeräte einem Layout-Förderer zu
|
||||
(Mubea: K0102 = Kreisel3, K0104 = Kreisel2, K0105 = Kreisel1). Das ist die in
|
||||
"Staubereiche" gesuchte Quelle für `<NNNN>` und für `xInCarouselRun`.
|
||||
- Gespeicherter Lagebezug mit SHA-256 der Positionsdatei; ein neuer Stand
|
||||
gleichen Namens wird neu ermittelt.
|
||||
- Noch offen: Weichen-/Ausschleus-Slots mit Layout-ID (sonst bleiben
|
||||
Stausensoren und Ausschleusventile `ohne_slot`), Scanner (`BX`), Weg "nur
|
||||
EA-Liste", Umstellung von `scl_skeleton` (Umbau 4).
|
||||
|
||||
### 4. `scl_skeleton.py` / `scl_context.py` (Stufe 4)
|
||||
|
||||
- Liest **eine** Eingabe: `<csv>_anlage.json`; ohne Hardwareteil wie heute
|
||||
`<csv>_tro.json`.
|
||||
- `--eplan` und die Zusammenführung in `scl_context._separator` entfallen; der
|
||||
Kontext liest Namen aus `bindings`, offene Slots werden `TODO(E-Planung)`.
|
||||
- **Prüfung gegen die globalen Variablen:** jeder Tag und jede `cIn…`-Konstante im
|
||||
SCL muss im TIA-Import stehen, sonst Abbruch mit Liste.
|
||||
- **Neue Bausteine** nach Stufe 2: `FB_CallSensors` aus der Sensorliste,
|
||||
Antriebsaufrufe (`fbCarousel0102`) aus den Motorpaaren, Staubereichs-DBs.
|
||||
|
||||
### 5. Defaults als dritte Quelle
|
||||
|
||||
Zeiten und Projektkonstanten, die heute fest in `scl_skeleton.py` stehen
|
||||
(`t#600ms`, `T#1500ms` …), wandern in den Typ-Katalog (`tro_catalog.py`) bzw. eine
|
||||
Projektdatei `cfg/defaults.ini`.
|
||||
|
||||
## Bestehende Programme
|
||||
|
||||
| Programm | Rolle im neuen Ablauf |
|
||||
|---|---|
|
||||
| `material_flow.py`, `tro_flow.py` | bleiben Stufe 1; Ausbau um Slots und `jamAreas` |
|
||||
| `tro_catalog.py`, `model.py`, `tro_overrides.py`, `dxf_registration.py` | bleiben Bibliotheken; `model.py` bekommt Slots, `jamAreas`, Anlagen-JSON |
|
||||
| `e_planning.py` | wird aufgeteilt (unten) |
|
||||
| `tro_annotate.py` | bleibt als Kontrollansicht, liest künftig die JSON (unten) |
|
||||
| `tro_extract.py` | entfällt bzw. wird umgebaut (unten) |
|
||||
| `scl_skeleton.py`, `scl_context.py` | Stufe 4, Umstellung auf `<csv>_anlage.json` |
|
||||
|
||||
### `e_planning.py` → `hw_import` + `merge_hw_layout`
|
||||
|
||||
| Teil heute | Wohin |
|
||||
|---|---|
|
||||
| `load_positionsdraw`, `EDevice`, `pair_drives` | `hw_import.py` |
|
||||
| `resolve_transform`, `load_rules`, `_assign_spatial`, `_assign_by_number`, Bericht | `merge_hw_layout.py`, Ziele = Slots statt CSV-Bauteile |
|
||||
| `cfg/e_planning.ini` | Regeldatei von `merge_hw_layout` |
|
||||
| `EplanDocument` / `<csv>_eplan.json` | entfällt, ersetzt durch `bindings[]` |
|
||||
|
||||
Übergang: `e_planning.py` bleibt kurz als Hülle, die `hw_import` und `merge_hw_layout`
|
||||
nacheinander aufruft, damit `bin/e_planning.bat` weiter funktioniert; danach
|
||||
löschen.
|
||||
|
||||
### `tro_annotate.py` → Kontrollansicht für Layout und Zuordnung
|
||||
|
||||
- **Aus der JSON zeichnen** statt `analyse_tros` selbst aufzurufen: die Zeichnung
|
||||
zeigt dann genau das, woraus das SCL entsteht.
|
||||
- **Zuordnungen einzeichnen:** neben jedem Slot das BMK (`BG3306`, `MB3306`),
|
||||
offene Slots und Widersprüche rot. Für die Hardwareplanung die schnellste
|
||||
Kontrolle der räumlichen Zuordnung.
|
||||
- `--emit-lisp` (`TROEDIT`) bleibt, solange Marker in BricsCAD bearbeitet werden;
|
||||
hängt an der Entscheidung zu `tro_extract`.
|
||||
|
||||
### `tro_extract.py` → entfällt im neuen Ablauf
|
||||
|
||||
Es wäre ein dritter Korrekturweg neben `cfg/tro_overrides.ini` (Layout) und
|
||||
`cfg/hw_overrides.ini` (Zuordnung) und schreibt noch das alte Schema
|
||||
`sps_skel-tro-extract-v1`, das niemand mehr liest.
|
||||
|
||||
- **Löschen** (Empfehlung), zusammen mit `TROEDIT`/`--emit-lisp`. Korrekturen nur
|
||||
in den INI-Dateien: nachvollziehbar und versioniert.
|
||||
- **Umbauen**, falls die Projektierung in BricsCAD korrigieren will: es schreibt
|
||||
dann Einträge in `tro_overrides.ini`/`hw_overrides.ini`, keine eigene JSON.
|
||||
|
||||
Bis zur Entscheidung bleibt es geparkt.
|
||||
|
||||
## Festlegungen (2026-10-11)
|
||||
|
||||
### TRO-IDs
|
||||
|
||||
**Festgelegt:** Die TRO-IDs vergeben die SPS-Programmierer, die zugleich die
|
||||
Anwender der Skripte sind. Eine ID muss nur **eindeutig** sein. Üblich ist eine
|
||||
Nummerierung entlang des Materialflusses und eine Benennung nach den Bauteilen
|
||||
(Kreisel usw.), vorgeschrieben ist das nicht. Sobald größere Teile des SPS-Codes
|
||||
stehen, ändern sich die IDs nicht mehr; in der frühen Projektphase dürfen sie
|
||||
mehrmals neu vergeben werden.
|
||||
|
||||
**Befund:** Heute vergibt `tro_flow` laufende Nummern je Lauf (`TRO01`, `TRO02` …,
|
||||
`tro_flow.py` Zeile ~1163); `cfg/tro_overrides.ini` warnt bereits, dass sich die
|
||||
Nummern bei neuer Datenbasis verschieben. Die Layout-`TeileId` ist ebenfalls nicht
|
||||
stabil: Vergleich zweier Exporte derselben Anlage
|
||||
|
||||
| Vergleich | Bauteile | gleiche Id und Lage | gleiche Lage, andere Id |
|
||||
|---|---|---|---|
|
||||
| `mubea.csv` ↔ `mubea_export.csv` | 119 / 125 | 46 | 27 |
|
||||
| `HundM05.csv` ↔ `HundM05_14.csv` | 45 / 44 | 22 | 16 |
|
||||
|
||||
Stabil ist nur die **Lage** des Bauteils.
|
||||
|
||||
**Umgesetzt (2026-10-11, `lib/tro_ids.py`, Schalter in `tro_flow.py`;
|
||||
Einzelheiten in `doc/Python_Scripts.md` §4 "TRO-IDs"):**
|
||||
|
||||
- Eine versionierte ID-Datei je Anlage, `cfg/<csv>_tro_ids.ini`, Abschnitt
|
||||
`[tro]`: je TRO die ID und die Lage seines Anker-Separators (x, y in mm), der
|
||||
Kommentar dahinter nennt Typ, Bezeichnung und Separatoren. Von `tro_flow`
|
||||
angelegt, von den SPS-Programmierern umbenannt. Weitere Abschnitte (später
|
||||
Separator- und Förderernummern) bleiben bei jedem Lauf erhalten. Ein anderer
|
||||
Export derselben Anlage nutzt dieselbe Datei über `--ids`.
|
||||
|
||||
```ini
|
||||
[tro]
|
||||
TRO01 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
|
||||
K0302_1 = 10310.0, -440.1 ; von Hand umbenannt
|
||||
```
|
||||
|
||||
- **Standard (IDs halten):** `tro_flow` liest die ID-Datei und findet jeden TRO
|
||||
über die Lage seiner Separatoren wieder (Toleranz `--id-tolerance`, Standard
|
||||
100 mm, nächster Treffer zuerst, je TRO ein Eintrag). Gefundene TROs behalten
|
||||
ihre ID, auch umbenannte. Neue TROs bekommen die nächste freie Nummer und
|
||||
werden an `[tro]` angehängt; weggefallene stehen im Bericht (mit dem TRO, der
|
||||
ihren Separator jetzt enthält), bleiben in der Datei, ihre ID wird nicht
|
||||
wiederverwendet. `tro_annotate` liest dieselbe Datei, schreibt sie aber nicht.
|
||||
- **Programmschalter `--renumber` (frühe Phase):** alle IDs neu entlang des
|
||||
Materialflusses vergeben (Tiefensuche ab den Beladestationen, auf Kreisel und
|
||||
Strecke in `TrackIds`-Reihenfolge) und `[tro]` neu schreiben. Ohne ID-Datei
|
||||
geschieht dasselbe beim ersten Lauf.
|
||||
- **Geprüft** mit `mubea.csv` → `mubea_export.csv` (andere TeileIds): 18 TROs
|
||||
wiedergefunden, darunter eine umbenannte ID; `TRO14` als weggefallen gemeldet,
|
||||
weil der Export die Separatoren 0005 und 0036 zu einem `2Sep2Swi` (`TRO13`)
|
||||
zusammenfasst.
|
||||
- **Noch offen:** die Abschnitte `[mubea.csv.tro.TRO03]` usw. in
|
||||
`cfg/tro_overrides.ini` hängen an der alten Nummerierung und passen schon
|
||||
heute nicht mehr (`TRO03` ist dort Sep 0010, im aktuellen Lauf Sep 0011).
|
||||
Sie werden auf die IDs der ID-Datei umgestellt, sobald `tro_flow` die
|
||||
Overrides wieder liest.
|
||||
- Dadurch werden auch die Abschnitte `[<csv>.tro.<TRO-ID>]` in
|
||||
`cfg/tro_overrides.ini` und die Zuordnungen von `merge_hw_layout` stabil.
|
||||
|
||||
### Staubereiche
|
||||
|
||||
**Bisheriges Namensschema** (Referenzen UH01–UH05, `doc/TRO_Katalog/connect.ini`,
|
||||
286 Staubereiche):
|
||||
|
||||
```text
|
||||
"DB_JamArea<NNNN>".stJam<NNNN>_<k> Kurzform JA<NNNN>_<k>
|
||||
```
|
||||
|
||||
- `<NNNN>` = **Förderer**, auf dem der Staubereich liegt, ein DB je Förderer:
|
||||
`0102` = Kreisel K0102 (vgl. `#fbCarousel0102`), `0103`/`0104` = Strecken,
|
||||
`4103`/`4104` = Vario-Förderer.
|
||||
- `<k>` = laufende Nummer **entlang des Materialflusses** auf diesem Förderer;
|
||||
Kreisel K0102 hat `_1` … `_6` in Umlaufrichtung.
|
||||
- 249 der 286 folgen diesem Schema. Sonderfälle: Lagerlinien `Storage1.1.2` (24),
|
||||
Übergabe an eine andere Steuerung `IO_Device_UHxx_TO_UHyy…` bzw. lokal
|
||||
`#arCarrierHandoverJam5000` (9), `JamBuffer101` im Leerträgerpuffer (2), F2H (1).
|
||||
|
||||
**Prüfung der Annahme "ein Staubereich = Ausgang des Vorgängers und Eingang des
|
||||
Nachfolgers":**
|
||||
|
||||
- **Kein** Staubereich hat mehr als einen Nachfolger-TRO.
|
||||
- **65** Staubereiche haben mehrere Vorgänger: dort laufen Ströme zusammen, z. B.
|
||||
`JA0102_2` mit Vorgängern TRO101, TRO106 und TRO107 (Weichenausgang), alle mit
|
||||
Nachfolger TRO112.
|
||||
- Jeder TRO hat genau einen Eingangs-Staubereich; einzige Ausnahme sind die
|
||||
PinStore-TROs (zusätzlich die Lagerlinie).
|
||||
|
||||
Ein Staubereich ist also immer **der Eingang genau eines TRO**, aber nicht immer
|
||||
der Ausgang genau eines Vorgängers. Ein Name nach dem Schema `ja_tro3tro4` ist bei
|
||||
Zusammenführungen nicht eindeutig.
|
||||
|
||||
**Vorschlag:**
|
||||
|
||||
**Ergänzung (2026-10-11):** In einem Mubea-Layout heißen die Staubereiche nach
|
||||
**ihrem Separator**: der Staubereich vor `Sep1024` heißt `J1024`. Das passt zum
|
||||
Befund oben: der Staubereich ist die Warteschlange vor dem Stopper, also der
|
||||
Eingang genau eines TRO bzw. genau eines Separators. Das Layout liegt nicht im
|
||||
Repo; Quelle der Separatornummer (Zeichnungsbeschriftung, E-Planung, Vergabe durch
|
||||
die SPS-Programmierer) noch klären.
|
||||
|
||||
- **Kennung nach dem Separator:** `J<Separatornummer>`, z. B. `J1024`. Das ist
|
||||
feiner als eine Kennung je TRO und deckt TROs mit mehreren Separatoren
|
||||
(`2Sep1Swi` mit `JamEntr1`/`JamEntr2`) ohne Sonderregel ab. Zusammenführungen
|
||||
sind kein Problem, weil der Name nur am Nachfolger hängt.
|
||||
- **Separatornummern wie TRO-IDs behandeln:** in derselben ID-Datei, über die Lage
|
||||
des Separators wiedergefunden, von den SPS-Programmierern vergeben bzw.
|
||||
übernommen; `--renumber` vergibt sie in der frühen Phase neu.
|
||||
- **Name im SCL — festgelegt: Standard sind die SCL-Dateien UH01–UH05,** also
|
||||
`"DB_JamArea<NNNN>".stJam<NNNN>_<k>`. `J<Separatornummer>` ist nur die Kennung
|
||||
in der JSON (und die Beschriftung im Layout), nicht der SCL-Name. Für den
|
||||
SCL-Namen braucht es je Staubereich:
|
||||
- `<NNNN>`: die **Förderernummer** des Förderers, auf dem der Staubereich liegt
|
||||
(Kreisel K0102 → `0102`); vergeben von den SPS-Programmierern bzw. aus der
|
||||
Hardwareplanung (`KR-K0102`), gepflegt in der ID-Datei wie TRO-IDs und
|
||||
Separatornummern;
|
||||
- `<k>`: laufende Nummer **entlang des Materialflusses** auf diesem Förderer,
|
||||
aus dem Layout abgeleitet (Kreisel in Umlaufrichtung, beginnend an einer
|
||||
festzulegenden Stelle, z. B. nach der Einschleusung).
|
||||
|
||||
Ohne Förderernummer bleibt der Name `TODO(E-Planung)` wie heute.
|
||||
- Ausnahmen bleiben Sonderfälle mit eigenem Namen: Lagerlinien, Übergabe an eine
|
||||
andere Steuerung, Leerträgerpuffer.
|
||||
|
||||
### Korrekturen in BricsCAD
|
||||
|
||||
**Zurückgestellt.** Eine Alternative wäre, die `<csv>_tro.json` zu visualisieren
|
||||
und von Hand zu bearbeiten. Bis dahin bleibt `tro_extract.py` geparkt, `TROEDIT`
|
||||
unverändert.
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
1. **TRO-IDs stabil machen:** ID-Datei, Wiederfinden über die Lage,
|
||||
`--renumber`. Darauf bauen Overrides, Staubereiche und Zuordnungen auf.
|
||||
**Erledigt** (2026-10-11).
|
||||
2. Slots und `jamAreas` in die TRO-JSON (Umbau 1), ein Eingangs-Staubereich je
|
||||
TRO — ohne Slots hat die Zuordnung kein Ziel. **Erster Teil erledigt**
|
||||
(2026-10-11, siehe Umbau 1 "Stand").
|
||||
3. `hw_import` mit dem TIA-Import als erster Quelle. **Erledigt** (2026-10-15,
|
||||
siehe unten "Stand Umbau 2/3").
|
||||
4. `merge_hw_layout` mit der räumlichen Logik aus `e_planning.py`; `e_planning.py` als
|
||||
Übergangshülle. **Erledigt bis auf die Hülle** (2026-10-15): die Logik liegt
|
||||
in `merge_hw_layout.py`, `e_planning.py` importiert sie und bleibt vollständig,
|
||||
solange `scl_skeleton --eplan` die `<csv>_eplan.json` liest (Schritt 5).
|
||||
5. `scl_skeleton` auf `<csv>_anlage.json` umstellen, `--eplan` entfernen;
|
||||
`tro_annotate` auf JSON umstellen.
|
||||
6. Defaults (`cfg/defaults.ini`), `FB_CallSensors`, EA-Liste ohne Positionen.
|
||||
|
||||
Zu jedem Schritt: `bin/`-Wrapper für neue Skripte, Tests unter `tests/`
|
||||
(kleine TIA-Datei als Testdaten), Golden Files bei Schemawechsel bewusst neu
|
||||
erzeugen, `doc/Python_Scripts.md` ergänzen.
|
||||
|
||||
## Offene Entscheidungen
|
||||
|
||||
- **Staubereiche:** woher die Separatornummer (`Sep1024` → `J1024`) stammt; wo auf
|
||||
einem Kreisel die Zählung `<k>` beginnt (Referenz K0102 prüfen). Das SCL-Schema
|
||||
ist festgelegt (UH01–UH05).
|
||||
- **Toleranz** beim Wiederfinden der TROs über die Lage: vorläufig 100 mm
|
||||
(`--id-tolerance`). Ein weiter verschobener Separator wird als "neu" und
|
||||
"weggefallen" gemeldet, der Programmierer übernimmt die ID von Hand in der
|
||||
ID-Datei. Mit echten Layoutänderungen prüfen, ob der Wert passt.
|
||||
- **Korrekturen in BricsCAD oder in der JSON:** zurückgestellt.
|
||||
+513
-90
@@ -1,13 +1,20 @@
|
||||
# Python Scripts — CLI Tooling Overview
|
||||
|
||||
**As of:** 2026-09-17
|
||||
**As of:** 2026-10-15
|
||||
|
||||
`lib/` currently holds nine Python modules (five of them runnable CLI tools,
|
||||
four supporting libraries), plus one documentation-generator script that lives
|
||||
under `doc/TRO_Katalog/tro_graphs/`. Together the CLI tools turn a CSV export
|
||||
of the mechanical layout (ILS 2.0) into a material-flow graph, a derived TRO
|
||||
list/diagram, an annotated copy of the BricsCAD drawing, a TRO-JSON read back
|
||||
out of that drawing, and an `FB_Main` SCL skeleton generated from that JSON.
|
||||
`lib/` currently holds sixteen Python modules (eight of them runnable CLI
|
||||
tools, eight supporting libraries), plus one documentation-generator script
|
||||
that lives under `doc/TRO_Katalog/tro_graphs/`. Together the CLI tools turn a
|
||||
CSV export of the mechanical layout (ILS 2.0) into a material-flow graph, a
|
||||
derived TRO list/diagram, a TRO-JSON (shared model `lib/model.py`), an
|
||||
`FB_Main` SCL skeleton generated from that JSON, and an annotated copy of the
|
||||
BricsCAD drawing for visual checking. `e_planning.py` bridges to the
|
||||
electrical planning: it assigns the sensors/valves of the E-planning drawing
|
||||
to the layout parts, so the skeleton can carry their real names.
|
||||
`hw_import.py` and `merge_hw_layout.py` are the new path for that bridge
|
||||
(`doc/Konzept_Layout_Hardware_SCL.md`, stages 2 and 3): TIA import +
|
||||
positions file → `<project>_hw.json`, merged with the TRO-JSON into
|
||||
`<csv>_anlage.json` (§5d, §5e).
|
||||
|
||||
This document is the practical "what does each script do and which switches
|
||||
does it take" reference. For the underlying domain model (TRO types, JSON
|
||||
@@ -26,18 +33,36 @@ material_flow.py ──► <name>_material_flow.dot / .svg / .md
|
||||
▼
|
||||
tro_flow.py ──► <name>_tro_flow.dot / .svg, _tro_doc.md, _tro_errors.md
|
||||
│ (derives TROs, using the type catalog in tro_catalog.py)
|
||||
▼
|
||||
tro_annotate.py ──► <name>_annotated.dxf, <name>_registration.json
|
||||
│ (also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF
|
||||
│ coordinate offset and tro_catalog.py for marker shape/color per TRO type)
|
||||
▼ (drawing may be hand-edited in BricsCAD before the next step)
|
||||
tro_extract.py ──► <drawing>_tro.json
|
||||
│ (reads only the drawing; resolves plant coords via the registration)
|
||||
▼
|
||||
scl_skeleton.py ──► <source>_FB_Main.scl
|
||||
(one REGION per TRO; E-planning values left as TODO(E-Planung) gaps)
|
||||
│
|
||||
├── --json ──► <name>_tro.json (model.TroDocument — MAIN PATH)
|
||||
│ │
|
||||
│ ▼
|
||||
│ scl_skeleton.py ──► <name>_FB_Main.scl
|
||||
│ (validates the JSON against model.py; one REGION per TRO;
|
||||
│ E-planning values left as TODO(E-Planung) gaps)
|
||||
│ ▲
|
||||
│ │ --eplan <name>_eplan.json (optional)
|
||||
│ e_planning.py ◄── <drawing>_positionsdraw.json (kabellaengen)
|
||||
│ (assigns BG/MB of the E-planning drawing to the layout
|
||||
│ parts by position + numbering rule; see §5c)
|
||||
│
|
||||
│ NEW PATH (stages 2/3, scl_skeleton does not read it yet):
|
||||
│ <name>_tro.json ──┐
|
||||
│ ├─► merge_hw_layout.py ──► <name>_anlage.json + .md (§5e)
|
||||
│ hw_import.py ─────┘ (TRO slots <- BMK, conveyors K0102 -> Kreisel3)
|
||||
│ ▲ *_TIA.xlsx + *_positionsconv.json (§5d)
|
||||
│
|
||||
└── same derivation ──► tro_annotate.py ──► <name>_annotated.dxf (VIEWER ONLY)
|
||||
(also needs the BricsCAD DXF; uses dxf_registration.py for the CSV↔DXF
|
||||
coordinate offset and tro_catalog.py for marker shape/color per TRO type)
|
||||
|
||||
parked: tro_extract.py drawing ──► JSON (not extended; see §5a)
|
||||
```
|
||||
|
||||
The drawing is a **view** for checking the result, not a data source: the
|
||||
TRO-JSON is written by `tro_flow.py --json` straight from the CSV, and
|
||||
`tro_annotate.py` derives exactly the same TROs to draw them.
|
||||
|
||||
- `material_flow.py` models the material flow of the **mechanical** objects only
|
||||
(Gefällestrecke, Strecke, Kreisel).
|
||||
- `tro_flow.py` is its sister program: it derives the **control objects**
|
||||
@@ -51,16 +76,20 @@ Supporting libraries (no CLI of their own, imported by the tools above):
|
||||
- **`lib/tro_catalog.py`** — the single source of truth for TRO types: short
|
||||
name, Siemens FB block, sub-components and counts, color group, CAD marker
|
||||
shape.
|
||||
- **`lib/model.py`** — the shared pydantic model of TRO *instances* and of the
|
||||
TRO-JSON (`TroBase`, `DerivedTro`, `Connection`, `TroCandidate`, `Analysis`,
|
||||
`TroDocument`). Written by `tro_flow.py --json`, read and validated by
|
||||
`scl_skeleton.py`. See §6b.
|
||||
- **`lib/dxf_registration.py`** — derives, verifies and persists the rigid
|
||||
(rotation + offset) transform between the CSV export's coordinate system and
|
||||
the DXF drawing's.
|
||||
- **`lib/tro_ids.py`** — the per-plant ID file `%SKEL_CFG%\<name>_tro_ids.ini`:
|
||||
reads it, finds each TRO again by the position of its separators, assigns
|
||||
new IDs, writes/extends the file. Used by `tro_flow.py` (read and write) and
|
||||
`tro_annotate.py` (read only). See §4, "TRO-IDs".
|
||||
- **`lib/tro_overrides.py`** — reads the hand-maintained
|
||||
`%SKEL_CFG%\tro_overrides.ini` (type corrections, open points, TRO merges)
|
||||
that survive re-runs of `tro_flow.py`. See §6a.
|
||||
- **`lib/tro_geometry.py`** — resolves a separator's host and a switch's exit
|
||||
count for `tro_flow.py` **directly from the part world-coordinates** (shapely
|
||||
`STRtree`) instead of from the CSV `Zuordnung`/`Nachbarn` columns, for exports
|
||||
where those columns have gaps. Only used with `tro_flow.py --geometry`. See §6b.
|
||||
`%SKEL_CFG%\tro_overrides.ini`; only `material_flow.py --connect` still uses
|
||||
it. See §6a.
|
||||
|
||||
Separate, documentation-only tool (not part of the generator pipeline):
|
||||
|
||||
@@ -81,7 +110,10 @@ Each CLI tool is invoked through its `bin/<name>.bat` (Windows) / `bin/<name>.sh
|
||||
|
||||
```bat
|
||||
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc
|
||||
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc --json
|
||||
bin\scl_skeleton.bat --json mubea_tro.json
|
||||
bin\e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json
|
||||
bin\scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json
|
||||
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
|
||||
```
|
||||
|
||||
@@ -93,14 +125,14 @@ Conventions shared by the CLI tools:
|
||||
- `--file` / `--dxf` accept either a bare filename — resolved against
|
||||
`%SKEL_DATA%` — or a full path.
|
||||
- All generated output is written to `%SKEL_RESULTS%`.
|
||||
- `tro_annotate.py` additionally persists the verified CSV↔DXF coordinate
|
||||
offset to `%SKEL_CFG%\dxf_registration.json`.
|
||||
- `tro_annotate.py` and `e_planning.py` additionally persist the verified
|
||||
coordinate offset (CSV↔DXF, CSV↔E-planning drawing) to
|
||||
`%SKEL_CFG%\dxf_registration.json`.
|
||||
- Every run prints a `====` bordered report to stdout (counts, warnings,
|
||||
output file paths); non-fatal findings are printed as `?`/`!` lines and also
|
||||
do not stop the run.
|
||||
- Requires the packages pinned in `requirements.txt` (`pydantic>=2.0.0`;
|
||||
`ezdxf>=1.4.0` for `tro_annotate.py` only; `shapely>=2.1` for
|
||||
`tro_flow.py --geometry` only) — run `bin\install_py.bat` once.
|
||||
`ezdxf>=1.4.0` for `tro_annotate.py` only) — run `bin\install_py.bat` once.
|
||||
- `--tosvg` needs Graphviz's `dot` (or `neato`, for `tro_flow.py
|
||||
--use-cords`) on `PATH`, or the `GRAPHVIZ_DOT` environment variable pointing
|
||||
at the `dot` executable. `bin/material_flow.bat`/`.sh` and `bin/tro_flow.bat`/`.sh`
|
||||
@@ -241,16 +273,18 @@ See `doc/track.md` for the fuller derivation notes.
|
||||
| `--use-cords` (alias `--use-coords`) | — | off | Place every node at its plant coordinate and render with `neato -n` (fixed positions) instead of `dot`, so the material graph matches the plant layout — the same option `tro_flow.py` has. The two Kreisel lanes (`-L`/`-R`) are nudged apart by `LANE_NUDGE_MM`; scale is `COORD_SCALE` (0.25 pt/mm, identical to `tro_flow.py`). Only has an effect together with `--tosvg`; fails (exit 1) if any flow node has no coordinate. |
|
||||
| `--doc` | — | off | Also write a Markdown report (`<name>_material_flow.md`) with object list, connections and plausibility findings. |
|
||||
| `--connect` | — | off | Also write the topology as `<name>_connect.ini` (item/flow level): `[…nodes]` (Kreisel lanes, Strecke, Gefällestrecke), `[…connections]` with a `kind` per edge (`normal`/`umlauf`/`weiche`/`einschleusung`/`ausschleusung`), and `[…externals]` — the BTMT entry/exit if present, otherwise the open lane-ends listed as entry candidates. A hand-editable bridge into the TRO step, styled after `doc/TRO_Katalog/connect.ini`. |
|
||||
| `--show-bbs` | — | off | Also write `<name>_bbs.svg`: a plain 2-D top-down view of every CSV row (not just flow nodes — Separator/Scanner/Ein-/Ausschleuselement too), one axis-aligned rectangle per row from the `Position` column (its center, X/Y) and the `Boundingbox` column (its width/depth, X/Y — the Z extent is ignored), with the row's `Bezeichnung` centered in it. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (`_kreisel_lane_rects()` — short axis divided in half, `Zuordnung`-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with `--show-kx`) still belong to the Kreisel as a whole and are drawn once, not per half. No rotation, no Graphviz — hand-built SVG; the `viewBox` stays in plant millimetres (Y flipped so plant "up" renders as screen "up") but the printed page size is scaled to DIN A3 (landscape or portrait, whichever matches the layout's aspect ratio) instead of the plant's real-world extent. Larger objects are drawn first so small ones (Separator, Scanner, …) stay visible on top. |
|
||||
| `--show-bbs [flow\|all]` | `flow` | off | Also write `<name>_bbs.svg`: a plain 2-D top-down view of every CSV row (not just flow nodes — Separator/Scanner/Ein-/Ausschleuselement too), one axis-aligned rectangle per row from the `Position` column (its center, X/Y) and the `Boundingbox` column (its width/depth, X/Y — the Z extent is ignored), with the row's `Bezeichnung` centered in it. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (`_kreisel_lane_rects()` — short axis divided in half, `Zuordnung`-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with `--show-kx`) still belong to the Kreisel as a whole and are drawn once, not per half. No rotation, no Graphviz — hand-built SVG; the `viewBox` stays in plant millimetres (Y flipped so plant "up" renders as screen "up") but the printed page size is scaled to DIN A3 (landscape or portrait, whichever matches the layout's aspect ratio) instead of the plant's real-world extent. Larger objects are drawn first so small ones (Separator, Scanner, …) stay visible on top. A legend column right of the plant lists every drawn kind with its row count, and each rectangle carries a `<title>` tooltip (row, TeileId, TeileArt, name). Mode `flow` (default, also bare `--show-bbs`) skips the VF/GF chain detail rows exported by dxfmakros since Sept. 2026 (`Bogen`, `Streckenelement`, `Motorstation`, `Umlenkstation` — `BBS_DETAIL_KINDS`), because they fully overlap their Strecke/Gefällestrecke box; `all` draws every row, detail kinds in their own stronger colours. dxfmakros calls this via `bin/show_exportcsv.bat` (mode `all`). |
|
||||
| `--show-kx` | — | off | Only valid together with `--show-bbs` (else exit 1). Additionally draw each object's connection coordinate systems K1–K4 into the `_bbs.svg`: the K1–K4 columns are 12-char base64 strings (three 24-bit fixed-point values `x,y,z`, factor 10, `csv:trans-encode` in `dxfmakros/Lisp/export.lsp`) decoded to a plant-mm X/Y. Each present K-point is drawn as a small red cross (two lines) with a small `K1`…`K4` label, and consecutive points are joined `K1→K2→K3→K4` by a blue line. Empty K columns are skipped, so an object with only K1/K2 gets two crosses and one link. |
|
||||
| `--track` | — | off | Additionally write the finer Separator/Ausschleus/Einschleus/Weiche graph described above (`<name>_track.dot`, and `<name>_track.svg` with `--tosvg`). Does not change the normal `_material_flow.*` output. See `doc/track.md`. |
|
||||
| `--chain-graph` | — | off | Write **one** to-scale "chain-/Lageplan" SVG of the **grobe Objekte** (big items) as `<name>_chain.svg`: every Kreisel split into its Bahn L/R halves (`_kreisel_lane_rects()`, same to-scale split as `--show-bbs`) and every Strecke/Gefällestrecke ("VF"/"GF") drawn as rectangles at their real `Position`/`Boundingbox` coordinates, connected by the already-resolved top-level `graph.edges` (material flow, Kreisel-Umlauf, unresolved). Step 1 of the new chain-graph build-up — no Separator/Scanner/Weiche/Ausschleus-/Einschleuselement and no BTMT Ein-/Ausgang yet; an edge touching a node that isn't drawn (e.g. a station) is simply skipped. Hand-built SVG, no Graphviz. |
|
||||
| `--track` | — | off | Additionally write the finer Separator/Ausschleus/Einschleus/Weiche graph described above (`<name>_track.dot`, and `<name>_track.svg` with `--tosvg`). Does not change the normal `_material_flow.*` output. See `doc/track.md`. Every node has a **mouseover text** (`tooltip`, shown on hover in the SVG): for a separator its `Zuordnung`, the container whose `TrackIds` list it (Kreisel/VF/GF) and the Kreisel lane it lies on by geometry (`lib/kreisel_junctions.py`); for an ILS Weiche the two Kreisel that created it (`Kreisel A`/`B`) and the touch point by geometry; for Beladung/Entladung the Kreisel they sit on. Contradictions between these sources get a line starting with `!` (e.g. `export.csv` 0015: `Zuordnung` Kreisel 0003, lies on Kreisel 0002). |
|
||||
| `--chain-graph` | — | off | Write a "chain-/Lageplan" of the **grobe Objekte** (big items): every Kreisel split into its Bahn L/R halves (`_kreisel_lane_rects()`, same split as `--show-bbs`), every Strecke/Gefällestrecke ("VF"/"GF"), and every Ein-/Ausgabestation (BTMT `Beladung`/`Entladung`, or the generic `Eingang`/`Ausgang` symbol where no BTMT station absorbed it — see `_merge_station_symbols()`) — the plant runs as a closed loop, so these stations are its only material sources/sinks and are drawn like any other node, not skipped. Connected by the already-resolved top-level `graph.edges` (material flow, Kreisel-Umlauf, unresolved). Rendering method and output file are chosen by `--graphtype`. |
|
||||
| `--graphtype` | `coarsebox`\|`compound`\|`subgraph` | `coarsebox` | Only valid together with `--chain-graph` (else exit 1). `coarsebox`: containers only (as above), to real scale, hand-built `<name>_chain.svg`, no Graphviz — the original behaviour. `compound`: the same to-scale picture, **plus** each container's Anbauteile (Separator, Scanner, Ausschleus-/Einschleuselement, Weiche) drawn nested inside it at their real position (`_container_children()` reads them off `Element.track_ids`, the same source `build_track_graph()`/`--track` uses — not a re-derivation from `Nachbarn` like the old `data/gen_chain_svg.py` prototype), linked by the already-resolved `TrackGraph.edges`; still `<name>_chain.svg`, no Graphviz. `subgraph`: the same containment expressed as **nested Graphviz clusters** (`subgraph cluster_*` — one per Kreisel, nested one per Bahn L/R, plus one per Strecke/Gefällestrecke, each containing its Anbauteile as child nodes) — schematic (`dot` auto-layout), **not** to scale; writes `<name>_chain.dot` always, and `<name>_chain.svg` additionally with `--tosvg` (needs Graphviz `dot`, unlike the other two graph types). |
|
||||
|
||||
**Output** (in `%SKEL_RESULTS%`): `<name>_material_flow.dot` (always, with `pos`
|
||||
attributes when `--use-cords` was used), `<name>_material_flow.svg` (with `--tosvg`),
|
||||
`<name>_material_flow.md` (with `--doc`), `<name>_connect.ini` (with `--connect`),
|
||||
`<name>_bbs.svg` (with `--show-bbs`), `<name>_track.dot` / `<name>_track.svg` (with `--track`),
|
||||
`<name>_chain.svg` (with `--chain-graph`).
|
||||
`<name>_chain.svg` (with `--chain-graph --graphtype coarsebox|compound`, or `--graphtype subgraph --tosvg`),
|
||||
`<name>_chain.dot` (with `--chain-graph --graphtype subgraph`).
|
||||
|
||||
**Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed
|
||||
(Graphviz).
|
||||
@@ -269,8 +303,27 @@ draws *their* flow. Derivation rules (see
|
||||
| Gefällestrecke, single line | `1Sep` |
|
||||
| Strecke (driven) | `Vario` (one per segment) |
|
||||
| Kreisel lane | `1Sep` |
|
||||
| Separator before an Ausschleuselement (one / two in a row) | `1Sep1Swi` / `1Sep2Swi` |
|
||||
| Junction between Kreisel: *n* separators directly upstream of *m* touch points on one pulley | `1Sep1Swi`, `2Sep1Swi`, `2Sep2Swi`, `3Sep2Swi` |
|
||||
| — additionally: 2 or 3 outgoing paths | upgraded to `1Sep1Swi` / `1Sep2Swi` |
|
||||
|
||||
**Junctions between Kreisel** (`lib/kreisel_junctions.py`, step 3c in
|
||||
`derive_tros`). Every Kreisel is modelled as a stadium (two pulleys plus two
|
||||
straight lanes, from position, `Drehung` and bounding box) with its direction of
|
||||
travel (`Drehrichtung`). Wherever two Kreisel touch and run the same way, there
|
||||
is a switch. An exported `ILS Weiche` is matched to it by position; a touch point
|
||||
without one gets the ID `W<KreiselA>_<KreiselB>` and a finding (in
|
||||
`export.csv` three are missing: 2/3, 3/4, 8/9). Touch points on the same pulley
|
||||
form one junction (Kreisel 5 touches 4 and 6 with its west pulley → 2 switches).
|
||||
A separator controls a junction if, walking **against** the direction of travel
|
||||
from the junction on one of its Kreisel, it is the first thing reached — before
|
||||
any other switch. The type follows from the count: separators × switches. The
|
||||
host of each separator is the lane it physically lies on, not its `Zuordnung`.
|
||||
If the two Kreisel run against each other at the touch point (both `UZS` at a
|
||||
pulley-to-pulley touch, as in Mubea and Dematic — probably an export error), no
|
||||
junction is formed, a finding is written, and the ILS Weiche is handled by the
|
||||
older neighbour rule (`_find_weiche_separators`, step 3c').
|
||||
|
||||
A scanner mounted on a separator does **not** change the type (stays `1Sep`);
|
||||
the `1Sep_SSCC` special case can't be recognized from the mechanical layout
|
||||
alone and is only raised as a finding. TRO types, FB blocks, sub-components and
|
||||
@@ -282,21 +335,59 @@ colors all come from `lib/tro_catalog.py` — nothing is hard-coded here.
|
||||
| `--tosvg` | — | off | Also render the TRO flow diagram to `.svg`. |
|
||||
| `--use-cords` (alias `--use-coords`) | — | off | Place every TRO at the centroid of the components it was built from and render with `neato -n` (fixed positions) instead of `dot`'s computed layout, so the diagram matches the plant layout. Only has an effect together with `--tosvg`; fails if any TRO has no coordinate. |
|
||||
| `--doc` | — | off | Also write a Markdown report (`<name>_tro_doc.md`) with the TRO list, connections, findings and the full type catalog. |
|
||||
| `--geometry` | — | off | Determine each separator's host and each switch's exit count (fanout) **from the part world-coordinates** via a shapely `STRtree` (`lib/tro_geometry.py`, see §6b) instead of from the CSV `Zuordnung`/`Nachbarn` columns. Use this for exports where those columns are incomplete (the columns are then bypassed entirely for these two inputs). Requires `shapely>=2.1`. |
|
||||
| `--geo-tol-mm` | `MM` (float) | catalog default | Only with `--geometry`: the tolerance in mm within which two connection points count as the same transfer point. |
|
||||
| `--connect` | — | off | Also write the TRO topology as `<name>_tro_connect.ini` (nodes, connections, system boundaries, predecessors/successors per TRO). Pure output, never read back. |
|
||||
| `--true-scale` | — | off | Only with `--use-cords`: draw to scale. By default empty stretches longer than 1500 mm between nodes are shortened (order and neighbourhood are kept). |
|
||||
| `--json` | — | off | Also write the TRO-JSON `<name>_tro.json` (schema `sps_skel-tro-v4`, model `TroDocument` in `lib/model.py`): TROs with their `slots`, system boundaries (`SYSn`), connections with their kind, `jamAreas`, findings. Input for `scl_skeleton.py`. |
|
||||
| `--ids` | `DATEI` | `<name>_tro_ids.ini` | ID file of the plant (name in `%SKEL_CFG%`, or a path). Use it to run another export of the same plant against the same IDs, e.g. `--file mubea_export.csv --ids mubea_tro_ids.ini`. |
|
||||
| `--renumber` | — | off | Number all TROs afresh along the material flow and rewrite the `[tro]` section of the ID file (early project phase). Renamed IDs are lost; other sections of the file are kept. |
|
||||
| `--id-tolerance` | `MM` (float) | `100.0` | Maximum distance between an ID-file entry and a separator of a TRO for the TRO to count as found again. |
|
||||
|
||||
TROs, types and connections always come from the layout alone;
|
||||
`cfg/tro_overrides.ini` is not read. Errors in the derivation are fixed in
|
||||
`lib/tro_flow.py`, not in a correction file.
|
||||
|
||||
**TRO-IDs** (`lib/tro_ids.py`). The IDs belong to the SPS programmers and must
|
||||
survive a new layout export; the layout `TeileId` changes between exports, the
|
||||
position does not. So every run reads `%SKEL_CFG%\<name>_tro_ids.ini`:
|
||||
|
||||
```ini
|
||||
[tro]
|
||||
TRO01 = -400.0, 13920.0 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L | Sep 0003
|
||||
K0302_1 = 10310.0, -440.1 ; renamed by hand
|
||||
```
|
||||
|
||||
- Each entry is `ID = x, y` (mm, CSV coordinates) of the TRO's anchor
|
||||
separator. An entry finds the TRO with a separator within `--id-tolerance`
|
||||
(closest first, one entry per TRO), and that TRO keeps the ID — renamed IDs
|
||||
included. An ID must be unique and a valid SCL identifier; `SYSn` is
|
||||
reserved for system boundaries.
|
||||
- No file yet: the TROs are numbered along the material flow (depth-first from
|
||||
the loading stations over the material-flow graph, on a carousel or Strecke in
|
||||
`TrackIds` order) and the file is created.
|
||||
- A TRO not found gets the next free `TRO<n>` and is appended to `[tro]`;
|
||||
an entry without a TRO is reported ("weggefallen", with the TRO that now
|
||||
holds its separator, e.g. after two TROs merged into a `2Sep2Swi`) and stays
|
||||
in the file; its ID is not reused.
|
||||
- The console report lists found / new / dropped IDs.
|
||||
|
||||
**Output** (in `%SKEL_RESULTS%`): `<name>_tro_flow.dot` (always, with `pos`
|
||||
attributes when `--use-cords` was used), `<name>_tro_flow.svg` (with
|
||||
`--tosvg`), `<name>_tro_doc.md` (with `--doc`), `<name>_tro_errors.md` (only
|
||||
if TROs are unconnected or of an unknown type).
|
||||
`--tosvg`), `<name>_tro_doc.md` (with `--doc`), `<name>_tro.json` (with
|
||||
`--json`), `<name>_tro_errors.md` (only if TROs are unconnected or of an
|
||||
unknown type). In `%SKEL_CFG%`: `<name>_tro_ids.ini` (created, extended or,
|
||||
with `--renumber`, rewritten — see above).
|
||||
|
||||
**Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed
|
||||
(Graphviz) · `3` TRO validation errors found (error file written).
|
||||
|
||||
## 5. `tro_annotate.py` — burn the TROs into a copy of the CAD drawing
|
||||
|
||||
**Viewer only.** The drawing shows the result for a visual check in BricsCAD;
|
||||
it is no longer a data source for the SCL (that is `tro_flow.py --json`).
|
||||
|
||||
Takes the CSV export, the original BricsCAD drawing (DXF) and the TROs
|
||||
derived the same way as `tro_flow.py`, and writes a **copy** of the drawing
|
||||
derived with exactly the same call as `tro_flow.py` (including the separator
|
||||
cross-check from `<name>_raw.json`), and writes a **copy** of the drawing
|
||||
with:
|
||||
|
||||
- one colored marker block per TRO, each type with its own shape (circle,
|
||||
@@ -335,22 +426,31 @@ overridden.
|
||||
| `--legend` | — | off | Draw a legend of the color groups. |
|
||||
| `--marker-size` | `MM` (float) | `300.0` | Marker radius in mm. |
|
||||
| `--text-height` | `MM` (float) | `220.0` | Text height in mm. |
|
||||
| `--layer-prefix` | `P` | `TRO_` | Prefix for the layers this tool creates. |
|
||||
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. |
|
||||
| `--ids` | `DATEI` | `<csv-stem>_tro_ids.ini` | ID file as for `tro_flow.py`, **read only**: the markers carry the same IDs as the TRO-JSON. Without the file the TROs are numbered along the material flow, as `tro_flow.py` would. |
|
||||
| `--offset` | `DX,DY` | — | Set the CSV↔DXF offset by hand instead of deriving/loading it. For a drawing built with dxfmakros (CSV exported from the same drawing) this is `0,0`. |
|
||||
| `--re-register` | — | off | Re-derive the coordinate offset even if a verified one is already cached in `%SKEL_CFG%`. |
|
||||
| `--max-residual` | `MM` (float) | `600.0` | Largest residual error of the coordinate registration that is still accepted; above it the run fails (exit 4). |
|
||||
| `--check` | — | off | Dry run: only analyze and report, write no output file. |
|
||||
| `--emit-lisp` | — | off | Regenerate `cad/tro_types.lsp` (the type list for the `TRO_INSERT`/`TRO_EDIT` dialogs) from `tro_catalog.py` and exit — no `--dxf` needed. Written under `%DXFM_LISP%` if set (next to the `dxfmakros` SSG_LIB modules that provide the dialogs), otherwise under `<SPS_SKEL>/cad`. |
|
||||
|
||||
Layers always carry the prefix `TRO_`. Like `tro_flow.py`, it never reads
|
||||
`cfg/tro_overrides.ini`, so the markers always match the flow diagram.
|
||||
|
||||
**Output** (in `%SKEL_RESULTS%`, unless `--check`): `<name>_annotated.dxf`,
|
||||
`<name>_registration.json` (the coordinate transform used, with its proof).
|
||||
|
||||
**Exit codes:** `0` ok · `1` input/CLI error · `4` coordinate registration
|
||||
could not be proven (or exceeds `--max-residual`) — resolve with `--offset` or
|
||||
by raising `--max-residual`.
|
||||
could not be proven (or its residual error exceeds 600 mm) — resolve with
|
||||
`--offset`.
|
||||
|
||||
## 5a. `tro_extract.py` — read the TROs back out of the drawing into JSON
|
||||
|
||||
> **Parked since 2026-10-09.** The TRO-JSON for `scl_skeleton.py` now comes
|
||||
> from `tro_flow.py --json`. `tro_extract.py` is kept but not extended; its
|
||||
> output (schema `sps_skel-tro-extract-v1`) is **not** accepted by
|
||||
> `scl_skeleton.py` any more, which says so and asks for `tro_flow --json`. If
|
||||
> hand edits in the drawing are ever needed again, it should write the same
|
||||
> `model.TroDocument` format.
|
||||
|
||||
The counter-direction to `tro_annotate.py`:
|
||||
|
||||
```
|
||||
@@ -392,12 +492,18 @@ Last step of the chain:
|
||||
|
||||
```
|
||||
material_flow.py objects -> material flow
|
||||
tro_flow.py flow -> TROs
|
||||
tro_annotate.py TROs -> markers in the drawing
|
||||
tro_extract.py drawing -> JSON
|
||||
tro_flow.py flow -> TROs -> <name>_tro.json (--json)
|
||||
scl_skeleton.py JSON -> FB_Main.scl
|
||||
```
|
||||
|
||||
The JSON is read through `model.TroDocument` and **validated**: an unknown or
|
||||
renamed key, or `items`/`fbType` that do not match the TRO type in
|
||||
`tro_catalog.py`, stops the run with exit code `1` and the validation error —
|
||||
no SCL is written. A file with a different schema (e.g. the old
|
||||
`tro_extract.py` output) gets a short message asking for `tro_flow --json`.
|
||||
Part counts (`items`) and the FB type are taken from the catalog via the TRO
|
||||
type.
|
||||
|
||||
Produces a **skeleton, not a running program**: one `REGION` per TRO with the
|
||||
instance call and every parameter line in the right order, following the real
|
||||
call sites in `doc/TRO_Katalog/scl_templates/*.scl`.
|
||||
@@ -412,7 +518,8 @@ is missed.
|
||||
|---|---|
|
||||
| REGION order (material flow), instance list, FB type per TRO | sensor/actuator addresses (`cInBG`, `MB`, `MA`) |
|
||||
| number of separator / switch / scanner parameter blocks | separator and switch numbers, `cMainTro` |
|
||||
| `nTo1Destinations` when the TRO has exactly one switch | jam areas (`DB_JamArea…`) |
|
||||
| `nTo1Destinations` when the TRO has exactly one switch | jam-area DB names (`DB_JamArea…`) |
|
||||
| number of jam entries (one per own jam area, not per predecessor) and which jam area each exit leads to (`J<Separator>` in the comment) | |
|
||||
| destination list per switch exit (graph reachability) | times marked "pruefen" |
|
||||
| cross-reference to the layout parts (separator numbers, coordinate) | |
|
||||
|
||||
@@ -424,23 +531,286 @@ only auto-filled for a single-switch TRO; where several switches share the
|
||||
exits, how they divide them is a planning decision, so it stays a gap rather
|
||||
than a guess.
|
||||
|
||||
Switches: `--json NAME` (default `export_tro.json`), `--out NAME` (default
|
||||
`<source>_FB_Main.scl`), `--start TRO`. The last one matters while the plant has
|
||||
Switches: `--json NAME` (default `export_tro.json`, looked up in
|
||||
`%SKEL_RESULTS%`; written by `tro_flow.py --json`), `--out NAME` (default
|
||||
`<source>_FB_Main.scl`), `--eplan NAME` (see below), `--start TRO`. The last one matters while the plant has
|
||||
no loading/unloading station: the flow is then a closed loop with no entry, so
|
||||
the order has to be broken somewhere arbitrary. The chosen start and the reason
|
||||
are recorded in the file header.
|
||||
|
||||
**Stations (`SYSn`).** Loading/unloading stations from `tro_flow.py` appear in
|
||||
the JSON under `boundaries` and as `SYSn` neighbours of the TROs, not in
|
||||
`tros`. They get no REGION and no instance.
|
||||
A TRO fed by a `SYSn` is the start of the order ("Materialeingang ueber SYSn"),
|
||||
so `--start` is no longer needed once the export contains a station. In the
|
||||
jam-area comments they are marked "(Station, kein TRO)", and an exit station
|
||||
shows up as a destination in the per-switch lists.
|
||||
|
||||
**Internal structure.** `scl_skeleton.py` works in two stages:
|
||||
`lib/scl_context.py` derives everything from the TRO-JSON into a render context
|
||||
(`build_context` → `SclFile` / `SclTro`: flow order, separator/switch/scanner
|
||||
blocks, jam entries/exits, destination lists, names), and `scl_skeleton.py` only
|
||||
turns that context into text (`render_context`). Every name or address is an
|
||||
`SclName` (value + `todo` flag); today all of them are placeholders with
|
||||
`todo=True`. This split prepares the move to parameterised templates and
|
||||
per-project naming rules described in [`SCL_Templates.md`](SCL_Templates.md).
|
||||
|
||||
**Templates (opt-in, `--templates [DIR]`).** With this switch every TRO whose FB
|
||||
has a template in `lib/scl_templates/<fbType>.scl.j2` (Jinja2) is rendered from
|
||||
it; the others keep the generic text above. The header and the console summary
|
||||
name which FB types came from a template. Templates exist for the seven types
|
||||
with a real reference call site (`1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`,
|
||||
`Vario`, `Vario_workStation`, `PinStore_Auto`); each follows its call site in
|
||||
`doc/TRO_Katalog/scl_templates/` line for line, and every value that is still
|
||||
open carries `TODO(E-Planung)`. The line decisions are proposals to be confirmed
|
||||
by a PLC engineer (`doc/SCL_Templates.md` §6). `2Sep1Swi`, `EmptyCarrBuffer`,
|
||||
`LoadingBoom` and `2Sep2Swi` have no call site yet and stay generic. `--strict` stops (exit `1`, no SCL) if any FB type has no
|
||||
template. Without `--templates` the output is unchanged.
|
||||
|
||||
**Names from the E-planning (opt-in, `--eplan NAME`).** Reads the assignment
|
||||
`<csv>_eplan.json` written by `e_planning.py` (§5c; bare name looked up in
|
||||
`%SKEL_RESULTS%`). For every separator it assigns, the separator sensor
|
||||
(`stSenInSep := "DB_Inputs".Sensors["cInBG0642"]`) and the stopper valve
|
||||
(`xOutStopper<n> => "MB0643"`) are filled in and lose their `TODO(E-Planung)`
|
||||
marker (`SclName.todo = False`); everything else stays open. The header names
|
||||
the E-planning file. An assignment made for a different CSV (`csv_file` ≠ the
|
||||
TRO-JSON's `source`) stops the run with exit `1` — separator numbers are only
|
||||
unique within one export. Without `--eplan` the output is unchanged.
|
||||
|
||||
Tests: `tests/test_scl_context.py` (the derivations one by one),
|
||||
`tests/test_scl_templates.py` (each template rendered with the real values of
|
||||
its reference call site must equal that call site, comments and whitespace
|
||||
ignored) and `tests/test_scl_skeleton_golden.py` (the whole output of mubea and
|
||||
HundM05 byte for byte against `tests/fixtures/scl/*_FB_Main.scl` and
|
||||
`*_FB_Main_templates.scl`, timestamp masked; an intended change is written
|
||||
back with `SKEL_UPDATE_GOLDEN=1`).
|
||||
|
||||
## 5c. `e_planning.py` — assign the E-planning to the layout parts
|
||||
|
||||
The bridge between layout and electrical planning (background and findings:
|
||||
[`E-Planung_Bruecke.md`](E-Planung_Bruecke.md)). The layout knows which
|
||||
separators, switches and outfeed elements exist and where; the E-planning knows
|
||||
what their sensors and valves are called. The I/O list alone has no topology —
|
||||
but the E-planning is drawn as a layer over the hardware layout, every sensor or
|
||||
actuator symbol sits next to its part, and the cable-length tool
|
||||
(`kabellaengen/lib/getpositions.py`) writes those symbols with position and
|
||||
attributes to `<drawing>_positionsdraw.json`.
|
||||
|
||||
```
|
||||
<name>.csv (layout) + <drawing>_positionsdraw.json (E-planning)
|
||||
│
|
||||
▼
|
||||
e_planning.py ──► <name>_eplan.json (EplanDocument, schema sps_skel-eplan-v1)
|
||||
──► <name>_eplan.md (report: assignments, gaps, drives)
|
||||
│
|
||||
▼
|
||||
scl_skeleton.py --eplan <name>_eplan.json
|
||||
```
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Read** `sensors` (field devices) and `schaltschrank_elemente` (cabinet) of
|
||||
the positionsdraw file into `EDevice` (BMK, `VERW`, `BEZEICHNUNG`,
|
||||
`KENNZEICHNUNG` → controller `UH01` / I/O station `KF107`, position). A BMK
|
||||
listed twice keeps the entry that has a `VERW`.
|
||||
2. **Registration** E-planning drawing → CSV with `dxf_registration.py`: the
|
||||
E symbols matching `[Registrierung] anker` (default `IN SEP`, the sensor that
|
||||
sits on the separator) against the CSV separators. Stored under the key
|
||||
`<csv>|<positionsdraw>` in `%SKEL_CFG%\dxf_registration.json` and re-verified
|
||||
on every run; `--re-register` forces a new derivation.
|
||||
3. **Rules** from `%SKEL_CFG%\e_planning.ini`, one section `<kind>.<role>` per
|
||||
rule, applied in file order; each E device is given out at most once, each
|
||||
role of a layout part filled at most once:
|
||||
|
||||
| key | meaning |
|
||||
|---|---|
|
||||
| `layout` | layout kind pattern (default: the part before the dot): `Separator`, `Ausschleus`, `Einschleus`, `Weiche`, `Scanner`, `Beladung`, `Entladung` |
|
||||
| `verw`, `praefix` | pattern(s) for the E attribute `VERW`, BMK prefix (`BG`, `MB`) |
|
||||
| `max_mm` | spatial: maximum distance after registration |
|
||||
| `soll_mm`, `toleranz_mm` | spatial: the symbol always sits this far from the part; candidates are ranked by their deviation from it, not by the smallest distance |
|
||||
| `nummer_von`, `nummer_versatz` | numbering rule: BMK number = number of the given role + offset (Mubea: valve = sensor + 1) — tried first, distance only for the rest |
|
||||
|
||||
Shipped rules (tuned on Mubea): `Separator.sensor`, `Separator.stopper`,
|
||||
`Ausschleus.stau`, `Ausschleus.ventil`, `Weiche.ventil`. `VERW` texts and the
|
||||
numbering rule are project-specific — in the reference SCL (UH01..UH05)
|
||||
sensor and stopper share the number (BG3242/MB3242), i.e. offset `0`.
|
||||
4. **Drives**: motor `MAnnnn` ↔ motor protection `FCnnnn` (same number), for the
|
||||
report.
|
||||
|
||||
Uncertain cases stay open on purpose: a gap is better than a plausible wrong
|
||||
name. The report lists, per role, the layout parts without a match, the field
|
||||
devices without a match, and distance and runner-up distance of every match
|
||||
(close values = check by hand).
|
||||
|
||||
Switches: `--file NAME` (CSV, default `export.csv`), `--eplan NAME`
|
||||
(positionsdraw JSON, required; bare name resolved against `%SKEL_DATA%`, or a
|
||||
full path), `--rules FILE` (default `%SKEL_CFG%\e_planning.ini`),
|
||||
`--re-register`. Exit `0` also when not everything was assigned; `1` on input
|
||||
errors or when the registration cannot be proven.
|
||||
|
||||
**Today only separator sensor and stopper reach the SCL.** Jam sensors and
|
||||
outfeed/switch valves are assigned in the JSON/report, but the TRO-JSON does not
|
||||
yet say which outfeed element or switch belongs to which TRO exit, so
|
||||
`scl_skeleton` cannot place them. The same holds for the storage lines of a
|
||||
`PinStore` TRO (`arInSep[n]`): their sensors are assigned but the generator does
|
||||
not emit per-line parameters yet.
|
||||
|
||||
Result on Mubea (`mubea.csv` + `Mubea_positionsdraw.json`): registration
|
||||
dx +50613.6 / dy +8555.8 mm; separator sensor 21/38, stopper 22/38 (all 20
|
||||
storage lines exact, 0–1 mm), jam sensor 20/23, outfeed valve 20/23, switch
|
||||
valve 1/1. The open separators are on the carousels K0102/K0104/K0105, where
|
||||
the E-planning drawing and the CSV apparently differ.
|
||||
|
||||
Tests: `tests/test_e_planning.py` (reading, rules, numbering vs. distance on a
|
||||
storage-line grid, target distance, uniqueness, registration, drives, the path
|
||||
into `scl_context` and `scl_skeleton --eplan`).
|
||||
|
||||
> **Since 2026-10-15** the E model (`EDevice`, `load_positionsdraw`,
|
||||
> `pair_drives`) lives in `hw_import.py` and the assignment logic (rules,
|
||||
> spatial, numbering rule, registration) in `merge_hw_layout.py`; `e_planning.py`
|
||||
> imports and re-exports them and keeps only `EplanDocument`, the CSV path,
|
||||
> report and CLI. Results are unchanged (Mubea: 43 names in the SCL). It stays
|
||||
> until `scl_skeleton` reads `<csv>_anlage.json`. The stored registration now
|
||||
> carries a SHA-256 of the positions file: a new file with the same name is
|
||||
> registered anew instead of silently reusing the old offset. `--eplan` (and
|
||||
> every `--file`-style input) also resolves a relative sub-folder against
|
||||
> `%SKEL_DATA%` (`Mubea/Mubea_positionsdraw.json`).
|
||||
|
||||
## 5d. `hw_import.py` — read the hardware planning (TIA import + positions file)
|
||||
|
||||
Stage 2 of [`Konzept_Layout_Hardware_SCL.md`](Konzept_Layout_Hardware_SCL.md).
|
||||
Two sources are enough (finding in [`E-Planung_Bruecke.md`](E-Planung_Bruecke.md),
|
||||
"Nachprüfung 2026-10-15"); the EA list and the WSCAD import add nothing once a
|
||||
positions file exists.
|
||||
|
||||
```
|
||||
*_TIA.xlsx (PLC Tags + Constants) ─┐
|
||||
├─► hw_import.py ──► <project>_hw.json (HardwareDocument, sps_skel-hw-v1)
|
||||
*_positionsconv.json (optional) ────┘ ──► <project>_hw.md (report: devices, drives, checks)
|
||||
```
|
||||
|
||||
- **TIA import**: sheets are recognised by their header row, not their name
|
||||
(English and German TIA exports): a sheet with `Logical Address` /
|
||||
`Logische Adresse` holds tags, one with `Value` / `Wert` constants. Read with
|
||||
`zipfile` + XML — no openpyxl dependency.
|
||||
- **Per BMK one `HwDevice`** (an `EDevice` plus): `direction` (`in`/`out` from
|
||||
`%E`/`%I` vs. `%A`/`%Q`), `address`, `dataType`, `comment`, the index constant
|
||||
`constant` (`cInBG0532` / `cOutMB0533` — what the SCL references) with its
|
||||
`constantValue`, `conveyor` + `conveyorKind`, and `sources` (`tia`,
|
||||
`positions`).
|
||||
- **Conveyor from the plain text**: `KREISEL-K0102`, `KR-K0104` → `K0102`,
|
||||
`K0104` (kind `Kreisel`); `CONVEYOR-#103` and `CONVEYOR-K0103` are the same
|
||||
conveyor and both become `K0103` (kind `Strecke`) — the four-digit conveyor
|
||||
number of the SCL naming scheme (`DB_JamArea0103`, `fbCarousel0102`).
|
||||
- **Drives**: motor `MA` ↔ motor protection `FC` (same number), conveyor and
|
||||
kind from their texts, encoder `GF…` of the same conveyor.
|
||||
- **Cross-check** (`checks`, also printed as `?` lines): BMK only in the
|
||||
positions file / field device (`BG`/`MB`/`MA`/`BX`) only in TIA, TIA comment ≠
|
||||
`BEZEICHNUNG`, signal without index constant, index constant without signal,
|
||||
duplicate addresses, duplicate constant values, field device without
|
||||
controller in `KENNZEICHNUNG`. The positions file's own `warnings` are passed
|
||||
through.
|
||||
|
||||
Switches: `--tia NAME` (required), `--positions NAME` (optional; without it no
|
||||
positions), `--project NAME` (default from the positions file name:
|
||||
`Mubea_positionsconv.json` → `Mubea`). Names resolve against `%SKEL_DATA%`,
|
||||
also with a sub-folder. Exit `0` (findings are in the report), `1` on input or
|
||||
read errors.
|
||||
|
||||
Result on Mubea (`mubea-1_TIA.xlsx` + `Mubea_positionsconv.json`): 238 devices,
|
||||
237 in both sources, 237 index constants, 158 field devices with position,
|
||||
11 drives all with motor protection and conveyor (encoder `GF20000` at K0102);
|
||||
checks: `QA7000` only in the positions file, `BG0514` without controller.
|
||||
|
||||
Tests: `tests/test_hw_import.py` (xlsx reader with shared/inline strings,
|
||||
header-based sheet detection, conveyor texts, merge per BMK, drives, every
|
||||
cross-check, TIA-only run, JSON round trip, CLI with a sub-folder in
|
||||
`%SKEL_DATA%`). The test builds its own small TIA xlsx.
|
||||
|
||||
## 5e. `merge_hw_layout.py` — merge layout and hardware planning into the plant JSON
|
||||
|
||||
Stage 3 of the concept. Reads **no CSV**: the TRO-JSON carries the TRO slots
|
||||
and, since 2026-10-15, the positions and bounding boxes of the layout parts
|
||||
(`parts`, written by `tro_flow --json`).
|
||||
|
||||
```
|
||||
<name>_tro.json + <project>_hw.json
|
||||
└──────┬──────┘
|
||||
merge_hw_layout.py rules %SKEL_CFG%\e_planning.ini, corrections %SKEL_CFG%\hw_overrides.ini
|
||||
│
|
||||
├─► <name>_anlage.json (AnlageDocument, sps_skel-anlage-v1:
|
||||
│ layout unchanged + bindings[] + conveyors[])
|
||||
└─► <name>_anlage.md (report)
|
||||
```
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Registration** E drawing → layout as in §5c (`IN SEP` sensors against the
|
||||
layout separators), stored with the SHA-256 of the positions file. Warning
|
||||
when fewer than `registration_min_share` of the separators find a partner —
|
||||
a sign that layout and E drawing are different planning states.
|
||||
2. **Rules** from `e_planning.ini`, unchanged logic (spatial with maximum /
|
||||
target distance, numbering rule `MB = BG + 1`).
|
||||
3. **Slots**: each assignment goes to the slot role of the TRO that holds the
|
||||
part — key **TRO-ID + slot + role**. A `PinStore_Auto` now has a slot per
|
||||
storage line (`line1` … `lineN`, same split as `scl_context._store`);
|
||||
`separator1` and `line1` can be the same part and both get the binding.
|
||||
Parts without a slot (outfeed elements, switches — `tro_flow` does not keep
|
||||
them on the TRO yet) stay visible as `ohne_slot`.
|
||||
4. **Conveyors**: the E-planning names (`K0102`) and the layout names
|
||||
(`Kreisel3`) do not match, so every field device whose text names a conveyor
|
||||
votes for the nearest layout conveyor (distance to its bounding box, at most
|
||||
`conveyor_max_mm`). Only conveyors of the named kind vote — the bounding
|
||||
boxes overlap (storage lines reach the Kreisel). Assigned at
|
||||
`conveyor_min_votes` votes and `conveyor_min_share` share; one E conveyor per
|
||||
layout conveyor. This gives motor, motor protection and encoder per layout
|
||||
conveyor (`AnlageDocument.conveyor_for("0010-R")` → the TROs on that
|
||||
Kreisel, i.e. `xInCarouselRun`) and fills `drive.motor` /
|
||||
`drive.motorProtection` of Vario TROs.
|
||||
5. **Corrections** from `hw_overrides.ini` (last):
|
||||
`[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and
|
||||
`[<csv>.conveyors] K0102 = 0010`.
|
||||
6. **Status** per binding: `zugeordnet`; `offen` (required role without BMK →
|
||||
later `TODO(E-Planung)`; optional `?` roles are not listed); `widerspruch`
|
||||
(BMK not in the TIA import, unknown, or given to two parts); `ohne_slot`.
|
||||
Every binding carries `constant` and `address` from the TIA import.
|
||||
|
||||
Switches: `--tro NAME`, `--hw NAME` (both required, resolved against
|
||||
`%SKEL_RESULTS%`), `--rules FILE`, `--overrides FILE`, `--re-register`. Exit
|
||||
`0` also when not everything was assigned; `1` on input errors, an old
|
||||
TRO-JSON without `parts`, or an unprovable registration. Parameters:
|
||||
`cfg/parameter.ini [merge_hw_layout]`.
|
||||
|
||||
Result on Mubea (`mubea.csv` → `mubea_tro.json`, `Mubea_hw.json`): registration
|
||||
dx −151.4 / dy −696.6 mm (23/38, warning); 45 slot roles assigned (all 20
|
||||
storage-line sensors, 19 stoppers, separator sensor/stopper at TRO03/04/05/18),
|
||||
58 open, 0 contradictions; 41 assignments at outfeed elements/switch without a
|
||||
slot; conveyors **K0102 = Kreisel3 (21/23 votes), K0104 = Kreisel2 (8/8),
|
||||
K0105 = Kreisel1 (2/3)**, each with motor and motor protection. The other
|
||||
conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or
|
||||
have a single vote and stay open.
|
||||
|
||||
**Not yet:** `scl_skeleton` still reads `<csv>_eplan.json` (`--eplan`);
|
||||
switching it to `<csv>_anlage.json` is stage 4 of the concept, and only then
|
||||
`e_planning.py` becomes the thin wrapper and is removed.
|
||||
|
||||
Tests: `tests/test_merge_hw_layout.py` (sensor spatial / stopper by number,
|
||||
contradiction, open roles, part without slot, conveyor majority with
|
||||
overlapping boxes, Vario drive, too few votes, corrections incl. moving a BMK
|
||||
and clearing a conveyor, storage-line slots, missing `parts`, JSON round trip,
|
||||
fingerprint of the stored registration, CLI end to end with a real offset).
|
||||
|
||||
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
|
||||
|
||||
The single source of truth for everything needed to generate or draw a TRO
|
||||
type: short name (as used in `FB_Main`/`connect.ini`), Siemens FB block,
|
||||
sub-components and their counts, color group (for Graphviz/Mermaid/CAD) and
|
||||
CAD marker shape. **Eleven** types are defined in `TRO_CATALOG`. Ten of them are
|
||||
CAD marker shape. **Twelve** types are defined in `TRO_CATALOG`. Ten of them are
|
||||
verified against `data/*.scl` where the source is vendored locally, and against
|
||||
`TRO_Typen.md`'s mapping table otherwise (see the module docstring,
|
||||
`CATALOG_AS_OF`, for provenance per type).
|
||||
|
||||
> **`2Sep2Swi` is the exception — it is not backed by any SCL.** It was added on
|
||||
> **`2Sep2Swi` and `3Sep2Swi` are the exceptions — they are not backed by any SCL.** It was added on
|
||||
> 2026-08-12 for project 500573 (Mubea), to model the transfer point at the
|
||||
> Kreisel2/Kreisel1 touch as a single control object with 2 separators,
|
||||
> 2 scanners and 2 switches. **`FB_ILS_MTRO_2Sep2Swi` does not exist** in this
|
||||
@@ -448,14 +818,71 @@ verified against `data/*.scl` where the source is vendored locally, and against
|
||||
> the ILSLib — otherwise a future SCL generator will emit a call to a missing
|
||||
> block. The closest verified type is `2Sep1Swi` (2 separators, 2 scanners,
|
||||
> **1** switch), which matches the hardware actually present in that export.
|
||||
> `3Sep2Swi` (3 separators, 3 scanners, 2 switches) was added on 2026-10-14
|
||||
> for the test plant `data/export.csv`, where one Kreisel pulley touches two
|
||||
> others; **`FB_ILS_MTRO_3Sep2Swi` does not exist either.**
|
||||
|
||||
Everything is a Pydantic v2 model
|
||||
(`TroDefinition`/`TroStyle`), so it validates on every assignment and comes
|
||||
with `model_dump()`/`model_validate()` for the planned JSON layout model for
|
||||
free. Consumed by `tro_flow.py` and `tro_annotate.py`; not run directly.
|
||||
free. Consumed by `model.py`, `tro_flow.py`, `tro_annotate.py` and
|
||||
`scl_skeleton.py`; not run directly.
|
||||
|
||||
## 6b. `model.py` — shared model of TRO instances and the TRO-JSON (library, no CLI)
|
||||
|
||||
`tro_catalog.py` says what a TRO **type** is; `model.py` says what one concrete
|
||||
TRO in a plant, and the whole TRO-JSON, look like. It imports only the catalog.
|
||||
|
||||
```
|
||||
tro_catalog.py type level TroDefinition (items, FB block, color, symbol)
|
||||
▲ get_tro()
|
||||
model.py instance level TroBase ─► DerivedTro, Slot, JamArea, Connection,
|
||||
TroCandidate, Analysis, TroDocument
|
||||
▲
|
||||
tro_flow.py (writes) · scl_skeleton.py (reads) · tro_annotate.py (draws)
|
||||
```
|
||||
|
||||
| Class | What it is |
|
||||
|---|---|
|
||||
| `TroBase` | Core of every TRO: `id`, `type`, `confidence`, `separators`, `predecessors`, `successors`, `position` (`{x,y,z}` in JSON). `definition()`, `fb_block` (JSON `fbType`) and `items` are **read from the catalog** on every access, never stored — so they follow a type change and cannot go stale. |
|
||||
| `DerivedTro` | `TroBase` + what `tro_flow.py` knows from the derivation: `label`, `hosts`, `scanners`, `reason`, `colocated`, `positionFrom`, `positionPoints`, `slots`. Also used for the system boundaries `SYSn` (no slots). |
|
||||
| `Slot` | A place on a TRO where hardware belongs: `slot` (`separator1`, `switch1`, `scanner1`, `drive`, `line<n>` for the storage lines of a store — the stable key together with the TRO-ID), `layoutId` (TeileId in the current export, for tracing only; empty for switches, whose element `tro_flow` does not keep yet), `roles` the hardware planning must fill (`sensor`, `stopper`, `jamSensor?` …; `?` = optional). Built by `derive_slots()` from the catalog type. |
|
||||
| `JamArea` | A jam area = the queue in front of a separator, the entry of exactly one TRO: `id` (`J<Separator>`, e.g. `J0042`), `tro`, `entry` (1, 2 for 2Sep types), `separator`, `host`, `predecessors` (all TROs/stations feeding it — merges have several), `note`, `position`. Built by `derive_jam_areas()`; the SCL name `"DB_JamArea<NNNN>".stJam<NNNN>_<k>` needs the conveyor number from the hardware planning and is not stored. |
|
||||
| `Connection` | Directed edge `from` → `to` with its `kind` (transfer / umlauf / weiche / unbestimmt / normal / bahn). |
|
||||
| `TroCandidate` | A spot reported in a review where a TRO is **expected** but not derived (formerly `OpenPoint`): `name`, `type` (the expected catalog type), `position`, `belongsTo` (the TRO that holds the part today), `note`. Not a TRO — no separators, no connections, no SCL. Built by `tro_overrides.py` from `[<csv>.openpoint.<Name>]`; empty in the TRO-JSON for now. |
|
||||
| `Analysis` | Working result of `tro_flow.analyse_tros` (moved here from `tro_flow.py`). |
|
||||
| `TroDocument` | The TRO-JSON: `schema` (`sps_skel-tro-v4`), `source`, `created`, `note`, `tros`, `boundaries`, `connections`, `candidates`, `jamAreas`, `parts`, `findings`. `from_analysis()` (derives slots and jam areas, takes `parts`), `to_json()`, `from_json()`, `jam_areas_or_derived()` (a v3 file without `jamAreas` still renders), `part()` (layout part by TeileId, lane suffix ignored). |
|
||||
| `LayoutPart` | A layout part with position (`layoutId`, `kind`, `name`, `position`, `bbox`) — the target of the spatial assignment in `merge_hw_layout.py`, so that it never reads the CSV. `distance_to_box()`. Optional in v4; a file without `parts` still reads. |
|
||||
| `Binding` | One assignment hardware → slot role: `tro`, `slot`, `role`, `layoutId`, `bmk`, `constant`, `address`, `source`, `rule` (`raeumlich`/`nummer`/`mehrheit`/`hand`), `distance`, `runnerUp`, `status` (`zugeordnet`/`offen`/`widerspruch`/`ohne_slot`), `note`. |
|
||||
| `ConveyorBinding` | A conveyor of the E-planning (`K0102`) at a layout conveyor: `eplanName`, `layoutId`, `layoutName`, `votes`/`total`, `motor`, `protection`, `encoder`, `rule`, `status`, `note`. |
|
||||
| `AnlageDocument` | The plant JSON `<csv>_anlage.json` (`sps_skel-anlage-v1`): `layout` (the TRO-JSON unchanged), `registration`, `bindings`, `conveyors`, `unassigned`, `warnings`. `binding(tro, slot, role)`, `conveyor_for(layout_id)`. Written by `merge_hw_layout.py`; the future single input of `scl_skeleton`. |
|
||||
|
||||
`TroBase` and `TroCandidate` share the `position` handling (JSON `{x,y,z}`,
|
||||
`x`/`y`/`z`, `describe_position()`) through the private mixin `_Positioned`.
|
||||
The same data has the same field name in every class, including
|
||||
`tro_overrides.py`: `tro_id`, `type_name`, `separators`, `scanners`, `hosts`,
|
||||
`label`, `reason`, `confidence`, `position`.
|
||||
|
||||
All models validate on creation and assignment and reject unknown keys
|
||||
(`extra="forbid"`). JSON keys keep the camelCase names of the TRO-JSON (`id`,
|
||||
`type`, `fbType`, `belongsTo` …); Python code uses the field names
|
||||
(`tro_id`, `type_name`, `fb_block`, `belongs_to`) — both are accepted on input.
|
||||
Schema v3 differs from v2 only in `candidates` replacing `openPoints`; v4 adds
|
||||
`slots` per TRO and `jamAreas`. A v3 file still reads (both default to empty;
|
||||
`scl_skeleton` then derives the jam areas while rendering).
|
||||
|
||||
Tests: `tests/test_model.py` (round trip, catalog-backed fields, rejection of
|
||||
wrong keys/items, SCL rendering from a document) — run with
|
||||
`python -m pytest tests/`.
|
||||
|
||||
## 6a. `tro_overrides.py` — manual corrections to the derived TROs (library, no CLI)
|
||||
|
||||
> **Since 2026-10-05 only `material_flow.py --connect` reads this file.**
|
||||
> `tro_flow.py` and `tro_annotate.py` no longer apply corrections: the sections
|
||||
> were keyed on running TRO numbers and went stale with every new export.
|
||||
> Errors in the TRO derivation are fixed in `lib/tro_flow.py` instead. The
|
||||
> format below is kept for `material_flow.py`.
|
||||
|
||||
`tro_flow.py` derives TRO types from the mechanical layout alone. Some things
|
||||
are simply not in the CSV export: a switch that the export does not carry, a
|
||||
scanner that lifts the type to `1Sep_SSCC`, or a spot that a walkthrough
|
||||
@@ -525,47 +952,18 @@ Three kinds of entry, deliberately different in weight:
|
||||
flow node is primary, the rest stay attached so the linking still sees both
|
||||
sides of a transition. Like splits, this renumbers.
|
||||
- **`openpoint`** — a spot reported in a review that is **not** turned into a
|
||||
TRO. The structure stays untouched; the point is only marked, in the doc
|
||||
(section 6) and in the annotated drawing on layer `TRO_OPENPOINT` as a red
|
||||
crossed circle with plain-text label. This keeps "derived" and "asserted"
|
||||
distinguishable until the spot has been checked in BricsCAD.
|
||||
TRO. The structure stays untouched. Read as `model.TroCandidate` (`expect` →
|
||||
`type_name`, `x`/`y`/`z` → `position`, missing `z` = 0).
|
||||
|
||||
Consumers: `tro_flow.py` (type corrections + doc section 6), `tro_annotate.py`
|
||||
(corrected marker types + open-point markers), `material_flow.py` (writes the
|
||||
`[<plant>.manual]` section into `connect.ini`). Unknown TRO IDs and types are
|
||||
not applied silently — they are reported as warnings in the console output and
|
||||
in doc section 6.3, which catches the common trap: **TRO IDs are running
|
||||
numbers of one run and are only valid for the CSV named in the section.** Swap
|
||||
the data basis and the numbers shift.
|
||||
In Python the entries are `TroOverride` (`tro_id` *or* `separator`, from
|
||||
`TRO<n>` / `SEP<n>`), `SplitSpec`/`MergeSpec` (`hosts` is a list; the INI key
|
||||
stays `host`, comma-separated), `model.Connection` for `[<csv>.connections]`,
|
||||
and `model.TroCandidate` — field names as in `model.py`.
|
||||
|
||||
## 6b. `tro_geometry.py` — geometric separator-host and switch-fanout (library, no CLI)
|
||||
|
||||
`tro_flow.py` normally derives two of its inputs from the CSV columns
|
||||
(`material_flow.py::build_graph`):
|
||||
|
||||
```
|
||||
Zuordnung (host of the separator) -> which conveyor/Kreisel a separator sits on
|
||||
Nachbarn / graph edges (fanout) -> how many exits a switch has (1Sep / 1Sep1Swi / 1Sep2Swi)
|
||||
```
|
||||
|
||||
For some exports those columns are incomplete — e.g. `hundmM05_1.csv` reports
|
||||
"nur ein Partner" for several `Strecke` rows, and the older `hundm_export.csv`
|
||||
failed outright (5 TROs, all `Vario`, 0 connections, all isolated) because
|
||||
`Zuordnung`/`Nachbarn` could not be resolved. This module derives the same two
|
||||
values **directly from the part world-coordinates** instead, so the TRO
|
||||
derivation no longer depends on the quality of those two columns:
|
||||
|
||||
- a shapely `STRtree` over the AS/ES chain ends (K1/K2) of every
|
||||
Strecke/Gefällestrecke plus the two Kreisel lane-ends;
|
||||
- `resolve_host_geometric` — `.nearest()` to find the closest host to a
|
||||
separator;
|
||||
- `count_geometric_exits` — `.query(..., predicate="dwithin")` to find every
|
||||
other chain that actually meets at a given point (within a tolerance, mm).
|
||||
|
||||
It is activated by `tro_flow.py --geometry` (via `make_geometric_resolvers`);
|
||||
`--geo-tol-mm` sets the meeting tolerance. This module knows nothing about
|
||||
`tro_flow.py` — only `material_flow.py::Element` lists. Requires `shapely>=2.1`
|
||||
(pinned in `requirements.txt`, needed only for this path).
|
||||
Consumer: `material_flow.py --connect` (writes the `[<plant>.manual]` section
|
||||
into `connect.ini`). Keep in mind: **TRO IDs are running numbers of one run and
|
||||
are only valid for the CSV named in the section.** Swap the data basis and the
|
||||
numbers shift.
|
||||
|
||||
## 7. `dxf_registration.py` — CSV↔DXF coordinate registration (library, no CLI)
|
||||
|
||||
@@ -598,6 +996,31 @@ shows a larger `residual_max` than either role alone. That is expected; the
|
||||
mean residual is the number to watch, and only pairs within `FIT_TOL` (50 mm)
|
||||
enter the Procrustes fit.
|
||||
|
||||
## 7a. `parameter.py` — numeric settings from `cfg/parameter.ini` (library, no CLI)
|
||||
|
||||
Tolerances, thresholds, scales and drawing sizes of the libraries are not
|
||||
written in the code but read from **`cfg/parameter.ini`**: one section per
|
||||
module, the key is the constant's name in lower case
|
||||
(`[kreisel_junctions] contact_tolerance` → `kreisel_junctions.CONTACT_TOLERANCE`).
|
||||
Meaning and unit of every value are documented as comments in the INI file.
|
||||
|
||||
- The base file in the repository is read first and must be complete — a
|
||||
missing key or a non-number stops the import with `ParameterError`; there are
|
||||
deliberately no fallback values in the code.
|
||||
- A second `parameter.ini` in `%SKEL_CFG%` (when it points somewhere else)
|
||||
overrides single values, e.g. a different contact tolerance for one plant.
|
||||
- Read by `kreisel_junctions`, `tro_flow`, `tro_ids`, `material_flow`,
|
||||
`dxf_registration`, `tro_annotate` and `e_planning` (fallback for
|
||||
`toleranz_mm` in `e_planning.ini`). `tests/test_parameter.py` checks that
|
||||
every key is read by its module.
|
||||
- Not in the INI, on purpose: format constants (`KX_TRANS_FAKTOR`, the coding
|
||||
of the CSV columns K1–K4), mathematical epsilons, and style tables
|
||||
(colours, fonts, `COORD_SYMBOL_SIZE`, symbol proportions in the drawing
|
||||
code).
|
||||
|
||||
Changing a value that drives the derivation (a tolerance or threshold) can
|
||||
change TRO types and IDs — compare the outputs of all plants afterwards.
|
||||
|
||||
## 8. `gen_tro_graphs.py` — documentation image generator (doc tooling, not part of the generator)
|
||||
|
||||
Lives under `doc/TRO_Katalog/tro_graphs/`, not `lib/` — it produces the
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
# SCL Generation from Parameterised Templates — Concept
|
||||
|
||||
**Status:** steps 1–3 and 5 of §11 done — templates for all 7 types with a reference call
|
||||
site (`1Sep`, `1Sep1Swi`, `1Sep2Swi`, `1Sep_SSCC`, `Vario`, `Vario_workStation`,
|
||||
`PinStore_Auto`) usable with `--templates`, default output unchanged; the line decisions
|
||||
(§6) are proposals still to be confirmed by a PLC engineer · **As of:** 2026-10-12
|
||||
|
||||
This is the plan for moving `scl_skeleton.py` from hard-coded SCL (Python f-strings in
|
||||
the `emit_*` functions) to **parameterised templates**: the SCL structure for each FB
|
||||
lives in a template file, and Python only supplies the data.
|
||||
|
||||
It is written so that it fits the existing main path unchanged:
|
||||
|
||||
```
|
||||
export.csv ─► tro_flow --json ─► <name>_tro.json ─► scl_skeleton ─► <name>_FB_Main.scl
|
||||
(dxfmakros (derive) (TroDocument, (render) (skeleton with
|
||||
EXPORTCSV) sps_skel-tro-v4) TODO(E-Planung))
|
||||
```
|
||||
|
||||
Nothing upstream of `scl_skeleton` changes. The TRO-JSON stays the only input, and the
|
||||
CLI, output file name, `TODO(E-Planung)` marker and exit codes stay compatible. A
|
||||
workflow that calls the chain today keeps working, and gets the new behaviour only by
|
||||
passing the new optional switches.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why
|
||||
|
||||
Today the knowledge about each FB call site is copied into Python by hand. That has
|
||||
two problems:
|
||||
|
||||
1. **It drifts.** Compared with the real call site of `FB_ILS_MTRO_1Sep1Swi` (TRO107),
|
||||
the generated code already differs: the priority-manager call parameters
|
||||
(`stInPriorityManagerDir2`/`stOutPriorityManagerDir2`, `stInSettings.xPrioDir2`),
|
||||
`xInCarouselRun1` and the jam sensors `stInSenJam2/3` are missing; the separator times
|
||||
differ (600/200 ms instead of 100/50 ms); the parameter order differs.
|
||||
2. **It is in the wrong place for the people who own it.** A PLC engineer can correct
|
||||
an SCL file; they will not edit Python f-strings.
|
||||
|
||||
With templates, the SCL structure is SCL again, the reference call sites become testable,
|
||||
and the project-specific names can be plugged in through a config (§7).
|
||||
|
||||
---
|
||||
|
||||
## 2. Reference material that exists today
|
||||
|
||||
`doc/TRO_Katalog/scl_templates/` holds two **different kinds** of files. Both stay
|
||||
**read-only** (see `CLAUDE.md`); they are the reference, not the templates we render.
|
||||
|
||||
| Kind | What it is | Use in this concept |
|
||||
|---|---|---|
|
||||
| **Call site** (43–138 lines) | A real `REGION` from an `FB_Main`: parameter lines plus the instance call, with real values (`TRO107`, `cSep107.1`, `MB3306`) | Source for the parameterised template **and** the expected output of its round-trip test (§8.1) |
|
||||
| **FB source** (800–1550 lines) | The full `FUNCTION_BLOCK` with `VAR_INPUT`/`VAR_OUTPUT`/`VAR_IN_OUT` | The FB **interface**; every parameter a template writes must exist there (§8.2) |
|
||||
|
||||
Coverage per catalog type (`lib/tro_catalog.py`):
|
||||
|
||||
| Catalog type | FB | Call site | FB source | Template possible |
|
||||
|---|---|---|---|---|
|
||||
| `1Sep` | `FB_ILS_MTRO_1Sep` | yes (TRO 100) | — | ✅ done |
|
||||
| `1Sep1Swi` | `FB_ILS_MTRO_1Sep1Swi` | yes (TRO 107) | — | ✅ done (first pilot) |
|
||||
| `1Sep2Swi` | `FB_ILS_MTRO_1Sep2Swi` | yes (TRO 267) | — | ✅ done |
|
||||
| `1Sep_SSCC` | `FB_ILS_MTRO_1Sep_SSCC` | yes (TRO 105) | — | ✅ done |
|
||||
| `Vario` | `FB_ILS_MTRO_Vario` | yes (TRO 115) | — | ✅ done |
|
||||
| `PinStore_Auto` | `FB_ILS_MTRO_PinStore_Auto` | yes (TRO 208) | — | ✅ done (outlier, largest) |
|
||||
| `Vario_workStation` | `FB_ILS_MTRO_Vario_workStation` | yes (TRO 421, `_CallSite.scl`) | yes | ✅ done (interface check §8.2 still open) |
|
||||
| `2Sep1Swi` | `FB_ILS_MTRO_2Sep1Swi` | **no** | yes | from interface only — needs a real call site |
|
||||
| `EmptyCarrBuffer` | `FB_EmptyCarrBuffer` | **no** | yes | from interface only — needs a real call site |
|
||||
| `LoadingBoom` | `FB_LoadingBoom_INBOUND` | **no** | yes | from interface only — needs a real call site |
|
||||
| `2Sep2Swi` | `FB_ILS_MTRO_2Sep2Swi` | **no** | **no** | no — FB does not exist yet (Mubea needs it) |
|
||||
|
||||
So 7 types can be done with a test against real code. 3 can only be checked against the
|
||||
interface until someone supplies a real call site. `2Sep2Swi` stays on the fallback (§9).
|
||||
|
||||
---
|
||||
|
||||
## 3. Design principles
|
||||
|
||||
1. **Templates hold structure, Python holds data.** No SCL text in Python except the
|
||||
file header and the fallback.
|
||||
2. **Keep the item-based composition.** `doc/HundM/suggestion.md` shows that all
|
||||
`FB_ILS_MTRO_*` share one skeleton and differ only in how many separators, switches,
|
||||
directions and jam areas they repeat. The templates mirror that: shared macros for the
|
||||
repeated blocks, one thin template per FB that arranges them in that FB's order.
|
||||
3. **Never invent a value.** Anything without a source stays `TODO(E-Planung)`, as it
|
||||
does today. A plausible-looking wrong address is worse than an obvious gap.
|
||||
4. **Every value has exactly one origin**, visible in the template (§6): layout, naming
|
||||
rule, reference default, E-Planung, or condition.
|
||||
5. **The TRO-JSON is the contract.** The render context is built only from
|
||||
`model.TroDocument`; templates never read the CSV, the DXF or the catalog directly.
|
||||
6. **Fail loudly in templates.** An undefined variable is an error (Jinja2
|
||||
`StrictUndefined`), not an empty string in the SCL.
|
||||
7. **Additive migration.** The existing `emit_*` code stays as the fallback until every
|
||||
type has a template; with no new switch the output is byte-identical to today.
|
||||
|
||||
---
|
||||
|
||||
## 4. Files and folders
|
||||
|
||||
```
|
||||
sps_skel/
|
||||
├── doc/TRO_Katalog/scl_templates/*.scl reference, read-only (unchanged)
|
||||
├── lib/
|
||||
│ ├── scl_skeleton.py CLI + order + context + render (changed)
|
||||
│ ├── scl_context.py NEW: TroDocument -> render context (pydantic)
|
||||
│ ├── scl_naming.py NEW: reads the naming config, resolves names
|
||||
│ └── scl_templates/ NEW: the parameterised templates
|
||||
│ ├── _macros.scl.j2 shared blocks (separator, switch, jam, call, direction)
|
||||
│ ├── FB_ILS_MTRO_1Sep.scl.j2 ✅
|
||||
│ ├── FB_ILS_MTRO_1Sep1Swi.scl.j2 pilot ✅
|
||||
│ ├── FB_ILS_MTRO_1Sep2Swi.scl.j2 ✅
|
||||
│ ├── FB_ILS_MTRO_1Sep_SSCC.scl.j2 ✅
|
||||
│ ├── FB_ILS_MTRO_Vario.scl.j2 ✅
|
||||
│ ├── FB_ILS_MTRO_Vario_workStation.scl.j2 ✅
|
||||
│ └── FB_ILS_MTRO_PinStore_Auto.scl.j2 ✅ (one per FB, named exactly like fbType)
|
||||
├── cfg/
|
||||
│ ├── scl_naming.ini NEW: default naming rules (from the reference plant)
|
||||
│ └── <project>_scl_naming.ini NEW, optional: per-project override
|
||||
└── tests/
|
||||
├── test_scl_templates.py round trip against the reference call sites ✅
|
||||
├── test_scl_skeleton_golden.py whole-file regression on mubea/HundM05 ✅
|
||||
├── test_scl_context.py the derivations one by one ✅
|
||||
└── fixtures/scl/
|
||||
├── <name>_tro.json fixed inputs (mubea, HundM05)
|
||||
├── <name>_FB_Main.scl expected output, generic
|
||||
└── <name>_FB_Main_templates.scl expected output, with --templates
|
||||
```
|
||||
|
||||
The file frame (header, `VAR` list, REGION loop) stays in Python (`render_context`) for
|
||||
now; only the REGIONs come from templates. A `_file.scl.j2` is only worth it once every
|
||||
REGION does. The reference values of a round trip are set in the test itself
|
||||
(`tests/test_scl_templates.py`) instead of a JSON fixture, so new context fields need no
|
||||
fixture update.
|
||||
|
||||
**Lookup:** the template file name is the TRO's `fbType` from the catalog
|
||||
(`FB_ILS_MTRO_1Sep1Swi` → `FB_ILS_MTRO_1Sep1Swi.scl.j2`). No second mapping table, so the
|
||||
catalog stays the only place that links a type to its FB.
|
||||
|
||||
**Dependency:** `jinja2 >= 3.1` is added to `requirements.txt`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Template structure
|
||||
|
||||
### 5.1 Jinja2 settings
|
||||
|
||||
| Setting | Value | Reason |
|
||||
|---|---|---|
|
||||
| `undefined` | `StrictUndefined` | A misspelled variable stops the run instead of writing empty SCL |
|
||||
| `trim_blocks`, `lstrip_blocks` | `True` | `{% for %}` lines leave no blank lines/indent in the SCL |
|
||||
| `keep_trailing_newline` | `True` | Stable files, clean diffs |
|
||||
| `autoescape` | `False` | SCL, not HTML |
|
||||
|
||||
SCL syntax does not clash with Jinja2: SCL uses `#name`, `{ S7_... }` and `//`, never
|
||||
`{{`, `{%` or `{#`. The one thing to avoid is writing `{#` directly (e.g. `{#TRO…`).
|
||||
|
||||
### 5.2 Shared macros (`_macros.scl.j2`)
|
||||
|
||||
One macro per repeated block. Each takes the TRO context and the block's own data:
|
||||
|
||||
```jinja
|
||||
{% macro separator(t, s) %}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no }}";{{ s.no_note }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := {{ d.sep_trailing }}; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := {{ d.sep_handling }}; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ 'TRUE' if t.has_scanner else 'FALSE' }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor }}"];{{ s.sensor_note }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro jam(kind, j) %}
|
||||
arInOutJam{{ kind }}{{ j.n }} := {{ j.db }}.arCarrier, // {{ j.note }}
|
||||
stInOutJam{{ kind }}{{ j.n }} := {{ j.db }}.stData,
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
`s.no` is either a resolved name (`cSep107.1`) or the placeholder `cSep_____`, and
|
||||
`s.no_note` is then ` // TODO(E-Planung)` or empty. Python decides both (§7.3); the
|
||||
template only places them.
|
||||
|
||||
### 5.3 One template per FB
|
||||
|
||||
Each FB template is the reference call site with values replaced by variables and
|
||||
repeated parts replaced by loops. Sketch for `FB_ILS_MTRO_1Sep1Swi.scl.j2`:
|
||||
|
||||
```jinja
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.comments(t) }}
|
||||
{% for s in t.separators %}{{ m.separator(t, s) }}{% endfor %}
|
||||
{% for w in t.switches %}{{ m.switch(t, w) }}{% endfor %}
|
||||
|
||||
#{{ t.id }}.stInSettings.xPrioDir2 := {{ t.prio_dir2 }};{{ t.prio_dir2_note }}
|
||||
#{{ t.id }}(stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.udt_{{ t.id }}.TroIn,
|
||||
stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.udt_{{ t.id }}.TroOut,
|
||||
nInMainTroNo := "{{ t.main_tro }}",{{ t.main_tro_note }}
|
||||
xInSftyOk := {{ t.safety }},{{ t.safety_note }}
|
||||
xInAllRdyToStart := TRUE,
|
||||
{% if t.carousel_run %}
|
||||
xInCarouselRun1 := {{ t.carousel_run }},
|
||||
{% endif %}
|
||||
xInRelease := {{ t.release }},
|
||||
...
|
||||
{% for j in t.jam_entries %}{{ m.jam("Entr", j) }}{% endfor %}
|
||||
{% for j in t.jam_exits %}{{ m.jam("Exit", j) }}{% endfor %}
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.{{ t.id }},
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["{{ t.main_tro }}"],
|
||||
{% for s in t.separators %}
|
||||
xOutStopper{{ s.n }} => "{{ s.stopper }}"{{ "," if not loop.last or t.switches else ");" }}{{ s.stopper_note }}
|
||||
{% endfor %}
|
||||
...);
|
||||
{{ m.direction(t) }}
|
||||
END_REGION
|
||||
```
|
||||
|
||||
The order of the lines in the template **is** the parameter order of the reference call
|
||||
site. That replaces the order that is hard-coded in `emit_call` today.
|
||||
|
||||
### 5.4 File frame and whitespace
|
||||
|
||||
The file header (source, timestamp, start reason, open-TODO note), `FUNCTION_BLOCK
|
||||
"FB_Main"`, the `VAR` list of all instances and the REGION loop are written by
|
||||
`render_context` in Python. Each REGION is rendered from the TRO's own template and
|
||||
inserted there; TROs without a template get the generic text (§9). With `--templates`
|
||||
the header names which FB types came from a template and which did not.
|
||||
|
||||
Templates do **not** use Jinja whitespace control (`{%- … -%}`) except inside the
|
||||
one-line macros `todo`/`note`. Instead the renderer drops trailing blanks and collapses
|
||||
runs of blank lines (`SclTemplates.render`). That keeps the templates readable as SCL and
|
||||
the indentation intact.
|
||||
|
||||
Rule for every template: **each line that contains a placeholder carries the
|
||||
`TODO(E-Planung)` marker** — also the second line of a pair such as
|
||||
`stInOutJamExit<n> … .stData`. `tests/test_scl_templates.py` checks this.
|
||||
|
||||
---
|
||||
|
||||
## 6. Where each value comes from
|
||||
|
||||
Every line in a template belongs to exactly one of these classes. The class decides who
|
||||
supplies the value and what happens when it is missing.
|
||||
|
||||
| Class | Source | Example | If missing |
|
||||
|---|---|---|---|
|
||||
| **L — layout** | TRO-JSON (`TroDocument`) | `#TRO107`, number of separators/switches, jam entries/exits per neighbour, destination list per switch exit, `nTo1Destinations` (single switch) | cannot be missing — the JSON is validated |
|
||||
| **N — naming rule** | naming config (§7) | `cSep107.1`, `cSwi107.1`, `cMainTro107`, `udt_TRO107` | `TODO(E-Planung)` placeholder |
|
||||
| **R — reference default** | the reference call site, written in the template or in `[defaults]` | `tTrailingTime t#100ms`, `tJam "TimeForSepJam"`, `xPluggedJam TRUE` | always present; marked `pruefen` where plant-specific |
|
||||
| **E — E-Planung** | I/O list (not available yet) | `cInBG3306`, `MB3306`, `MA…`, safety zone, `DB_JamArea0102.stJam0102_1` | `TODO(E-Planung)` placeholder |
|
||||
| **C — condition** | topology flag in the context | `xInCarouselRun1` only next to a carousel; `stInSenJam2/3` only with extra jam sensors | line left out, or written as a TODO |
|
||||
|
||||
Class **C** is the real design work, not the conversion. Each reference call site is one
|
||||
concrete location and contains lines that only exist because of that location. They have
|
||||
to be decided once per template, together with a PLC engineer, and recorded in the
|
||||
template as a comment. For the pilot `1Sep1Swi` (TRO107):
|
||||
|
||||
| Line in reference | Implemented in the pilot | Context field | Status |
|
||||
|---|---|---|---|
|
||||
| `stInSenJam2`, `stInSenJam3` | one per jam exit, sensor `TODO(E-Planung)` | `jam_exits[].sensor` | to confirm |
|
||||
| `stInSettings.xPrioDir2 := TRUE` | written as `TRUE`, marked `TODO(E-Planung)` (priority is a planning decision) | `prio_dir2` | to confirm |
|
||||
| `stIn/OutPriorityManagerDir2` | always; UDT `udt_____` + TODO until the naming rule `udt_{id}` (step 4) | `priority_udt` | to confirm |
|
||||
| `xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed` | **always written, marked TODO "nur am Kreisel, sonst Zeile loeschen"**. The proposed condition (a connection of kind `umlauf`) does not work: `tro_flow` emits `bahn`/`normal`/`weiche`/`transfer` in practice, `umlauf` never occurs in mubea or HundM05, so the TRO-JSON cannot tell whether a TRO sits at a carousel. Needs either a carousel flag from `tro_flow` or a manual decision. | `carousel_run` | to confirm |
|
||||
| `xInRelease := TRUE` (generic: `"DB_TRO_TEST".<id>`) | reference value `TRUE` | — (literal) | to confirm |
|
||||
| `stInOutMachineState := #stInOutControlUnitCabinetOF1` | placeholder `#stInOutControlUnitCabinet` + TODO (cabinet is plant-specific) | `machine_state` | to confirm |
|
||||
| separator times `t#100ms` / `t#50ms` | reference values, marked `pruefen`; `tSenFree`/`tSenWait` of the generic path are **not** written (not in the reference) | — (literal) | to confirm |
|
||||
| `nTo1Destinations := 3` (TRO107 has **2** exits) | number of exits, marked `pruefen` — the reference suggests it counts destinations, not exits (§13) | `switches[].to1_destinations` | open |
|
||||
| `stInPriorityManager.xReleaseOk` (generic only) | not written — not in the reference | — | to confirm |
|
||||
|
||||
The same decisions were made for the other six templates (step 5). Each template's
|
||||
header comment lists them in full; this is the summary of what deviates from "copy the
|
||||
reference line":
|
||||
|
||||
| Template (reference) | Decisions to confirm |
|
||||
|---|---|
|
||||
| `1Sep` (TRO100) | times 600/200 ms and `tSenFree`/`tSenWait` 1500/4000 ms as in the reference (`pruefen`); `xInCarouselRun` always written as TODO; `xInRelease := "DB_TRO_TEST".<id>` as in the reference |
|
||||
| `Vario` (TRO115) | times 800/300 ms, `xPluggedJam FALSE`, `tSenFree`/`tSenWait` 2000/5000 ms, `tRasterHigh/Low` 5 s (`pruefen`); `stInSettings.xIsBorner := TRUE` marked TODO (meaning unclear); `JamEntr` = the TRO's own entry jam area (TRO-JSON v4: several predecessors merge into it), `JamFinger` = the entry jam area of the first successor; `xInErrorQuit`/`xInAutomatic` open |
|
||||
| `Vario_workStation` (TRO421) | `xPluggedScanner FALSE` although the catalog lists a scanner (it sits at the work station, not the separator — `pruefen`); work-station sensor, release and `xBeladenError` sensor open |
|
||||
| `1Sep2Swi` (TRO267) | separator without times, `xPluggedPart TRUE` + `stSenPart` as in the reference; **switch 2 has no `nSwitchNo` in the reference** — written as a commented-out proposal; `nTo1Destinations` per switch open (split of the exits); `bSepType.%X2 := TRUE` marked `pruefen`; one `xInCarouselRun<n>` per switch; **no `FC_Direction`** at this place in the reference — only the destination list as a comment; no `nOutStateLast` (not in the reference) |
|
||||
| `1Sep_SSCC` (TRO105) | second (SSCC) camera with its own interface structure, end-measurement sensor and HMI key open; `xInRelease` written as `"DB_TRO_TEST".<id>` with a TODO (the reference releases via the WCS connection); `FC_CARR_GLOB_CORRECTION` with `nInStation` open; the WCS telegram region of the reference is only noted as TODO |
|
||||
| `PinStore_Auto` (TRO208) | **number of lines from the layout** (one per storage lane, `nNoOfLines`), not the 19 of the reference; all plant constants (encoder, destinations, stations, thresholds, storage DB, removal handshake) open; pin separator times 0/50 ms (`pruefen`); simulation settings `stSimu` of the reference not written; the FB has one `JamEntr`/`JamExit` — since v4 several feeds merge into the one entry jam area (mubea: 2 feeds → 1 entry); a store with more than one entry or exit jam area (mubea: 2 exits) gets a TODO in the REGION header |
|
||||
|
||||
New context fields for these templates (`lib/scl_context.py`): Cognex wiring per scanner
|
||||
(`interface`, `tag`, `hw_results`), `part_sensor` per separator, `carriers`/`data` per jam
|
||||
area (handover jams use their own variables), `jam_in`/`jam_out` (same objects as the
|
||||
first entry/exit), `error_quit`, `automatic`, `is_border`, `correction_station`, and the
|
||||
blocks `sscc`, `work_station`, `store` (with one `SclStoreLine` per lane). The generic
|
||||
path does not use them — its golden files are unchanged.
|
||||
|
||||
Effect on the open-value count with `--templates`: with the pilot only, mubea 282 → 302
|
||||
and HundM05 323 → 377; with all seven templates mubea → 489 and HundM05 → 475
|
||||
`TODO(E-Planung)` (HundM05 now comes completely from templates, mubea except the
|
||||
`2Sep2Swi`). Since TRO-JSON v4 (one entry jam area per TRO instead of one per
|
||||
predecessor) it is: mubea 277 generic → 482 with templates, HundM05 316 → 461. More reference lines are written and every open value is marked; the count
|
||||
drops again with the naming rules (step 4).
|
||||
|
||||
---
|
||||
|
||||
## 7. Naming config
|
||||
|
||||
### 7.1 Purpose
|
||||
|
||||
Some names follow a rule in a given plant (`cSep107.1` = separator 1 of TRO 107). Others
|
||||
do not (`cInBG3306` is whatever E-Planung assigned). The naming config holds the rules,
|
||||
so that:
|
||||
|
||||
- rule-based names are generated instead of left as TODOs,
|
||||
- a project with a different naming scheme changes a file, not code,
|
||||
- anything without a rule stays `TODO(E-Planung)`, exactly as today.
|
||||
|
||||
### 7.2 File and lookup
|
||||
|
||||
INI, like `connect.ini` and the other files in `%SKEL_CFG%`. Lookup order (later wins):
|
||||
|
||||
1. `%SKEL_CFG%/scl_naming.ini` — defaults taken from the reference plant
|
||||
2. `%SKEL_CFG%/<name>_scl_naming.ini` — per project, `<name>` = stem of the TRO-JSON
|
||||
(`mubea_tro.json` → `mubea_scl_naming.ini`)
|
||||
3. `--naming FILE` — explicit file
|
||||
|
||||
A key that is **absent or empty** in all of them means "no rule" → placeholder +
|
||||
`TODO(E-Planung)`.
|
||||
|
||||
### 7.3 Syntax
|
||||
|
||||
```ini
|
||||
; scl_naming.ini - Namensregeln fuer das FB_Main-Geruest
|
||||
; Platzhalter: {id} TRO-ID (TRO107), {nr} Nummernteil (107), {n} laufende Nummer
|
||||
; des Bauteils im TRO (1, 2 ...), {sep} Separatornummer aus dem Layout
|
||||
|
||||
[names]
|
||||
separator_no = cSep{nr}.{n}
|
||||
switch_no = cSwi{nr}.{n}
|
||||
main_tro = cMainTro{nr}
|
||||
priority_udt = udt_{id}
|
||||
|
||||
; ohne Regel -> TODO(E-Planung)
|
||||
sensor_sep =
|
||||
stopper =
|
||||
switch_valve =
|
||||
safety =
|
||||
jam_area =
|
||||
|
||||
[defaults]
|
||||
sep_trailing = t#100ms
|
||||
sep_handling = t#50ms
|
||||
release = TRUE
|
||||
```
|
||||
|
||||
- `{nr}` is the numeric part of the ID **as written** in the JSON (`TRO01` → `01`,
|
||||
`TRO107` → `107`); padding is not changed.
|
||||
- `{sep}` is the separator's layout number from `separators[]` (`0040`) — useful if a
|
||||
project names separators after the layout instead of the TRO.
|
||||
- An unknown placeholder in a rule is an error (exit 1), not a silent literal.
|
||||
|
||||
### 7.4 Later: E-Planung data
|
||||
|
||||
When an I/O list exists in machine-readable form, a further section can point to it
|
||||
(e.g. `[eplanung] file = …csv`) and fill class **E** values per separator/switch. That is
|
||||
a separate step; the config format leaves room for it but this concept does not build it.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tests
|
||||
|
||||
All under `tests/`, run with `python -m pytest tests/` like the existing ones.
|
||||
|
||||
### 8.1 Round trip per template (the main safety net)
|
||||
|
||||
For each template that has a reference call site (`tests/test_scl_templates.py`):
|
||||
|
||||
1. The test builds the context for a TRO with the reference topology and sets the
|
||||
**real** values of the reference (ID `TRO107`, `cSep107.1`, `cInBG3306`, `MB3306`, the
|
||||
real jam DBs, `carousel_run` …).
|
||||
2. The test renders the template with that context.
|
||||
3. It compares the result with the `REGION … END_REGION` part of the reference file,
|
||||
**normalised**: whitespace collapsed, comments removed, case kept.
|
||||
|
||||
If they match, the parameterisation lost nothing: same lines, same order, same values.
|
||||
When someone later replaces a reference file with a newer call site, this test shows
|
||||
exactly where the template must follow.
|
||||
|
||||
### 8.2 Interface check (for FBs with source)
|
||||
|
||||
For `Vario_workStation`, `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom`: parse
|
||||
`VAR_INPUT`/`VAR_OUTPUT`/`VAR_IN_OUT` of the FB source and check that every parameter
|
||||
name the template writes exists there, with the right direction (`:=` for in/in-out,
|
||||
`=>` for out). This covers the types that have no call site.
|
||||
|
||||
### 8.3 Golden files for the whole output
|
||||
|
||||
`tests/test_scl_skeleton_golden.py` renders `mubea_tro.json` and `HundM05_tro.json`
|
||||
(copied into `tests/fixtures/`) and compares with checked-in expected SCL, with the
|
||||
timestamp line masked.
|
||||
|
||||
- **During migration:** the golden files are today's output, so switching the
|
||||
generator internals without enabling templates must give byte-identical files.
|
||||
- **When a template is switched on:** the diff in the golden file is reviewed once
|
||||
(it is the intended change) and then committed as the new golden file.
|
||||
|
||||
### 8.4 Naming and context
|
||||
|
||||
Unit tests for `scl_naming.py` (placeholders, lookup order, empty = TODO, unknown
|
||||
placeholder = error) and `scl_context.py` (jam numbering, destination lists, flags such
|
||||
as `carousel_run` from connection kinds).
|
||||
|
||||
---
|
||||
|
||||
## 9. Fallback and errors
|
||||
|
||||
| Situation | Behaviour | Exit |
|
||||
|---|---|---|
|
||||
| Template exists for the TRO's `fbType` | render template | 0 |
|
||||
| No template (e.g. `2Sep2Swi`) | render with the current `emit_*` code; header lines `// Aus Template : …` / `// Generisch : … (kein Template - von Hand pruefen)` and an entry in the console summary | 0 |
|
||||
| No template **and** `--strict` | stop, write nothing | 1 |
|
||||
| Template uses an undefined variable / syntax error | stop, write nothing, show template file and line | 1 |
|
||||
| Naming rule with unknown placeholder | stop, write nothing | 1 |
|
||||
| Invalid TRO-JSON | unchanged from today | 1 |
|
||||
|
||||
With `--templates` the console summary gets a `Templates` line (implemented); the
|
||||
`Namensregeln` line follows with step 4. Real output for mubea today:
|
||||
|
||||
```
|
||||
Templates = 4 von 5 Typen (generisch: FB_ILS_MTRO_2Sep2Swi)
|
||||
Offene Stellen = 482 (Marker 'TODO(E-Planung)')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. CLI — additive only
|
||||
|
||||
Existing switches (`--json`, `--out`, `--start`) and defaults are unchanged.
|
||||
|
||||
| New switch | Argument | Default | Effect | Status |
|
||||
|---|---|---|---|---|
|
||||
| `--templates` | `[DIR]` (optional) | off; without `DIR`: `lib/scl_templates` | Render REGIONs from the FB templates; types without template stay generic | ✅ |
|
||||
| `--strict` | — | off | With `--templates`: exit 1, write nothing, if any FB type of the plant has no template | ✅ |
|
||||
| `--naming` | `FILE` | lookup per §7.2 | Explicit naming config | step 4 |
|
||||
|
||||
Templates are **opt-in** (`--templates`) until step 6. Without the switch the output is
|
||||
byte-identical to before (golden files). When templates become the default in step 6, a
|
||||
`--no-templates` switch keeps the old path for one release.
|
||||
|
||||
---
|
||||
|
||||
## 11. Migration plan
|
||||
|
||||
Each step ends with all tests green and is usable on its own.
|
||||
|
||||
| Step | Work | Done when |
|
||||
|---|---|---|
|
||||
| 1 ✅ | Golden-file tests on today's output (mubea, HundM05) — `tests/test_scl_skeleton_golden.py` | today's SCL is pinned |
|
||||
| 2 ✅ | `scl_context.py`: build the render context from `TroDocument`; `emit_*` read from it — `tests/test_scl_context.py` | golden files byte-identical (also checked on dematic and showroom, with and without `--start`) |
|
||||
| 3 ✅ | Pilot `FB_ILS_MTRO_1Sep1Swi.scl.j2` + `_macros`; switches `--templates`, `--strict`; class-C table (§6) filled with proposals — **still to be confirmed by a PLC engineer**; round-trip test | round trip against TRO107 passes; only the 1Sep1Swi REGIONs (+ header) differ from the generic golden files |
|
||||
| 4 | `scl_naming.py` + `cfg/scl_naming.ini`; switch `--naming` | naming tests pass; Mubea renders with names instead of TODOs where a rule exists |
|
||||
| 5 ✅ | Remaining call-site templates (`1Sep`, `Vario`, `1Sep2Swi`, `1Sep_SSCC`, `Vario_workStation`, `PinStore_Auto`), each with round trip against its reference (`PinStore_Auto`: the parts the reference abbreviates are left out of the comparison); decisions in §6 — **still to be confirmed by a PLC engineer**. Interface check (§8.2) for `Vario_workStation` still open | every round trip passes; HundM05 fully from templates, mubea except `2Sep2Swi` |
|
||||
| 6 | Templates on by default; new `--no-templates` keeps the old path for one release | golden files updated and reviewed; one import into TIA Portal (as for Mubea) |
|
||||
| 7 | Remove `emit_*` once every type in use has a template (`2Sep2Swi` needs a call site first) | fallback only for types without FB |
|
||||
|
||||
Docs touched along the way: `doc/Python_Scripts.md` §5b, `doc/Commandlines.md` §2.5,
|
||||
`CLAUDE.md` (roadmap), this file.
|
||||
|
||||
---
|
||||
|
||||
## 12. What your workflow can rely on
|
||||
|
||||
For a workflow that runs **export CSV → SCL**:
|
||||
|
||||
**Calls (unchanged):**
|
||||
|
||||
```bat
|
||||
bin\tro_flow.bat --file <name>.csv --json
|
||||
bin\scl_skeleton.bat --json <name>_tro.json [--templates [DIR]] [--strict] [--naming <file>]
|
||||
```
|
||||
|
||||
`--templates` and `--strict` exist today; `--naming` follows with step 4.
|
||||
|
||||
**Inputs:**
|
||||
|
||||
| File | Where | Who writes it |
|
||||
|---|---|---|
|
||||
| `<name>.csv` | `%SKEL_DATA%` | dxfmakros `EXPORTCSV` |
|
||||
| `<name>_tro.json` | `%SKEL_RESULTS%` | `tro_flow --json` (schema `sps_skel-tro-v4`) |
|
||||
| `scl_naming.ini`, `<name>_scl_naming.ini` | `%SKEL_CFG%` | you, once per project (optional) |
|
||||
| `lib/scl_templates/*.scl.j2` | repo | maintained with the PLC engineers |
|
||||
|
||||
**Outputs:** `%SKEL_RESULTS%/<name>_FB_Main.scl` (same name as today).
|
||||
|
||||
**Exit codes:** `0` ok (also when some TROs used the fallback) · `1` input, template or
|
||||
naming error, or a missing template with `--strict`. `tro_flow` keeps its own codes (`3`
|
||||
= TRO validation errors → stop the workflow before `scl_skeleton`).
|
||||
|
||||
**Quality gates a workflow can check:**
|
||||
|
||||
- exit code of each step;
|
||||
- `Offene Stellen` in the summary (count of `TODO(E-Planung)`), e.g. track it per run;
|
||||
- `Templates = x von y` — any TRO on the fallback needs a manual look;
|
||||
- `grep "TODO(E-Planung)"` in the SCL lists every open value with its line.
|
||||
|
||||
**What stays manual:** class **E** values (sensor/actuator addresses, safety signals,
|
||||
jam-area DBs) until an E-Planung export exists, and the `pruefen` timings.
|
||||
|
||||
---
|
||||
|
||||
## 13. Open questions (to clarify with the PLC engineers)
|
||||
|
||||
1. The class-C decisions per template (§6) — starting with `1Sep1Swi`.
|
||||
2. Is the reference naming (`cSep{nr}.{n}`, `cMainTro{nr}`, `udt_{id}`) the standard for
|
||||
new projects, or plant-specific?
|
||||
3. Separator timings: reference values (100/50 ms) or the current skeleton values
|
||||
(600/200 ms) as the default?
|
||||
4. `xInRelease`: `TRUE` as in the reference, or the `"DB_TRO_TEST"` test release used today?
|
||||
5. Real call sites for `2Sep1Swi`, `EmptyCarrBuffer`, `LoadingBoom` — available from
|
||||
another plant?
|
||||
6. `FB_ILS_MTRO_2Sep2Swi` (Mubea): will it be created in the ILSLib, or should the two
|
||||
transfer points be modelled with existing types?
|
||||
7. `nTo1Destinations`: TRO107 has 2 exits but the value 3 ("Info noetig fuer HMI"). Does
|
||||
it count the destinations behind the switch rather than its exits? If so, which
|
||||
destinations — the per-exit target lists in the skeleton could supply them.
|
||||
8. `xInCarouselRun1`: how do we know a TRO sits at a carousel? `tro_flow` would need to
|
||||
mark it (e.g. a carousel neighbour per TRO); today the line is always written as TODO.
|
||||
9. `1Sep2Swi` (TRO267): switch 2 has no `nSwitchNo` and the REGION has no
|
||||
`FC_Direction` call — omission in the reference, or intended (direction elsewhere)?
|
||||
10. Meaning of `stInSettings.xIsBorner` (Vario) and `stInSettings.bSepType.%X2`
|
||||
(`1Sep2Swi`) — always `TRUE`, or depending on the location?
|
||||
11. `PinStore_Auto` with more than one feed/exit (mubea: 2/2): the FB has one
|
||||
`JamEntr`/`JamExit` plus `JamBypass` — how are the others connected?
|
||||
12. `Vario_workStation`: is `xPluggedScanner FALSE` right in general (scanner at the work
|
||||
station, not at the separator)?
|
||||
@@ -1,6 +1,6 @@
|
||||
# TRO Catalog — Building Blocks of the Skeleton Program (UH01–UH05)
|
||||
|
||||
**As of: 2026-07-20**
|
||||
**As of: 2026-10-06**
|
||||
|
||||
This document is the **central image/concept catalog** for all TRO types of the
|
||||
Fortna conveyor system. Goal: one place where, per TRO type, everything the
|
||||
@@ -100,6 +100,55 @@ and line style in both the per-type schematics and the controller graphs:
|
||||
|
||||
**Total: 250 TRO instances across 9 actively used types** (`2Sep1Swi` prepared, never instantiated).
|
||||
|
||||
### Deciding the type — evidence sources and decision order
|
||||
|
||||
Each type section below ends with **Identification criteria** in three evidence layers:
|
||||
|
||||
- **Layout** — what the mechanical drawing / CSV export shows (host object of the
|
||||
separator: Kreisel, Gefällestrecke (GF), driven Strecke (VF); switches, scanners, drives,
|
||||
station labels). Available first, before any electrical planning.
|
||||
- **Topology** — the neighbourhood in the material flow (`connect.ini`: in/out edges and
|
||||
their `kind`, shared JamAreas, typical predecessor/successor types).
|
||||
- **SCL / I/O** — what the call site in `FB_Main` (or the I/O list) proves beyond doubt:
|
||||
which parameters, outputs and sub-blocks the instance has.
|
||||
|
||||
The layout is often ambiguous; the SCL call site never is. Recommended decision order
|
||||
(it matches the derivation rules in `lib/tro_flow.py`):
|
||||
|
||||
| Step | Question | → Type |
|
||||
|---:|---|---|
|
||||
| 1 | Boom with forth/back, foot up/down, tilt + distance sensing, no stopper core? | `LoadingBoom` |
|
||||
| 2 | Group of ≥ 2 parallel gravity lines, one separator each, between an infeed and an outfeed conveyor, with encoder + scanner + returning Vario? | `PinStore_Auto` (one per block) |
|
||||
| 3 | Small multi-line buffer for empty carriers behind an infeed separator (`TROnnn_IN`), one distribution output per line, no encoder/scanner? | `EmptyCarrBuffer` |
|
||||
| 4 | Separator on a **driven** segment (VF, own drive motor)? | `Vario` — or `Vario_workStation` if a manual station (station sensor + release button) sits on it |
|
||||
| 5 | Separator with **3** outgoing paths (two switches in series)? | `1Sep2Swi` |
|
||||
| 6 | Separator with **2** outgoing paths (switch / Ausschleuselement)? | `1Sep1Swi` |
|
||||
| 7 | Separator with barcode **and** SSCC scanner, end measurement and WCS telegram (behind a loading boom)? | `1Sep_SSCC` |
|
||||
| 8 | Everything else with one stopper, one entry, one exit (Kreisel track, single GF line, last separator before re-entry onto the Kreisel) — with or without scanner | `1Sep` |
|
||||
| — | Two separate entry separators + one switch in one node | `2Sep1Swi` — concept only; in production modelled as 2× `1Sep` + `1Sep1Swi` |
|
||||
|
||||
**Summary per type** (details in each type's *Identification criteria*):
|
||||
|
||||
| Type | Deciding feature |
|
||||
|---|---|
|
||||
| `LoadingBoom` | Boom forth/back, foot up/down, tilt and distance sensing (analog), no separator. Always between a `1Sep` with scanner and a `1Sep_SSCC`. |
|
||||
| `PinStore_Auto` | At least 2 parallel gravity lines with one separator each, between an infeed and an outfeed conveyor, plus a returning Vario (loop TRO208 → `1Sep1Swi` TRO203 → `Vario` TRO209 → `1Sep` TRO210 → TRO208). One TRO per block. |
|
||||
| `EmptyCarrBuffer` | Small buffer (5 lines) behind an infeed separator `TROnnn_IN`, one output per line. No encoder, no scanner. Its exit merges with another flow into a `1Sep1Swi` (hence the priority manager). |
|
||||
| `Vario` | Separator on a driven segment with its own motor. The outgoing edge is a `finger` edge. Infeed is often from `EXTERN` (then `isBorner`) or from a switch branch. |
|
||||
| `Vario_workStation` | Like `Vario`, plus a station sensor, release button and load-error sensor. Pattern: `1Sep1Swi → Vario → WS → 1Sep → next switch`. |
|
||||
| `1Sep2Swi` | 3 exits, two switches in series, always with a scanner. |
|
||||
| `1Sep1Swi` | 2 exits (`normal` + `dir2`), a switch or an Ausschleuselement on the Kreisel. Typically part of a chain of switches along the main line. |
|
||||
| `1Sep_SSCC` | Two scanners (barcode + SSCC), end measurement, WCS telegram, directly after the LoadingBoom. Can't be recognized from the mechanical layout alone. |
|
||||
| `1Sep` | Everything with one stopper, one entry and one exit, with or without a scanner. This includes the last separator before re-entry onto the Kreisel and a single gravity line. |
|
||||
| `2Sep1Swi` | Concept only. In production this case is built as two `1Sep` feeding the shared entry of one `1Sep1Swi`. |
|
||||
|
||||
The `PinStore_Auto` rule is confirmed by the SCL call site: it has exactly two conveyor
|
||||
signals, `xInConveyorRunIn := fbSortConv1101` and `xInConveyorRunOut := fbExitConv1103`.
|
||||
|
||||
> **Limitations:** `1Sep2Swi`, `1Sep_SSCC`, `EmptyCarrBuffer` and `LoadingBoom` have only
|
||||
> 2 instances each, so their patterns rest on little evidence. Whether the
|
||||
> `EmptyCarrBuffer` lines are driven or gravity lines is not documented and left open.
|
||||
|
||||
---
|
||||
|
||||
## 1. `1Sep` — Standard Separator
|
||||
@@ -134,6 +183,25 @@ flowchart LR
|
||||
**All instances (96):**
|
||||
TRO100, TRO101_IN, TRO102_IN, TRO103, TRO104, TRO106, TRO108, TRO109, TRO111, TRO113, TRO116, TRO117, TRO122, TRO125, TRO129, TRO130, TRO201, TRO207, TRO210, TRO213, TRO216, TRO219, TRO221, TRO227, TRO230, TRO233, TRO236, TRO239, TRO241, TRO247, TRO250, TRO253, TRO256, TRO259, TRO261, TRO263, TRO265, TRO266, TRO269, TRO274, TRO275, TRO301, TRO304, TRO309, TRO310, TRO311, TRO312, TRO315, TRO316, TRO323, TRO325, TRO326, TRO329, TRO332, TRO335, TRO338, TRO345, TRO347, TRO348, TRO351, TRO354, TRO357, TRO360, TRO367, TRO369, TRO370, TRO373, TRO376, TRO379, TRO382, TRO386, TRO402, TRO404, TRO406, TRO409, TRO412, TRO415, TRO416, TRO417, TRO424, TRO501, TRO503, TRO504, TRO507, TRO509, TRO511, TRO513, TRO515, TRO517, TRO519, TRO521, TRO523, TRO525, TRO526, TRO528, TRO529.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** exactly one separator (`S-SP`) with **no** switch and **no** own drive behind it.
|
||||
Typical hosts: a separator on the (driven) Kreisel track; a single, isolated gravity line
|
||||
(GF) with one separator; the **last** separator of a VF/GF chain right before it re-enters
|
||||
the Kreisel (the Kreisel drives from there — alternating rule in `lib/tro_flow.py`);
|
||||
manual stop points without drive (Aufgabestation, Qualitätskontrolle, fire-door stop
|
||||
TRO402).
|
||||
- **Topology:** one entry JamArea, one `normal` exit. Several predecessors may share its
|
||||
entry JamArea (merge, e.g. TRO266 ← TRO263 + TRO274). Very common right **after a Vario**
|
||||
(incoming `finger` edge): TRO209 → **TRO210** → PinStore TRO208, TRO408 → TRO421 → **TRO409**
|
||||
→ switch TRO410. Also the feeder before a switch chain (TRO424 → TRO405).
|
||||
- **SCL / I/O:** one stopper output (`MB…`), `sensorInSep` (`BG…`), optional `sensorJam2`;
|
||||
`xInCarouselRun` from an `fbCarouselNNNN`; single exit JamArea; **no** `xInSwitchNo`, **no**
|
||||
Vario motor (`MA…`), no switch output.
|
||||
- **Not this type if:** 2/3 outgoing paths → `1Sep1Swi`/`1Sep2Swi`; own drive → `Vario`; SSCC
|
||||
scanner + WCS telegram → `1Sep_SSCC`. A **plain barcode scanner does not change the type**
|
||||
(TRO104, TRO109, TRO113, TRO117, TRO269, TRO406 are `1Sep` + scanner).
|
||||
|
||||
---
|
||||
|
||||
## 2. `1Sep1Swi` — Separator + 1 Switch
|
||||
@@ -169,6 +237,29 @@ flowchart LR
|
||||
**All instances (49):**
|
||||
TRO107, TRO112, TRO114, TRO124, TRO202, TRO203, TRO204, TRO205, TRO206, TRO222, TRO223, TRO224, TRO225, TRO226, TRO242, TRO243, TRO244, TRO245, TRO246, TRO313, TRO314, TRO317, TRO318, TRO319, TRO320, TRO321, TRO322, TRO339, TRO340, TRO341, TRO342, TRO343, TRO344, TRO361, TRO362, TRO363, TRO364, TRO365, TRO366, TRO405, TRO407, TRO410, TRO413, TRO505, TRO510, TRO514, TRO518, TRO522, TRO527.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** separator directly in front of **one** switch (ILS Weiche, serial `W.…`) or an
|
||||
**Ausschleuselement** where carriers leave the Kreisel track: one entry, two exits. Usually
|
||||
with a barcode scanner in front (most UH02/UH04 instances carry `+Scanner(BXnnnn)`), since
|
||||
the routing decision needs the carrier ID.
|
||||
- **Topology:** exactly **two** outgoing edges — one `normal` (exit2/Dir1) and one `dir2`
|
||||
(exit3). Typical patterns:
|
||||
- **Switch chain along a main line**: each switch hands the main flow on via `dir2` to the
|
||||
next switch and diverts via `normal` into a branch, which merges back into the next
|
||||
switch's entry (UH04: TRO405 → 407 → 410 → 413; UH05: TRO505 → 510 → 514 → 518 → 522).
|
||||
The branch is often `1Sep1Swi → Vario → (Vario_workStation) → 1Sep → merge`.
|
||||
- **In front of / behind a PinStore block**: TRO202 feeds PinStore TRO211 via `dir2`;
|
||||
TRO203 takes the PinStore exit and sends carriers to the returning Vario TRO209.
|
||||
- **Merge with priority**: if the `dir2` exit is a JamArea also fed by a second flow
|
||||
(TRO107 → JA0102_2 ← TRO101/TRO106), a `Dir2Manager` priority manager is used.
|
||||
- **SCL / I/O:** in addition to `1Sep`: switch output `sw1ExTo3` (`MB…`), `sensorJam3`,
|
||||
`xInSwitchNo := cSwiNNN.1`, `nTo1Destinations`, two exit JamAreas (`exit2`, `exit3`), and an
|
||||
`FC_Direction` call with the switch number and the carrier source; optional
|
||||
`PriorityManagerDir2In/Out`, `xInPrioDir2`.
|
||||
- **Not this type if:** three exits → `1Sep2Swi`; two separate entry separators →
|
||||
`2Sep1Swi` (concept only).
|
||||
|
||||
---
|
||||
|
||||
## 3. `1Sep2Swi` — Separator + 2 Switches
|
||||
@@ -204,6 +295,22 @@ flowchart LR
|
||||
|
||||
**All instances (2):** TRO267, TRO268.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** one separator followed by **two switches in series** (cascade) → three
|
||||
outgoing tracks; barcode scanner at the entry is always present. Sits at a distribution
|
||||
point where carriers are handed over to other areas (handover JamAreas).
|
||||
- **Topology:** **three** outgoing edges. TRO267 (entry JA1110_4, merge of TRO265 + TRO266):
|
||||
`exit2`/`exit3` → handover JamAreas (`handoverJam5001`/`5000`), `exit4` → `dir3` → TRO275
|
||||
(local loop). Both instances sit on the same loop JA1110 one after the other
|
||||
(TRO267 → TRO275 → TRO268 → `dir3` TRO269).
|
||||
- **SCL / I/O:** `switch1` **and** `switch2` (each with `nTo1Destinations`, `newSwitch`),
|
||||
outputs `sw1ExTo3` + `sw2ExTo3`, `sensorJam2`…`sensorJam4`, exits `exit2`…`exit4`,
|
||||
Cognex scanner block (`cScannerBXnnnn`, `hwResults`, `checkASCII`, timeout),
|
||||
`xInCarouselRun1/2`, `sepType_x2`, `pluggedScanner = true`.
|
||||
- **Derivation in `lib/tro_flow.py`:** a host with 3 outgoing flow edges upgrades `1Sep` →
|
||||
`1Sep2Swi`.
|
||||
|
||||
---
|
||||
|
||||
## 4. `1Sep_SSCC` — Separator + SSCC Scanner
|
||||
@@ -238,6 +345,22 @@ flowchart LR
|
||||
|
||||
**All instances (2):** TRO105, TRO110.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** separator with **two** read points — a normal barcode scanner plus an **SSCC**
|
||||
scanner — and an end-measurement sensor, placed at the **system inbound** directly behind
|
||||
a loading boom. Mechanically it looks like a `1Sep` + scanner; the type is decided by the
|
||||
WCS requirement (dock request), so it **cannot be derived from the mechanical layout
|
||||
alone** (`lib/tro_flow.py` only emits a hint).
|
||||
- **Topology:** 1 in / 1 out. Fixed pattern
|
||||
`1Sep(+Scanner) → LoadingBoom → 1Sep_SSCC → 1Sep`:
|
||||
TRO104 → fbLoadingBoom1 → **TRO105** → TRO106 and TRO109 → fbLoadingBoom2 → **TRO110** → TRO111.
|
||||
- **SCL / I/O:** second Cognex interface (`CognexInterfaceSSCC`, second `hwResults` BX),
|
||||
`sensorEndMeasurement`, WCS `DReqM` telegram fields; release coupled to the WCS
|
||||
connection (`FB_CommunicationWCSBL_DB.fbTeleHandler.xOutConnectionActive`);
|
||||
`pluggedScanner = true`; `xInCarouselRun := true`.
|
||||
- **Not this type if:** only one barcode reader and no WCS telegram → `1Sep`.
|
||||
|
||||
---
|
||||
|
||||
## 5. `Vario` — Chain Conveyor
|
||||
@@ -272,6 +395,27 @@ flowchart LR
|
||||
**All instances (70):**
|
||||
TRO115, TRO120, TRO121, TRO123, TRO126, TRO127, TRO128, TRO131, TRO133, TRO134, TRO200, TRO209, TRO212, TRO215, TRO218, TRO220, TRO229, TRO232, TRO235, TRO238, TRO240, TRO249, TRO252, TRO255, TRO258, TRO260, TRO262, TRO264, TRO270, TRO272, TRO300, TRO302, TRO303, TRO305, TRO306, TRO307, TRO308, TRO324, TRO328, TRO331, TRO334, TRO337, TRO346, TRO350, TRO353, TRO356, TRO359, TRO368, TRO372, TRO375, TRO378, TRO381, TRO400, TRO401, TRO403, TRO408, TRO411, TRO414, TRO418, TRO420, TRO500, TRO502, TRO506, TRO508, TRO512, TRO516, TRO520, TRO524, TRO530, TRO531.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** separator at the start of a **driven** segment (VF — friction/chain drive with
|
||||
its own motor `M.…`, layer `TX200FD`), i.e. host = driven "Strecke". One Vario TRO **per
|
||||
separator**, not per Strecke. In a mixed VF/GF chain the alternating rule applies counted
|
||||
back from the re-entry onto the Kreisel: last separator = `1Sep`, the one before = `Vario`,
|
||||
etc.; a separator whose preceding segment actually loses height (> 30 mm, GF) is `1Sep`.
|
||||
- **Topology:** the **outgoing** edge is of kind `finger` (index transfer to the next TRO,
|
||||
usually a `1Sep`, another `Vario` or a `Vario_workStation`). Incoming typically:
|
||||
- from **EXTERN** — system / PLC infeed (TRO115, TRO200, TRO220, TRO400; `isBorner`);
|
||||
- from a `1Sep1Swi` branch exit (TRO203 → TRO209, TRO407 → TRO408);
|
||||
- from another Vario via `finger` (TRO400 → TRO401, TRO500 → TRO530).
|
||||
As the **returning Vario** of a PinStore block it closes the loop back to the store
|
||||
entry (TRO209 → TRO210 → TRO208).
|
||||
- **SCL / I/O:** Vario motor output (`MA…`) + motor protection (`FC…`), `automatic` and
|
||||
`errorQuit` inputs, Vario sensors `carrInPos` / `finger` / `lastPos` / `jam`, Vario timings
|
||||
(`waitForJog`, `convEmpty`, `rasterHigh/Low`…), optional `xInBorner`; JamAreas `entry` +
|
||||
**`finger`** instead of `exit`; **no** `xInCarouselRun`.
|
||||
- **Not this type if:** station sensor + release button → `Vario_workStation`; separator on
|
||||
an undriven gravity piece → `1Sep`. F2H segments (UH03) are plain `Vario` (comment only).
|
||||
|
||||
---
|
||||
|
||||
## 6. `PinStore_Auto` — Automatic Pin Storage
|
||||
@@ -309,6 +453,31 @@ flowchart LR
|
||||
**All instances (24):**
|
||||
TRO208, TRO211, TRO214, TRO217, TRO228, TRO231, TRO234, TRO237, TRO248, TRO251, TRO254, TRO257, TRO327, TRO330, TRO333, TRO336, TRO349, TRO352, TRO355, TRO358, TRO371, TRO374, TRO377, TRO380.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** a **set of parallel gravity lines (GF, `ABH-GEF`) between two conveyors** —
|
||||
an infeed/sorter conveyor that distributes onto the lines and an outfeed conveyor that
|
||||
collects from them — plus a **returning Vario conveyor** that leads carriers back to the
|
||||
store entry. Each line has its own line separator; at the infeed sit a pin separator, a
|
||||
barcode scanner and an encoder on the infeed conveyor. ≥ 2 lines (`MIN_STORE_LANES` in
|
||||
`lib/tro_flow.py`), up to 19 per block. **One TRO per block** (group of lines), never one
|
||||
per line.
|
||||
- **Topology** (UH02 block 1.1.x, repeated 4× per hall section):
|
||||
- entry fed by a `1Sep1Swi` with scanner (TRO202 `dir2` → TRO211) **and** by the return
|
||||
chain `Vario → finger → 1Sep` (TRO209 → TRO210 → TRO208);
|
||||
- exit → `1Sep1Swi` (TRO208 → TRO203), whose branch leads into the returning Vario
|
||||
(TRO203 → TRO209) — the loop store → switch → Vario → 1Sep → store;
|
||||
- `bypass` edge from the neighbouring block (TRO211 → TRO208, TRO214 → TRO217);
|
||||
- `EXTERN` storage edge (`Storage1.1.1`) = handshake with the storage intelligence.
|
||||
- **SCL / I/O:** two conveyor signals **`xInConveyorRunIn`** (`fbSortConv…`) and
|
||||
**`xInConveyorRunOut`** (`fbExitConv…`) — the SCL proof of "between two conveyors";
|
||||
`arInSep[1..N]` with `nJamLength` per line, `nNoOfLines`, encoder parameters
|
||||
(`fbEncoderGF…`), `arInOutStorageLines := DB_Storage…`, `stMFRStorageRemovalInterface`,
|
||||
`ConveyorStations`, jam bypass, `CognexInterfacePin`, line outputs `MB…` per line, unloading
|
||||
jam sensors per line. No `xInCarouselRun`.
|
||||
- **Not this type if:** a **single** GF line with one separator → `1Sep`; a small manual
|
||||
empty-carrier buffer without encoder/scanner/storage handshake → `EmptyCarrBuffer`.
|
||||
|
||||
---
|
||||
|
||||
## 7. `Vario_workStation` — Manual Work Station
|
||||
@@ -341,6 +510,21 @@ flowchart LR
|
||||
|
||||
**All instances (3):** TRO421, TRO422, TRO423.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** a **driven** Vario segment at a **manual work station** — operator place,
|
||||
release button, station sensor, load-error sensor; station label in the drawing
|
||||
(*Arbeitsplatz*). Without a drive a manual stop is **not** this type (Mubea:
|
||||
Aufgabestation / Qualitätskontrolle → `1Sep`).
|
||||
- **Topology:** both edges are `finger`: in from an upstream Vario, out to a `1Sep` that
|
||||
merges back into the switch chain. Fixed branch pattern (UH04, 3×):
|
||||
`1Sep1Swi → Vario → finger → Vario_workStation → finger → 1Sep → next 1Sep1Swi`
|
||||
(TRO407 → TRO408 → **TRO421** → TRO409 → TRO410).
|
||||
- **SCL / I/O:** all `Vario` parameters **plus** `station.sensor`, `releaseStation`
|
||||
(edge-triggered button, `rTrig_Workstation…`), `loadErrorSensor`; internally
|
||||
`FB_ILS_STRO_Vario_WorkStation` + `FB_CarrAccumulate1Sep`.
|
||||
- **Not this type if:** no station sensor / release button → `Vario`.
|
||||
|
||||
---
|
||||
|
||||
## 8. `EmptyCarrBuffer` — Empty Carrier Buffer
|
||||
@@ -375,6 +559,23 @@ flowchart LR
|
||||
|
||||
**All instances (2):** TRO101, TRO102.
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** a **small multi-line buffer for empty carriers** (5 lines in UH01) branching
|
||||
off the main line, with one distribution switch output per line and an **infeed
|
||||
separator** (`TROnnn_IN`, a borner) in front of it. No encoder, no scanner.
|
||||
- **Topology:** `1Sep → TROnnn_IN (1Sep) → JamBufferNNN → EmptyCarrBuffer`. The `_IN`
|
||||
separators of several buffers sit in series on the same line (TRO100 → TRO101_IN →
|
||||
TRO102_IN). The buffer exit **merges** into the entry of a `1Sep1Swi` together with another
|
||||
flow (TRO101 → TRO112 at JA0102_2, TRO102 → TRO107 at JA0102_1) — hence the priority
|
||||
manager.
|
||||
- **SCL / I/O:** `FB_EmptyCarrBuffer` (not `FB_ILS_MTRO_*`), `numberOfLines`,
|
||||
`storageDb := DB_JamBufferNNN`, `inTro` block (`isBorner`, `bornerDest`, own stopper),
|
||||
`separatorLines[]` with `jamLength` + sensor, `switchLineOutputs` (`MB…` per line),
|
||||
`jamSensors` per line, `exitSensor`, `PriorityManagerIn/Out`.
|
||||
- **Not this type if:** encoder, scanner, storage-removal handshake, two conveyors and a
|
||||
returning Vario → `PinStore_Auto`.
|
||||
|
||||
---
|
||||
|
||||
## 9. `LoadingBoom` — Loading Boom
|
||||
@@ -406,6 +607,20 @@ flowchart LR
|
||||
|
||||
**All instances (2):** fbLoadingBoom1 (→ TRO104), fbLoadingBoom2 (→ TRO109).
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** a telescopic **loading boom** at the system inbound (loading area): boom drive
|
||||
forth/back, foot up/down, tilt and distance sensing. No separator/switch core — the only
|
||||
structurally independent type.
|
||||
- **Topology:** always between a `1Sep` with scanner and a `1Sep_SSCC`; its entry is the
|
||||
stopper JamArea of the preceding TRO (`relatedTro`): TRO104 → **fbLoadingBoom1** → TRO105,
|
||||
TRO109 → **fbLoadingBoom2** → TRO110.
|
||||
- **SCL / I/O:** instance name `fbLoadingBoomN` instead of `TROnnn`; `FB_LoadingBoom_INBOUND`
|
||||
with `FB_Tilt_TEST`, `FB_DistanceFoot`, `FB_BoomWorkStation`; **analog** inputs
|
||||
(`wTiltSensor…`, `wDistanceSensor…`), limit switches boom forth/back (`BG…`) and foot
|
||||
up/down (`SF…`), several motor protections (`FC…`), HMI under `stLoadingBoom` instead of
|
||||
`stTRO`. No `FB_ILS_STRO_Sep` / `FB_ILS_STRO_Switch`.
|
||||
|
||||
---
|
||||
|
||||
## 10. `2Sep1Swi` — Unused (Concept Block)
|
||||
@@ -436,6 +651,20 @@ flowchart LR
|
||||

|
||||
> _BricsCAD reference capture (concept block, not instantiated in production). File: `img/2Sep1Swi_layout.png`._
|
||||
|
||||
**Identification criteria:**
|
||||
|
||||
- **Layout:** two separators, each on its own incoming track, directly in front of **one**
|
||||
switch — merge and divert in a single hand-over point.
|
||||
- **Topology:** two incoming edges into **separate** entry JamAreas, two outgoing edges
|
||||
(`normal` + `dir2`). In the reference plants this situation is always modelled
|
||||
differently: two `1Sep` feeding the **shared** entry JamArea of one `1Sep1Swi`
|
||||
(TRO117 + TRO102 → TRO107 at JA0102_1). So: **do not choose this type** for production
|
||||
code as long as the FB is not finished.
|
||||
- **SCL / I/O:** `separator1`/`separator2` with `stopper1`/`stopper2`,
|
||||
`xInCarouselReq1/2`, `switch1`, `prioDir1…4`, `car2NecRight/Left`.
|
||||
- **Related:** `lib/tro_flow.py` derives a `2Sep2Swi` (Ausschleuselement + ILS Weiche
|
||||
directly in a row on the Kreisel, Mubea) — that FB does not exist in the ILSLib yet either.
|
||||
|
||||
---
|
||||
|
||||
## Controller topology graphs (UH01–UH05)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// REFERENZ: Staubereichs-Datenbausteine (DB_JamArea<NNNN>)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Zwei reale Beispiele, vom SPS-Programmierer zur Verfuegung gestellt (2026-10-13).
|
||||
// Nur Referenzmaterial fuer die spaetere Erzeugung der Staubereichs-DBs
|
||||
// (doc/Konzept_Layout_Hardware_SCL.md, Stufe 4 "Staubereichs-DBs") - nicht ausfuehren,
|
||||
// nicht aendern.
|
||||
//
|
||||
// Aufbau:
|
||||
// - ein DB je Foerderer <NNNN> (4501, 4104)
|
||||
// - je Staubereich auf diesem Foerderer ein Struct stJam<NNNN>_<k>
|
||||
// arCarrier : Array[1.."cJam<NNNN>_<k>"] of "stCarrier" (Groesse ueber PLC-Konstante)
|
||||
// stData : "stJamData"
|
||||
// - im BEGIN-Teil je Staubereich nCapacityMax / nCapacitySet
|
||||
//
|
||||
// Bezug: "DB_JamArea4104".stJam4104_1 ist der Eingangs-Staubereich von TRO 115
|
||||
// (FB_ILS_MTRO_Vario.scl, arInOutJamEntr).
|
||||
//
|
||||
// Offen (mit SPS klaeren): Wert der Konstanten cJam<NNNN>_<k> (= nCapacityMax?),
|
||||
// wann nCapacityMax <> nCapacitySet, Herkunft der Kapazitaet (Laenge / Traegerabstand?),
|
||||
// NON_RETAIN zusammen mit VAR RETAIN beabsichtigt, Beispiel fuer einen Kreisel-DB (_1.._6).
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
DATA_BLOCK "DB_JamArea4501"
|
||||
{ S7_Optimized_Access := 'TRUE' }
|
||||
VERSION : 0.1
|
||||
NON_RETAIN
|
||||
VAR RETAIN
|
||||
stJam4501_1 : Struct
|
||||
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4501_1"] of "stCarrier";
|
||||
stData : "stJamData";
|
||||
END_STRUCT;
|
||||
stJam4501_2 : Struct
|
||||
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4501_2"] of "stCarrier";
|
||||
stData : "stJamData";
|
||||
END_STRUCT;
|
||||
END_VAR
|
||||
|
||||
BEGIN
|
||||
stJam4501_1.stData.Config.nCapacityMax := 5;
|
||||
stJam4501_1.stData.Config.nCapacitySet := 5;
|
||||
stJam4501_2.stData.Config.nCapacityMax := 50;
|
||||
stJam4501_2.stData.Config.nCapacitySet := 50;
|
||||
|
||||
END_DATA_BLOCK
|
||||
|
||||
|
||||
DATA_BLOCK "DB_JamArea4104"
|
||||
{ S7_Optimized_Access := 'TRUE' }
|
||||
VERSION : 0.1
|
||||
NON_RETAIN
|
||||
VAR RETAIN
|
||||
stJam4104_1 : Struct
|
||||
arCarrier { S7_SetPoint := 'False'} : Array[1.."cJam4104_1"] of "stCarrier";
|
||||
stData : "stJamData";
|
||||
END_STRUCT;
|
||||
END_VAR
|
||||
|
||||
BEGIN
|
||||
stJam4104_1.stData.Config.nCapacityMax := 8;
|
||||
stJam4104_1.stData.Config.nCapacitySet := 8;
|
||||
|
||||
END_DATA_BLOCK
|
||||
@@ -477,5 +477,5 @@
|
||||
"to": "TRO05"
|
||||
}
|
||||
],
|
||||
"openPoints": []
|
||||
"candidates": []
|
||||
}
|
||||
+38
-14
@@ -50,6 +50,7 @@ neue Ermittlung.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from collections import Counter
|
||||
@@ -58,6 +59,8 @@ from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from parameter import get_float, get_int
|
||||
|
||||
__all__ = [
|
||||
"Transform",
|
||||
"RegistrationError",
|
||||
@@ -67,6 +70,7 @@ __all__ = [
|
||||
"load_transform",
|
||||
"save_transform",
|
||||
"registration_path",
|
||||
"file_fingerprint",
|
||||
]
|
||||
|
||||
# Rollen fuer den Lagebezug: CSV-Objektart -> Blocknamen im DXF, als Muster
|
||||
@@ -84,18 +88,15 @@ ANCHOR_BLOCKS: dict[str, tuple[str, ...]] = {
|
||||
"Scanner": ("SCAN", "Scanner_*"),
|
||||
}
|
||||
|
||||
# Rastergroesse der Abstimmung in mm
|
||||
VOTE_GRID = 100.0
|
||||
# So viele Kandidaten der Abstimmung werden ausgeglichen und bewertet
|
||||
VOTE_CANDIDATES = 6
|
||||
# Nur Paare bis zu diesem Abstand gehen in den Ausgleich ein (exakte Treffer)
|
||||
FIT_TOL = 50.0
|
||||
# Punkte gelten als zugeordnet, wenn sie nach der Verschiebung so nah liegen
|
||||
DEFAULT_MATCH_TOL = 600.0
|
||||
# So viele exakte Treffer muessen es mindestens sein
|
||||
MIN_INLIERS = 5
|
||||
# Ab dieser Drehung wird gewarnt (Grad)
|
||||
ROTATION_WARN_DEG = 0.05
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [dxf_registration] - Bedeutung
|
||||
# und Einheit stehen dort (lib/parameter.py).
|
||||
_P = "dxf_registration"
|
||||
VOTE_GRID = get_float(_P, "vote_grid")
|
||||
VOTE_CANDIDATES = get_int(_P, "vote_candidates")
|
||||
FIT_TOL = get_float(_P, "fit_tol")
|
||||
DEFAULT_MATCH_TOL = get_float(_P, "default_match_tol")
|
||||
MIN_INLIERS = get_int(_P, "min_inliers")
|
||||
ROTATION_WARN_DEG = get_float(_P, "rotation_warn_deg")
|
||||
|
||||
|
||||
class RegistrationError(RuntimeError):
|
||||
@@ -124,6 +125,10 @@ class Transform(BaseModel):
|
||||
csv_file: str = Field(default="")
|
||||
dxf_file: str = Field(default="")
|
||||
created: str = Field(default="")
|
||||
# Pruefsumme der Zeichnung/E-Datei beim Ermitteln ("" = unbekannt). Der
|
||||
# Schluessel enthaelt nur Dateinamen; ein neuer Stand gleichen Namens wird
|
||||
# daran erkannt (siehe load_transform).
|
||||
fingerprint: str = Field(default="")
|
||||
|
||||
# -- Anwenden ----------------------------------------------------------
|
||||
|
||||
@@ -330,8 +335,25 @@ def _key(csv_file: str, dxf_file: str) -> str:
|
||||
return f"{Path(csv_file).name}|{Path(dxf_file).name}"
|
||||
|
||||
|
||||
def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str) -> Transform | None:
|
||||
"""Geprueften Lagebezug aus der Konfiguration lesen."""
|
||||
def file_fingerprint(path: Path) -> str:
|
||||
"""SHA-256 einer Datei (Hex) - Kennung des Planungsstands."""
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1 << 20), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str,
|
||||
fingerprint: str = "") -> Transform | None:
|
||||
"""Geprueften Lagebezug aus der Konfiguration lesen.
|
||||
|
||||
Mit ``fingerprint``: nur verwenden, wenn der abgelegte Bezug fuer genau
|
||||
diesen Dateistand ermittelt wurde. Ein Eintrag ohne Pruefsumme (aeltere
|
||||
Ablage) oder mit anderer gilt als veraltet -> None, der Aufrufer ermittelt
|
||||
neu. Ohne ``fingerprint`` (z. B. tro_annotate) wie bisher nur ueber den
|
||||
Dateinamen.
|
||||
"""
|
||||
path = registration_path(cfg_dir)
|
||||
if not path.is_file():
|
||||
return None
|
||||
@@ -342,6 +364,8 @@ def load_transform(cfg_dir: Path, csv_file: str, dxf_file: str) -> Transform | N
|
||||
entry = data.get(_key(csv_file, dxf_file))
|
||||
if not entry:
|
||||
return None
|
||||
if fingerprint and entry.get("fingerprint", "") != fingerprint:
|
||||
return None
|
||||
transform = Transform.model_validate(entry)
|
||||
transform.source = "cfg"
|
||||
return transform
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
||||
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
||||
"""
|
||||
e_planning.py - Bruecke Layout <-> E-Planung ueber die Symbolpositionen.
|
||||
|
||||
Das Layout (CSV) sagt, welche Separatoren, Weichen, Aus-/Einschleuselemente
|
||||
es gibt und wo; die E-Planung, wie ihre Sensoren und Ventile heissen
|
||||
(BG0642, MB0643 ...). Die EA-Liste allein kennt keine Topologie. Die
|
||||
E-Planung wird aber als Layer ueber der Hardwareplanung gezeichnet: jedes
|
||||
Sensor-/Aktorsymbol liegt neben dem Bauteil, zu dem es gehoert. Das
|
||||
Kabellaengen-Tool (kabellaengen/lib/getpositions.py) schreibt diese Symbole
|
||||
mit Position und Attributen nach <zeichnung>_positionsdraw.json.
|
||||
|
||||
Hier wird daraus die Zuordnung Layout-Bauteil -> BMK:
|
||||
|
||||
1. E-Bauteile einlesen (`sensors` und `schaltschrank_elemente`).
|
||||
2. Lagebezug E-Zeichnung -> CSV mit lib/dxf_registration.py; Anker sind die
|
||||
Separator-Sensoren der E-Planung gegen die Separatoren des CSV. Der
|
||||
gepruefte Bezug wird in SKEL_CFG/dxf_registration.json abgelegt.
|
||||
3. Regeln aus SKEL_CFG/e_planning.ini anwenden, je Rolle (z. B.
|
||||
"Separator.sensor"): welche Layout-Art, welche E-Verwendung (VERW),
|
||||
welcher Hoechstabstand - oder "Nummer = Nummer des Partners + Versatz"
|
||||
(Ventil MB0643 zum Sensor BG0642). Die Zuordnung ist eindeutig: jedes
|
||||
E-Bauteil und jede Rolle eines Layout-Bauteils wird hoechstens einmal
|
||||
vergeben, die kuerzesten Abstaende zuerst.
|
||||
4. Ergebnis als <csv>_eplan.json (EplanDocument) und Bericht
|
||||
<csv>_eplan.md nach SKEL_RESULTS.
|
||||
|
||||
scl_skeleton.py --eplan liest die JSON und setzt die gefundenen Namen in die
|
||||
SCL ein (SclName mit todo=False). Was keine Regel trifft, bleibt dort
|
||||
TODO(E-Planung) - geraten wird nicht.
|
||||
|
||||
Seit 2026-10-15 liegen das E-Modell (EDevice, load_positionsdraw,
|
||||
pair_drives) in hw_import.py und die Zuordnung (Regeln, raeumlich,
|
||||
Nummernregel, Lagebezug) in merge_hw_layout.py - der neue Weg ueber die
|
||||
Anlagen-JSON (doc/Konzept_Layout_Hardware_SCL.md). e_planning bleibt fuer
|
||||
scl_skeleton --eplan, bis scl_skeleton die Anlagen-JSON liest, und
|
||||
re-exportiert die verschobenen Namen.
|
||||
|
||||
Schichten: e_planning importiert material_flow (CSV), hw_import und
|
||||
merge_hw_layout; scl_context importiert nur das Modell EplanDocument.
|
||||
|
||||
Aufruf ueber bin/e_planning.bat bzw. bin/e_planning.sh.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from dxf_registration import RegistrationError, Transform, file_fingerprint
|
||||
from merge_hw_layout import (
|
||||
LAYOUT_KINDS,
|
||||
RULES_FILE,
|
||||
Assignment,
|
||||
LayoutPoint,
|
||||
Rule,
|
||||
_match,
|
||||
assign,
|
||||
load_rules,
|
||||
resolve_transform,
|
||||
)
|
||||
from hw_import import Drive, EDevice, load_positionsdraw, pair_drives
|
||||
|
||||
__all__ = [
|
||||
"EPLAN_SCHEMA",
|
||||
"EDevice",
|
||||
"LayoutPoint",
|
||||
"Rule",
|
||||
"Assignment",
|
||||
"Drive",
|
||||
"EplanDocument",
|
||||
"load_positionsdraw",
|
||||
"load_rules",
|
||||
"assign",
|
||||
"pair_drives",
|
||||
"layout_points",
|
||||
"resolve_transform",
|
||||
"LAYOUT_KINDS",
|
||||
]
|
||||
|
||||
EPLAN_SCHEMA = "sps_skel-eplan-v1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modell
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EplanDocument(BaseModel):
|
||||
"""Die Zuordnungs-JSON <csv>_eplan.json."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
schema_id: str = Field(default=EPLAN_SCHEMA, alias="schema")
|
||||
created: str = ""
|
||||
csv_file: str = ""
|
||||
eplan_file: str = ""
|
||||
transform: Transform | None = None
|
||||
assignments: list[Assignment] = Field(default_factory=list)
|
||||
drives: list[Drive] = Field(default_factory=list)
|
||||
# E-Bauteile im Feld ohne Zuordnung (BMK)
|
||||
unassigned: list[str] = Field(default_factory=list)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
def for_layout(self, layout_id: str) -> dict[str, Assignment]:
|
||||
"""Rollen eines Layout-Bauteils: {"sensor": Assignment, ...}."""
|
||||
return {
|
||||
a.role.split(".", 1)[-1]: a
|
||||
for a in self.assignments if a.layout_id == layout_id
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> "EplanDocument":
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layout
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def layout_points(elements) -> list[LayoutPoint]:
|
||||
"""Punktartige Bauteile aus den CSV-Elementen (material_flow.Element)."""
|
||||
points = []
|
||||
for e in elements:
|
||||
if e.position is None:
|
||||
continue
|
||||
kind = next((k for k in LAYOUT_KINDS
|
||||
if k.lower() in f"{e.kind} {e.teile_art}".lower()), "")
|
||||
if kind:
|
||||
points.append(LayoutPoint(e.teile_id, kind, e.x, e.y, e.name))
|
||||
return points
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bericht
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def render_report(doc: EplanDocument, rules: list[Rule], points: list[LayoutPoint],
|
||||
devices: list[EDevice]) -> str:
|
||||
out = []
|
||||
add = out.append
|
||||
add(f"# E-Planung -> Layout: {doc.csv_file}")
|
||||
add("")
|
||||
add(f"- E-Planung: `{doc.eplan_file}`")
|
||||
add(f"- Erzeugt: {doc.created}")
|
||||
if doc.transform:
|
||||
add(f"- Lagebezug: {doc.transform.describe()}")
|
||||
add("")
|
||||
|
||||
add("## Zuordnung je Rolle")
|
||||
add("")
|
||||
add("| Rolle | Layout-Bauteile | zugeordnet | davon Nummernregel |")
|
||||
add("|---|---|---|---|")
|
||||
for r in rules:
|
||||
n_pts = sum(1 for p in points if _match(p.kind, r.layout))
|
||||
hits = [a for a in doc.assignments if a.role == r.role]
|
||||
n_num = sum(1 for a in hits if a.rule == "nummer")
|
||||
add(f"| {r.role} | {n_pts} | {len(hits)} | {n_num} |")
|
||||
add("")
|
||||
|
||||
add("## Zuordnungen")
|
||||
add("")
|
||||
add("Abstand nach dem Lagebezug; \"2.\" = Abstand des naechstbesten Kandidaten "
|
||||
"(knapp beieinander = pruefen).")
|
||||
add("")
|
||||
add("| Layout | Art | Rolle | BMK | VERW | Bezeichnung | Abstand mm | 2. | Regel | Station |")
|
||||
add("|---|---|---|---|---|---|---|---|---|---|")
|
||||
for a in doc.assignments:
|
||||
second = "" if a.runner_up is None else f"{a.runner_up:.0f}"
|
||||
add(f"| {a.layout_id} | {a.layout_kind} | {a.role.split('.', 1)[-1]} | {a.bmk} "
|
||||
f"| {a.verw} | {a.bezeichnung} | {a.distance:.0f} | {second} | {a.rule} "
|
||||
f"| {a.controller}-{a.station} |")
|
||||
add("")
|
||||
|
||||
roles_of: dict[str, set[str]] = {}
|
||||
for a in doc.assignments:
|
||||
roles_of.setdefault(a.layout_id, set()).add(a.role)
|
||||
add("## Layout-Bauteile ohne Zuordnung")
|
||||
add("")
|
||||
any_missing = False
|
||||
for r in rules:
|
||||
missing = [p for p in points
|
||||
if _match(p.kind, r.layout) and r.role not in roles_of.get(p.layout_id, set())]
|
||||
if missing:
|
||||
any_missing = True
|
||||
add(f"- **{r.role}**: {', '.join(p.layout_id for p in missing)}")
|
||||
if not any_missing:
|
||||
add("- keine")
|
||||
add("")
|
||||
|
||||
by_bmk = {d.bmk: d for d in devices}
|
||||
add("## E-Bauteile im Feld ohne Zuordnung")
|
||||
add("")
|
||||
if doc.unassigned:
|
||||
add("| BMK | VERW | Bezeichnung | Station |")
|
||||
add("|---|---|---|---|")
|
||||
for bmk in doc.unassigned:
|
||||
d = by_bmk[bmk]
|
||||
add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
|
||||
add("## Antriebe (Motor <-> Motorschutz)")
|
||||
add("")
|
||||
if doc.drives:
|
||||
add("| Motor | Bezeichnung | Motorschutz | Bezeichnung |")
|
||||
add("|---|---|---|---|")
|
||||
for dr in doc.drives:
|
||||
add(f"| {dr.motor} | {dr.bezeichnung} | {dr.protection or '-'} "
|
||||
f"| {dr.protection_bezeichnung} |")
|
||||
else:
|
||||
add("- keine Motoren in der E-Planung")
|
||||
add("")
|
||||
|
||||
if doc.warnings:
|
||||
add("## Hinweise")
|
||||
add("")
|
||||
for w in doc.warnings:
|
||||
add(f"- {w}")
|
||||
add("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def build_document(csv_path: Path, eplan_path: Path, rules_path: Path, cfg_dir: Path,
|
||||
re_register: bool = False
|
||||
) -> tuple[EplanDocument, list[Rule], list[LayoutPoint], list[EDevice]]:
|
||||
"""Kompletter Lauf ohne Ausgabe: CSV + positionsdraw -> EplanDocument."""
|
||||
from material_flow import read_elements # erst hier: Tests ohne CSV brauchen es nicht
|
||||
|
||||
warnings: list[str] = []
|
||||
elements = read_elements(csv_path, warnings)
|
||||
points = layout_points(elements)
|
||||
devices = load_positionsdraw(eplan_path, warnings)
|
||||
rules, anchors = load_rules(rules_path)
|
||||
transform = resolve_transform(points, devices, anchors, cfg_dir, csv_path.name,
|
||||
eplan_path.name, re_register, warnings,
|
||||
file_fingerprint(eplan_path))
|
||||
assignments = assign(points, devices, rules, transform)
|
||||
used = {a.bmk for a in assignments}
|
||||
unassigned = sorted(d.bmk for d in devices
|
||||
if not d.cabinet and d.pos is not None and d.bmk not in used)
|
||||
doc = EplanDocument(
|
||||
created=datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
csv_file=csv_path.name,
|
||||
eplan_file=eplan_path.name,
|
||||
transform=transform,
|
||||
assignments=assignments,
|
||||
drives=pair_drives(devices),
|
||||
unassigned=unassigned,
|
||||
warnings=warnings,
|
||||
)
|
||||
return doc, rules, points, devices
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from material_flow import env_dir, resolve_input
|
||||
|
||||
args = parse_args(argv)
|
||||
csv_path = resolve_input(args.file)
|
||||
eplan_path = resolve_input(args.eplan)
|
||||
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
|
||||
for label, path in (("CSV", csv_path), ("E-Planung", eplan_path),
|
||||
("Regeln", rules_path)):
|
||||
if not path.is_file():
|
||||
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
doc, rules, points, devices = build_document(
|
||||
csv_path, eplan_path, rules_path, cfg_dir, args.re_register)
|
||||
except RegistrationError as exc:
|
||||
print(f"FEHLER: Lagebezug E-Planung -> CSV nicht nachweisbar: {exc}",
|
||||
file=sys.stderr)
|
||||
print(" Passen CSV und E-Zeichnung zusammen? Anker-VERW in "
|
||||
f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr)
|
||||
return 1
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
results.mkdir(parents=True, exist_ok=True)
|
||||
json_file = results / f"{csv_path.stem}_eplan.json"
|
||||
md_file = results / f"{csv_path.stem}_eplan.md"
|
||||
json_file.write_text(doc.to_json(), encoding="utf-8")
|
||||
md_file.write_text(render_report(doc, rules, points, devices), encoding="utf-8")
|
||||
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("E-PLANUNG -> LAYOUT")
|
||||
print("================================================================")
|
||||
print(f"CSV = {csv_path}")
|
||||
print(f"E-Planung = {eplan_path}")
|
||||
print(f"Lagebezug = {doc.transform.describe() if doc.transform else '-'}")
|
||||
for r in rules:
|
||||
n_pts = sum(1 for p in points if _match(p.kind, r.layout))
|
||||
hits = sum(1 for a in doc.assignments if a.role == r.role)
|
||||
print(f" {r.role:<26} {hits:4} von {n_pts}")
|
||||
print(f"Ohne Zuordnung = {len(doc.unassigned)} E-Bauteile im Feld")
|
||||
print(f"Antriebe = {len(doc.drives)} "
|
||||
f"({sum(1 for d in doc.drives if d.protection)} mit Motorschutz)")
|
||||
for w in doc.warnings:
|
||||
print(f" ! {w}")
|
||||
print(f"JSON = {json_file}")
|
||||
print(f"Bericht = {md_file}")
|
||||
print("================================================================")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="e_planning",
|
||||
description="Ordnet die Sensoren/Ventile der E-Planung "
|
||||
"(*_positionsdraw.json des Kabellaengen-Tools) den Bauteilen "
|
||||
"des Layouts (CSV) zu. Ergebnis: <csv>_eplan.json fuer "
|
||||
"scl_skeleton --eplan und ein Bericht <csv>_eplan.md.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Beispiele:\n"
|
||||
" e_planning.bat --file mubea.csv --eplan Mubea_positionsdraw.json\n"
|
||||
" e_planning.bat --file mubea.csv --eplan C:\\pfad\\Mubea_positionsdraw.json "
|
||||
"--re-register\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
|
||||
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
|
||||
)
|
||||
parser.add_argument("--file", default="export.csv", metavar="NAME",
|
||||
help="CSV-Export in %%SKEL_DATA%% (oder ein Pfad). "
|
||||
"Standard: %(default)s")
|
||||
parser.add_argument("--eplan", required=True, metavar="NAME",
|
||||
help="*_positionsdraw.json der E-Planung in %%SKEL_DATA%% "
|
||||
"(oder ein Pfad)")
|
||||
parser.add_argument("--rules", metavar="DATEI",
|
||||
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
|
||||
parser.add_argument("--re-register", action="store_true",
|
||||
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
||||
"einer abgelegt ist")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,784 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
||||
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
||||
"""
|
||||
hw_import.py - Hardwareplanung einlesen: TIA-Import + Positionsdatei -> <projekt>_hw.json.
|
||||
|
||||
Stufe 2 des Ablaufs in doc/Konzept_Layout_Hardware_SCL.md. Zwei Quellen
|
||||
genuegen (Befund doc/E-Planung_Bruecke.md, "Nachpruefung 2026-10-15"):
|
||||
|
||||
* **TIA-Import** ``*_TIA.xlsx`` - Blatt "PLC Tags" (Name, Datentyp,
|
||||
Absolutadresse, Kommentar) und Blatt "Constants" (Index-Konstante
|
||||
``cIn<BMK>``/``cOut<BMK>`` je Signal). Das ist genau das, was im SCL steht
|
||||
("DB_Inputs".Sensors["cInBG0532"], "MB0533") und im TIA-Projekt existieren
|
||||
muss - deshalb die erste Quelle und die Pruefliste fuer das SCL.
|
||||
* **Positionsdatei** ``*_positionsconv.json`` / ``*_positionsdraw.json`` des
|
||||
Kabellaengen-Tools (optional) - je BMK die Lage in der E-Zeichnung, die
|
||||
Verwendung (VERW, z. B. "IN SEP"), Klartext und E/A-Station.
|
||||
|
||||
EA-Liste und WSCAD-Import bringen bei vorhandener Positionsdatei nichts
|
||||
dazu und werden nicht gelesen.
|
||||
|
||||
Ergebnis ``HardwareDocument`` (Schema sps_skel-hw-v1): je BMK ein Geraet mit
|
||||
beiden Seiten, die Antriebe (Motor MA <-> Motorschutz FC, Geber GF) je
|
||||
Foerderer der E-Planung (K0102; "#106" wird K0106), und die Gegenpruefung
|
||||
beider Quellen.
|
||||
Die Zuordnung zum Layout macht merge_hw_layout.py.
|
||||
|
||||
Die xlsx-Datei wird ohne Fremdbibliothek gelesen (zipfile + XML): der
|
||||
TIA-Import ist eine einfache Tabelle, openpyxl waere eine Abhaengigkeit nur
|
||||
dafuer.
|
||||
|
||||
Aufruf ueber bin/hw_import.bat bzw. bin/hw_import.sh.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from dxf_registration import file_fingerprint
|
||||
|
||||
__all__ = [
|
||||
"HW_SCHEMA",
|
||||
"EDevice",
|
||||
"Drive",
|
||||
"TiaTag",
|
||||
"TiaConstant",
|
||||
"HwDevice",
|
||||
"HardwareDocument",
|
||||
"read_xlsx",
|
||||
"load_tia",
|
||||
"load_positionsdraw",
|
||||
"pair_drives",
|
||||
"conveyor_of",
|
||||
"conveyor_kind_of",
|
||||
"build_hardware",
|
||||
]
|
||||
|
||||
HW_SCHEMA = "sps_skel-hw-v1"
|
||||
|
||||
_CONFIG = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
# "=A1+UH01-KF107" -> Ort/Steuerung "UH01", Geraet "KF107"
|
||||
_KENNZEICHNUNG = re.compile(r"\+([A-Za-z0-9]+)-([A-Za-z0-9]+)")
|
||||
# "MB0643" -> ("MB", "0643")
|
||||
_BMK = re.compile(r"^([A-Za-z]+)(\d+)$")
|
||||
# "%E053.2" / "%I53.2" / "%A020.0" / "%Q20.0"
|
||||
_ADDRESS = re.compile(r"^%?([EIAQM])\s*(\d+)\.(\d)$", re.IGNORECASE)
|
||||
# Foerderer im Klartext der E-Planung: "KREISEL-K0102", "KR-K0104", "K0105"
|
||||
_CONVEYOR_K = re.compile(r"(?<![A-Z0-9])K(\d{4})(?!\d)")
|
||||
# ... bzw. "CONVEYOR #100", "CONVEYOR-#106", "CONVEYOR#110"
|
||||
_CONVEYOR_HASH = re.compile(r"#\s*(\d{3})(?!\d)")
|
||||
# Art des Foerderers im Klartext: "KREISEL-K0102", "KR-K0104" -> Kreisel;
|
||||
# "CONVEYOR #106", "KLINKENFOERDERER" -> Strecke
|
||||
_KIND_KREISEL = re.compile(r"(?<![A-Z])(KREISEL|KR)(?![A-Z])")
|
||||
_KIND_STRECKE = re.compile(r"CONVEYOR|CONVEYER|F(OE|Ö)RDERER")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modell
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EDevice(BaseModel):
|
||||
"""Ein Sensor/Aktor/Schrankelement der E-Planung (Positionsdatei)."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
bmk: str
|
||||
verw: str = ""
|
||||
bezeichnung: str = ""
|
||||
kennzeichnung: str = ""
|
||||
# Position in der E-Zeichnung (mm); None bei Schrankelementen ohne Lage
|
||||
pos: tuple[float, float] | None = None
|
||||
# True: aus "schaltschrank_elemente" (im Schrank, nicht im Feld)
|
||||
cabinet: bool = False
|
||||
|
||||
@property
|
||||
def controller(self) -> str:
|
||||
"""Steuerung/Ort aus der Kennzeichnung ("UH01"), "" wenn unbekannt."""
|
||||
m = _KENNZEICHNUNG.search(self.kennzeichnung)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
@property
|
||||
def station(self) -> str:
|
||||
"""E/A-Station aus der Kennzeichnung ("KF107"), "" wenn unbekannt."""
|
||||
m = _KENNZEICHNUNG.search(self.kennzeichnung)
|
||||
return m.group(2) if m else ""
|
||||
|
||||
@property
|
||||
def prefix(self) -> str:
|
||||
m = _BMK.match(self.bmk)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
@property
|
||||
def number(self) -> int | None:
|
||||
m = _BMK.match(self.bmk)
|
||||
return int(m.group(2)) if m else None
|
||||
|
||||
|
||||
class Drive(BaseModel):
|
||||
"""Motor und sein Motorschutz (gleiche Nummer), dazu Foerderer und Geber."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
motor: str
|
||||
protection: str = ""
|
||||
bezeichnung: str = ""
|
||||
protection_bezeichnung: str = ""
|
||||
# Foerderer der E-Planung aus dem Klartext ("K0102", "#106" -> "K0106")
|
||||
conveyor: str = ""
|
||||
# "Kreisel" | "Strecke" | "" aus dem Klartext (conveyor_kind_of)
|
||||
conveyor_kind: str = Field(default="", alias="conveyorKind")
|
||||
# Inkrementalgeber (GF...) desselben Foerderers
|
||||
encoder: str = ""
|
||||
|
||||
|
||||
class TiaTag(BaseModel):
|
||||
"""Eine Zeile aus dem Blatt "PLC Tags" des TIA-Imports."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
name: str
|
||||
data_type: str = Field(default="", alias="dataType")
|
||||
address: str = ""
|
||||
comment: str = ""
|
||||
|
||||
|
||||
class TiaConstant(BaseModel):
|
||||
"""Eine Zeile aus dem Blatt "Constants" des TIA-Imports."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
name: str
|
||||
data_type: str = Field(default="", alias="dataType")
|
||||
value: str = ""
|
||||
comment: str = ""
|
||||
|
||||
|
||||
class HwDevice(EDevice):
|
||||
"""
|
||||
Ein Geraet der Hardwareplanung: Positionsdatei und TIA-Import zusammen.
|
||||
|
||||
Die Felder aus EDevice kommen aus der Positionsdatei (leer, wenn das BMK
|
||||
dort fehlt), Adresse/Konstante aus dem TIA-Import (leer, wenn es dort
|
||||
fehlt). ``sources`` sagt, wo das BMK steht.
|
||||
"""
|
||||
|
||||
# "in" | "out" | "" (Merker, unbekannt)
|
||||
direction: str = ""
|
||||
address: str = ""
|
||||
data_type: str = Field(default="", alias="dataType")
|
||||
comment: str = ""
|
||||
# Index-Konstante fuer "DB_Inputs".Sensors[...] bzw. Ausgaenge
|
||||
constant: str = ""
|
||||
constant_value: int | None = Field(default=None, alias="constantValue")
|
||||
# Foerderer der E-Planung aus Bezeichnung/Kommentar ("K0102"; "#106" -> "K0106")
|
||||
conveyor: str = ""
|
||||
# "Kreisel" | "Strecke" | "" - nur aus dem eigenen Klartext
|
||||
conveyor_kind: str = Field(default="", alias="conveyorKind")
|
||||
# "tia", "positions"
|
||||
sources: list[str] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def in_tia(self) -> bool:
|
||||
return "tia" in self.sources
|
||||
|
||||
|
||||
class HardwareDocument(BaseModel):
|
||||
"""Die Hardware-JSON ``<projekt>_hw.json``."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
schema_id: str = Field(default=HW_SCHEMA, alias="schema")
|
||||
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
project: str = ""
|
||||
tia_file: str = Field(default="", alias="tiaFile")
|
||||
tia_sha256: str = Field(default="", alias="tiaSha256")
|
||||
positions_file: str = Field(default="", alias="positionsFile")
|
||||
positions_sha256: str = Field(default="", alias="positionsSha256")
|
||||
devices: list[HwDevice] = Field(default_factory=list)
|
||||
drives: list[Drive] = Field(default_factory=list)
|
||||
# Konstanten ohne passendes Signal (z. B. Projektkonstanten)
|
||||
constants: list[TiaConstant] = Field(default_factory=list)
|
||||
# Gegenpruefung TIA <-> Positionsdatei (je Befund eine Zeile)
|
||||
checks: list[str] = Field(default_factory=list)
|
||||
# Hinweise beim Einlesen (auch die der Positionsdatei selbst)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
def by_bmk(self) -> dict[str, HwDevice]:
|
||||
return {d.bmk: d for d in self.devices}
|
||||
|
||||
def field_devices(self) -> list[HwDevice]:
|
||||
"""Geraete im Feld mit Lage in der E-Zeichnung."""
|
||||
return [d for d in self.devices if d.pos is not None and not d.cabinet]
|
||||
|
||||
def to_json(self) -> str:
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> HardwareDocument:
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# xlsx lesen (ohne openpyxl)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_NS = {
|
||||
"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
}
|
||||
_T = "{%s}t" % _NS["m"]
|
||||
|
||||
|
||||
def _column(ref: str) -> int:
|
||||
"""'C12' -> 2 (Spalte, 0-basiert)."""
|
||||
n = 0
|
||||
for ch in re.match(r"[A-Z]+", ref).group():
|
||||
n = n * 26 + ord(ch) - 64
|
||||
return n - 1
|
||||
|
||||
|
||||
def read_xlsx(path: Path) -> dict[str, list[list[str]]]:
|
||||
"""Alle Blaetter einer xlsx-Datei als Zeilen von Texten ("" = leer).
|
||||
|
||||
Reicht fuer Tabellen-Exporte (TIA, EA-Liste): Werte, keine Formeln,
|
||||
keine Formate. Leere Zeilen fallen weg.
|
||||
"""
|
||||
with zipfile.ZipFile(path) as z:
|
||||
shared: list[str] = []
|
||||
if "xl/sharedStrings.xml" in z.namelist():
|
||||
root = ET.fromstring(z.read("xl/sharedStrings.xml"))
|
||||
shared = ["".join(t.text or "" for t in si.iter(_T))
|
||||
for si in root.findall("m:si", _NS)]
|
||||
workbook = ET.fromstring(z.read("xl/workbook.xml"))
|
||||
rels = ET.fromstring(z.read("xl/_rels/workbook.xml.rels"))
|
||||
targets = {r.get("Id"): r.get("Target") for r in rels}
|
||||
sheets: dict[str, list[list[str]]] = {}
|
||||
for sheet in workbook.find("m:sheets", _NS):
|
||||
target = targets[sheet.get("{%s}id" % _NS["r"])].lstrip("/")
|
||||
target = target if target.startswith("xl/") else "xl/" + target
|
||||
rows = []
|
||||
for row in ET.fromstring(z.read(target)).iter("{%s}row" % _NS["m"]):
|
||||
cells: dict[int, str] = {}
|
||||
for c in row.findall("m:c", _NS):
|
||||
v = c.find("m:v", _NS)
|
||||
kind = c.get("t")
|
||||
if kind == "s" and v is not None:
|
||||
text = shared[int(v.text)]
|
||||
elif kind == "inlineStr":
|
||||
text = "".join(t.text or "" for t in c.iter(_T))
|
||||
else:
|
||||
text = v.text if v is not None and v.text is not None else ""
|
||||
if text != "":
|
||||
cells[_column(c.get("r"))] = text
|
||||
if cells:
|
||||
rows.append([cells.get(i, "") for i in range(max(cells) + 1)])
|
||||
sheets[sheet.get("name")] = rows
|
||||
return sheets
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TIA-Import
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Spaltenkoepfe im englischen und deutschen TIA-Export
|
||||
_TAG_HEADERS = {
|
||||
"name": ("Name",),
|
||||
"data_type": ("Data Type", "Datentyp"),
|
||||
"address": ("Logical Address", "Logische Adresse", "Adresse"),
|
||||
"comment": ("Comment", "Kommentar"),
|
||||
}
|
||||
_CONST_HEADERS = {
|
||||
"name": ("Name",),
|
||||
"data_type": ("Data Type", "Datentyp"),
|
||||
"value": ("Value", "Wert"),
|
||||
"comment": ("Comment", "Kommentar"),
|
||||
}
|
||||
|
||||
|
||||
def _columns(header: list[str], wanted: dict[str, tuple[str, ...]]) -> dict[str, int] | None:
|
||||
"""Spaltennummern nach Kopfzeile; None, wenn "name" fehlt."""
|
||||
lower = [h.strip().lower() for h in header]
|
||||
found = {}
|
||||
for key, names in wanted.items():
|
||||
for n in names:
|
||||
if n.lower() in lower:
|
||||
found[key] = lower.index(n.lower())
|
||||
break
|
||||
return found if "name" in found else None
|
||||
|
||||
|
||||
def _cell(row: list[str], index: int | None) -> str:
|
||||
return row[index].strip() if index is not None and index < len(row) else ""
|
||||
|
||||
|
||||
def load_tia(path: Path, warnings: list[str] | None = None
|
||||
) -> tuple[list[TiaTag], list[TiaConstant]]:
|
||||
"""Tags und Konstanten aus einem TIA-Import (xlsx).
|
||||
|
||||
Die Blaetter werden an der Kopfzeile erkannt, nicht am Namen: ein Blatt
|
||||
mit Adressspalte sind Tags, eines mit Wertspalte Konstanten.
|
||||
"""
|
||||
warnings = warnings if warnings is not None else []
|
||||
tags: list[TiaTag] = []
|
||||
constants: list[TiaConstant] = []
|
||||
for sheet, rows in read_xlsx(path).items():
|
||||
if not rows:
|
||||
continue
|
||||
tag_cols = _columns(rows[0], _TAG_HEADERS)
|
||||
const_cols = _columns(rows[0], _CONST_HEADERS)
|
||||
if tag_cols and "address" in tag_cols:
|
||||
for row in rows[1:]:
|
||||
name = _cell(row, tag_cols["name"])
|
||||
if name:
|
||||
tags.append(TiaTag(
|
||||
name=name, data_type=_cell(row, tag_cols.get("data_type")),
|
||||
address=_cell(row, tag_cols.get("address")),
|
||||
comment=_cell(row, tag_cols.get("comment"))))
|
||||
elif const_cols and "value" in const_cols:
|
||||
for row in rows[1:]:
|
||||
name = _cell(row, const_cols["name"])
|
||||
if name:
|
||||
constants.append(TiaConstant(
|
||||
name=name, data_type=_cell(row, const_cols.get("data_type")),
|
||||
value=_cell(row, const_cols.get("value")),
|
||||
comment=_cell(row, const_cols.get("comment"))))
|
||||
else:
|
||||
warnings.append(f"TIA-Import: Blatt '{sheet}' weder Tags noch Konstanten "
|
||||
f"- uebersprungen")
|
||||
if not tags:
|
||||
raise ValueError(f"{path.name}: kein Blatt mit Tags (Spalte 'Logical Address' "
|
||||
f"bzw. 'Logische Adresse') gefunden")
|
||||
return tags, constants
|
||||
|
||||
|
||||
def _direction(address: str) -> str:
|
||||
m = _ADDRESS.match(address.strip())
|
||||
if not m:
|
||||
return ""
|
||||
area = m.group(1).upper()
|
||||
return "in" if area in "EI" else "out" if area in "AQ" else ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Positionsdatei (Kabellaengen-Tool)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _device(key: str, raw: dict, cabinet: bool) -> EDevice:
|
||||
# Schluessel "BG0642@UNKNOWN" / "QA7000@A/B": vor dem @ steht das BMK.
|
||||
bmk = str(raw.get("IO") or raw.get("B") or key.split("@", 1)[0]).strip()
|
||||
pos = raw.get("pos")
|
||||
return EDevice(
|
||||
bmk=bmk,
|
||||
verw=str(raw.get("VERW") or "").strip(),
|
||||
bezeichnung=str(raw.get("BEZEICHNUNG") or "").strip(),
|
||||
kennzeichnung=str(raw.get("KENNZEICHNUNG") or "").strip(),
|
||||
pos=(float(pos[0]), float(pos[1])) if pos and len(pos) >= 2 else None,
|
||||
cabinet=cabinet,
|
||||
)
|
||||
|
||||
|
||||
def load_positionsdraw(path: Path, warnings: list[str] | None = None) -> list[EDevice]:
|
||||
"""E-Bauteile aus einer *_positionsdraw.json / *_positionsconv.json lesen.
|
||||
|
||||
Ein BMK kommt hoechstens einmal vor. Steht es doppelt da (im Schrank einmal
|
||||
mit und einmal ohne Attribute), gewinnt der Eintrag mit VERW.
|
||||
"""
|
||||
warnings = warnings if warnings is not None else []
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
devices: dict[str, EDevice] = {}
|
||||
for section, cabinet in (("sensors", False), ("schaltschrank_elemente", True)):
|
||||
for key, raw in (data.get(section) or {}).items():
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
dev = _device(key, raw, cabinet)
|
||||
if not dev.bmk:
|
||||
continue
|
||||
old = devices.get(dev.bmk)
|
||||
if old is None or (not old.verw and dev.verw):
|
||||
devices[dev.bmk] = dev
|
||||
for bmk, positions in (data.get("double_ids") or {}).items():
|
||||
warnings.append(f"E-Planung: {bmk} steht {len(positions)}x in der Zeichnung "
|
||||
f"- verwendet wird ein Eintrag")
|
||||
return list(devices.values())
|
||||
|
||||
|
||||
def _positions_warnings(path: Path) -> list[str]:
|
||||
"""Die Hinweise, die das Kabellaengen-Tool selbst in die Datei schreibt."""
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
out = []
|
||||
for kind, entries in (data.get("warnings") or {}).items():
|
||||
if isinstance(entries, dict):
|
||||
for key, text in entries.items():
|
||||
out.append(f"Positionsdatei ({kind}): {key.split('@', 1)[0]}: "
|
||||
f"{str(text).split(':', 1)[0]}")
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zusammenfuehren
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def conveyor_of(*texts: str) -> str:
|
||||
"""Foerderer der E-Planung aus einem Klartext als "K<NNNN>", sonst "".
|
||||
|
||||
Die E-Planung schreibt denselben Foerderer zweifach: "CONVEYOR-K0103"
|
||||
(Motorschutz FC0203) und "CONVEYOR-#103" (Motor MA0203). Beides ist die
|
||||
vierstellige Foerderernummer des SCL-Namensschemas (DB_JamArea0103,
|
||||
fbCarousel0102) und wird zu "K0103".
|
||||
"""
|
||||
for text in texts:
|
||||
upper = (text or "").upper()
|
||||
m = _CONVEYOR_K.search(upper)
|
||||
if m:
|
||||
return f"K{m.group(1)}"
|
||||
m = _CONVEYOR_HASH.search(upper)
|
||||
if m:
|
||||
return f"K{int(m.group(1)):04d}"
|
||||
return ""
|
||||
|
||||
|
||||
def conveyor_kind_of(*texts: str) -> str:
|
||||
"""Art des genannten Foerderers aus einem Klartext: "Kreisel", "Strecke" oder ""."""
|
||||
for text in texts:
|
||||
upper = (text or "").upper()
|
||||
if _KIND_KREISEL.search(upper):
|
||||
return "Kreisel"
|
||||
if _KIND_STRECKE.search(upper):
|
||||
return "Strecke"
|
||||
return ""
|
||||
|
||||
|
||||
def pair_drives(devices: list[EDevice]) -> list[Drive]:
|
||||
"""Motor MAnnnn <-> Motorschutz FCnnnn (gleiche Nummer), Geber GF je Foerderer."""
|
||||
fc = {d.number: d for d in devices if d.prefix.upper() == "FC" and d.number is not None}
|
||||
encoders: dict[str, str] = {}
|
||||
for d in sorted(devices, key=lambda d: d.bmk):
|
||||
if d.prefix.upper() == "GF":
|
||||
conv = getattr(d, "conveyor", "") or conveyor_of(d.bezeichnung)
|
||||
if conv:
|
||||
encoders.setdefault(conv, d.bmk)
|
||||
drives = []
|
||||
for d in sorted(devices, key=lambda d: d.bmk):
|
||||
if d.prefix.upper() != "MA" or d.number is None:
|
||||
continue
|
||||
schutz = fc.get(d.number)
|
||||
conv = (getattr(d, "conveyor", "") or conveyor_of(d.bezeichnung)
|
||||
or (conveyor_of(schutz.bezeichnung) if schutz else ""))
|
||||
drives.append(Drive(
|
||||
motor=d.bmk,
|
||||
protection=schutz.bmk if schutz else "",
|
||||
bezeichnung=d.bezeichnung,
|
||||
protection_bezeichnung=schutz.bezeichnung if schutz else "",
|
||||
conveyor=conv,
|
||||
conveyor_kind=conveyor_kind_of(d.bezeichnung,
|
||||
schutz.bezeichnung if schutz else ""),
|
||||
encoder=encoders.get(conv, "") if conv else "",
|
||||
))
|
||||
return drives
|
||||
|
||||
|
||||
def _constant_name(bmk: str, direction: str) -> str:
|
||||
return f"cIn{bmk}" if direction == "in" else f"cOut{bmk}" if direction == "out" else ""
|
||||
|
||||
|
||||
def build_hardware(tia_path: Path | None, positions_path: Path | None,
|
||||
project: str = "") -> HardwareDocument:
|
||||
"""Beide Quellen einlesen, je BMK zusammenfuehren und gegeneinander pruefen."""
|
||||
if tia_path is None and positions_path is None:
|
||||
raise ValueError("weder TIA-Import noch Positionsdatei angegeben")
|
||||
warnings: list[str] = []
|
||||
checks: list[str] = []
|
||||
|
||||
tags: list[TiaTag] = []
|
||||
constants: list[TiaConstant] = []
|
||||
if tia_path is not None:
|
||||
tags, constants = load_tia(tia_path, warnings)
|
||||
e_devices: list[EDevice] = []
|
||||
if positions_path is not None:
|
||||
e_devices = load_positionsdraw(positions_path, warnings)
|
||||
warnings.extend(_positions_warnings(positions_path))
|
||||
|
||||
tag_of = {}
|
||||
for t in tags:
|
||||
if t.name in tag_of:
|
||||
checks.append(f"TIA: Tag {t.name} doppelt")
|
||||
tag_of[t.name] = t
|
||||
const_of = {c.name: c for c in constants}
|
||||
e_of = {d.bmk: d for d in e_devices}
|
||||
|
||||
devices: list[HwDevice] = []
|
||||
used_constants: set[str] = set()
|
||||
for bmk in sorted(set(tag_of) | set(e_of)):
|
||||
tag, e = tag_of.get(bmk), e_of.get(bmk)
|
||||
direction = _direction(tag.address) if tag else ""
|
||||
cname = _constant_name(bmk, direction)
|
||||
const = const_of.get(cname)
|
||||
if const:
|
||||
used_constants.add(cname)
|
||||
value = None
|
||||
if const is not None:
|
||||
try:
|
||||
value = int(const.value)
|
||||
except ValueError:
|
||||
checks.append(f"TIA: Konstante {cname} hat keinen ganzzahligen Wert "
|
||||
f"'{const.value}'")
|
||||
base = e.model_dump() if e else {"bmk": bmk}
|
||||
devices.append(HwDevice(
|
||||
**base,
|
||||
direction=direction,
|
||||
address=tag.address if tag else "",
|
||||
data_type=tag.data_type if tag else "",
|
||||
comment=tag.comment if tag else "",
|
||||
constant=cname if const else "",
|
||||
constant_value=value,
|
||||
conveyor=conveyor_of(e.bezeichnung if e else "", tag.comment if tag else ""),
|
||||
conveyor_kind=conveyor_kind_of(e.bezeichnung if e else "",
|
||||
tag.comment if tag else ""),
|
||||
sources=[s for s, ok in (("tia", tag is not None), ("positions", e is not None))
|
||||
if ok],
|
||||
))
|
||||
|
||||
_check(devices, constants, used_constants, tags, e_devices, tia_path, positions_path,
|
||||
checks)
|
||||
return HardwareDocument(
|
||||
project=project,
|
||||
tia_file=tia_path.name if tia_path else "",
|
||||
tia_sha256=file_fingerprint(tia_path) if tia_path else "",
|
||||
positions_file=positions_path.name if positions_path else "",
|
||||
positions_sha256=file_fingerprint(positions_path) if positions_path else "",
|
||||
devices=devices,
|
||||
drives=pair_drives(devices),
|
||||
constants=[c for c in constants if c.name not in used_constants],
|
||||
checks=checks,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
def _check(devices: list[HwDevice], constants: list[TiaConstant], used_constants: set[str],
|
||||
tags: list[TiaTag], e_devices: list[EDevice], tia_path: Path | None,
|
||||
positions_path: Path | None, checks: list[str]) -> None:
|
||||
"""Gegenpruefung TIA-Import <-> Positionsdatei (Ergebnis in checks)."""
|
||||
if tia_path is not None and positions_path is not None:
|
||||
only_e = [d.bmk for d in devices if d.sources == ["positions"]]
|
||||
if only_e:
|
||||
checks.append(f"nur in der Positionsdatei, nicht im TIA-Import "
|
||||
f"({len(only_e)}): {', '.join(only_e)}")
|
||||
# Feldgeraete (Sensoren/Ventile/Motoren) gehoeren in die Zeichnung;
|
||||
# Taster, Leuchten, Not-Halt (SF/PF) stehen dort meist nicht.
|
||||
only_tia = [d.bmk for d in devices if d.sources == ["tia"]
|
||||
and d.prefix.upper() in ("BG", "MB", "MA", "BX")]
|
||||
if only_tia:
|
||||
checks.append(f"Feldgeraet nur im TIA-Import, nicht in der Positionsdatei "
|
||||
f"({len(only_tia)}): {', '.join(only_tia)}")
|
||||
differ = [d.bmk for d in devices if len(d.sources) == 2 and d.bezeichnung
|
||||
and d.comment and d.bezeichnung.strip() != d.comment.strip()]
|
||||
if differ:
|
||||
checks.append(f"TIA-Kommentar weicht von der BEZEICHNUNG ab ({len(differ)}): "
|
||||
f"{', '.join(differ)}")
|
||||
if tia_path is not None:
|
||||
no_const = [d.bmk for d in devices if d.in_tia and d.direction and not d.constant]
|
||||
if no_const:
|
||||
checks.append(f"Signal ohne Index-Konstante cIn/cOut ({len(no_const)}): "
|
||||
f"{', '.join(no_const)}")
|
||||
orphan = [c.name for c in constants
|
||||
if c.name not in used_constants and re.match(r"^c(In|Out)[A-Z]+\d+$", c.name)]
|
||||
if orphan:
|
||||
checks.append(f"Index-Konstante ohne Signal ({len(orphan)}): {', '.join(orphan)}")
|
||||
addresses = Counter(t.address for t in tags if t.address)
|
||||
double = [a for a, n in addresses.items() if n > 1]
|
||||
if double:
|
||||
checks.append(f"Adresse mehrfach vergeben: {', '.join(sorted(double))}")
|
||||
for direction in ("in", "out"):
|
||||
values = Counter(d.constant_value for d in devices
|
||||
if d.direction == direction and d.constant_value is not None)
|
||||
double = sorted(v for v, n in values.items() if n > 1)
|
||||
if double:
|
||||
checks.append(f"Index-Konstanten ({direction}) mit gleichem Wert: "
|
||||
f"{', '.join(map(str, double))}")
|
||||
no_ctrl = [d.bmk for d in e_devices if not d.cabinet and not d.controller]
|
||||
if no_ctrl:
|
||||
checks.append(f"Feldgeraet ohne Steuerung in KENNZEICHNUNG ({len(no_ctrl)}): "
|
||||
f"{', '.join(no_ctrl)}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bericht
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def render_report(doc: HardwareDocument) -> str:
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
add(f"# Hardwareplanung: {doc.project}")
|
||||
add("")
|
||||
add(f"- TIA-Import: `{doc.tia_file or '-'}`")
|
||||
add(f"- Positionsdatei: `{doc.positions_file or '-'}`")
|
||||
add(f"- Erzeugt: {doc.created}")
|
||||
add("")
|
||||
add("## Geraete")
|
||||
add("")
|
||||
prefixes = Counter(d.prefix or "?" for d in doc.devices)
|
||||
add("| Praefix | Anzahl | im Feld mit Lage | mit Index-Konstante |")
|
||||
add("|---|---|---|---|")
|
||||
for p, n in sorted(prefixes.items()):
|
||||
devs = [d for d in doc.devices if (d.prefix or "?") == p]
|
||||
add(f"| {p} | {n} | {sum(1 for d in devs if d.pos and not d.cabinet)} "
|
||||
f"| {sum(1 for d in devs if d.constant)} |")
|
||||
add("")
|
||||
add("## Antriebe (Motor <-> Motorschutz, Geber)")
|
||||
add("")
|
||||
if doc.drives:
|
||||
add("| Foerderer | Motor | Bezeichnung | Motorschutz | Geber |")
|
||||
add("|---|---|---|---|---|")
|
||||
for dr in doc.drives:
|
||||
add(f"| {dr.conveyor or '-'} | {dr.motor} | {dr.bezeichnung} "
|
||||
f"| {dr.protection or '-'} | {dr.encoder or '-'} |")
|
||||
else:
|
||||
add("- keine Motoren")
|
||||
add("")
|
||||
conveyors = Counter(d.conveyor for d in doc.field_devices() if d.conveyor)
|
||||
add("## Foerderer der E-Planung (Feldgeraete, die ihn im Klartext nennen)")
|
||||
add("")
|
||||
if conveyors:
|
||||
for name, n in sorted(conveyors.items()):
|
||||
add(f"- {name}: {n}")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
add("## Gegenpruefung")
|
||||
add("")
|
||||
for c in doc.checks or ["keine Befunde"]:
|
||||
add(f"- {c}")
|
||||
add("")
|
||||
if doc.constants:
|
||||
add("## Konstanten ohne Signal")
|
||||
add("")
|
||||
add(", ".join(c.name for c in doc.constants))
|
||||
add("")
|
||||
if doc.warnings:
|
||||
add("## Hinweise")
|
||||
add("")
|
||||
for w in doc.warnings:
|
||||
add(f"- {w}")
|
||||
add("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_POSITION_SUFFIXES = ("_positionsconv", "_positionsdraw", "_positions")
|
||||
|
||||
|
||||
def project_name(tia_path: Path | None, positions_path: Path | None) -> str:
|
||||
"""Projektname aus den Dateinamen: Mubea_positionsconv.json -> Mubea."""
|
||||
if positions_path is not None:
|
||||
stem = positions_path.stem
|
||||
for suffix in _POSITION_SUFFIXES:
|
||||
if stem.lower().endswith(suffix):
|
||||
return stem[: -len(suffix)]
|
||||
return stem
|
||||
stem = tia_path.stem if tia_path else "anlage"
|
||||
return stem[:-4] if stem.lower().endswith("_tia") else stem
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from material_flow import env_dir, resolve_input
|
||||
|
||||
args = parse_args(argv)
|
||||
tia_path = resolve_input(args.tia) if args.tia else None
|
||||
positions_path = resolve_input(args.positions) if args.positions else None
|
||||
for label, path in (("TIA-Import", tia_path), ("Positionsdatei", positions_path)):
|
||||
if path is not None and not path.is_file():
|
||||
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
|
||||
return 1
|
||||
project = args.project or project_name(tia_path, positions_path)
|
||||
|
||||
try:
|
||||
doc = build_hardware(tia_path, positions_path, project)
|
||||
except (OSError, ValueError, KeyError, zipfile.BadZipFile, ET.ParseError) as exc:
|
||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
results.mkdir(parents=True, exist_ok=True)
|
||||
json_file = results / f"{project}_hw.json"
|
||||
md_file = results / f"{project}_hw.md"
|
||||
json_file.write_text(doc.to_json(), encoding="utf-8")
|
||||
md_file.write_text(render_report(doc), encoding="utf-8")
|
||||
|
||||
n_tia = sum(1 for d in doc.devices if d.in_tia)
|
||||
n_pos = sum(1 for d in doc.devices if "positions" in d.sources)
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("HARDWAREPLANUNG EINLESEN")
|
||||
print("================================================================")
|
||||
print(f"TIA-Import = {tia_path or '-'}")
|
||||
print(f"Positionsdatei = {positions_path or '-'}")
|
||||
print(f"Projekt = {project}")
|
||||
print(f"Geraete = {len(doc.devices)} (TIA {n_tia}, Positionsdatei {n_pos}, "
|
||||
f"beide {sum(1 for d in doc.devices if len(d.sources) == 2)})")
|
||||
print(f"Feld mit Lage = {len(doc.field_devices())}")
|
||||
print(f"Index-Konstanten = {sum(1 for d in doc.devices if d.constant)} "
|
||||
f"(+ {len(doc.constants)} ohne Signal)")
|
||||
print(f"Antriebe = {len(doc.drives)} "
|
||||
f"({sum(1 for d in doc.drives if d.protection)} mit Motorschutz, "
|
||||
f"{sum(1 for d in doc.drives if d.conveyor)} mit Foerderer)")
|
||||
for c in doc.checks:
|
||||
print(f" ? {c}")
|
||||
for w in doc.warnings:
|
||||
print(f" ! {w}")
|
||||
print(f"JSON = {json_file}")
|
||||
print(f"Bericht = {md_file}")
|
||||
print("================================================================")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="hw_import",
|
||||
description="Liest die Hardwareplanung ein - TIA-Import (PLC Tags + "
|
||||
"Constants) und die Positionsdatei des Kabellaengen-Tools - "
|
||||
"und schreibt <projekt>_hw.json fuer merge_hw_layout plus einen "
|
||||
"Bericht <projekt>_hw.md.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Beispiele:\n"
|
||||
" hw_import.bat --tia Mubea/mubea-1_TIA.xlsx "
|
||||
"--positions Mubea/Mubea_positionsconv.json\n"
|
||||
" hw_import.bat --tia mubea-1_TIA.xlsx --project Mubea\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok (Befunde der Gegenpruefung stehen im Bericht)\n"
|
||||
" 1 Eingabe- oder Lesefehler\n",
|
||||
)
|
||||
parser.add_argument("--tia", required=True, metavar="NAME",
|
||||
help="TIA-Import *_TIA.xlsx in %%SKEL_DATA%% (oder ein Pfad, "
|
||||
"auch mit Unterordner)")
|
||||
parser.add_argument("--positions", metavar="NAME",
|
||||
help="*_positionsconv.json bzw. *_positionsdraw.json in "
|
||||
"%%SKEL_DATA%% (oder ein Pfad); ohne sie keine Lage")
|
||||
parser.add_argument("--project", metavar="NAME",
|
||||
help="Projektname fuer <projekt>_hw.json. Standard: aus dem "
|
||||
"Namen der Positionsdatei (Mubea_positionsconv.json -> Mubea)")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,391 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
kreisel_junctions.py - Weichenknoten zwischen Kreiseln aus der Geometrie.
|
||||
|
||||
Zwei Kreisel, die sich beruehren, bilden eine Weichenstelle: am Beruehrpunkt
|
||||
laufen beide Umlaeufe in dieselbe Richtung, ein Traeger kann dort von einem
|
||||
Kreisel auf den anderen wechseln. dxfmakros exportiert solche Stellen als
|
||||
"ILS Weiche" (Merkmal "Erzeugt aus: Kreisel-Beruehrpunkt") - aber nicht
|
||||
immer: in data/export.csv fehlen die Weichen zwischen Kreisel 9/8, 2/3 und
|
||||
3/4, obwohl sich die Kreisel dort beruehren. Darum wird hier jede Weiche aus
|
||||
der Kreisel-Geometrie bestimmt und eine exportierte ILS Weiche nur noch ihrem
|
||||
Beruehrpunkt zugeordnet (Id und Name).
|
||||
|
||||
Geometrie
|
||||
---------
|
||||
Ein Kreisel ist eine Stadion-Form: zwei Umlenkungen (Halbkreise vom Radius
|
||||
r) im Abstand L, verbunden durch die beiden geraden Bahnen. Mittelpunkt ist
|
||||
die CSV-Position, die Laengsachse zeigt in Richtung "Drehung". r und L kommen
|
||||
aus der Boundingbox (kurze Seite = 2r, lange Seite = L + 2r), bei schraeger
|
||||
Lage aus dem Merkmal "Abstand (Kreiselachse A - Kreiselachse) in Meter".
|
||||
Jeder Punkt auf dem Umlauf hat eine Laufposition s (mm) in Drehrichtung
|
||||
("Drehrichtung" UZS/GUZ) - damit ist "stromauf"/"stromab" definiert.
|
||||
|
||||
Weichenknoten und steuernde Separatoren
|
||||
---------------------------------------
|
||||
Beruehrpunkte, die auf derselben Umlenkung eines Kreisels liegen, bilden
|
||||
EINEN Weichenknoten (Kreisel 5 beruehrt mit seiner Umlenkung Kreisel 6 und
|
||||
Kreisel 4 - zwei Weichen, eine Uebergabestelle). Ein Separator steuert den
|
||||
Knoten, wenn er auf einem der beteiligten Kreisel unmittelbar stromauf vor
|
||||
dem Knoten liegt: vom Knoten gegen die Drehrichtung zurueckgegangen, kommt
|
||||
zuerst dieser Separator und keine andere Weiche. Liegt dazwischen eine
|
||||
fremde Weiche, steuert der Separator diese, nicht den Knoten.
|
||||
|
||||
Aus der Anzahl steuernder Separatoren und Weichen ergibt sich der TRO-Typ
|
||||
(tro_flow.derive_tros, Schritt 3c): 1Sep1Swi, 2Sep1Swi, 2Sep2Swi, 3Sep2Swi.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from material_flow import (
|
||||
LANE_LEFT,
|
||||
LANE_RIGHT,
|
||||
ROTATION_CCW,
|
||||
ROTATION_CW,
|
||||
Element,
|
||||
_as_float,
|
||||
_merkmal,
|
||||
)
|
||||
from parameter import get_float, get_int
|
||||
|
||||
# Zahlenwerte aus cfg/parameter.ini, Abschnitt [kreisel_junctions] - Bedeutung
|
||||
# und Einheit stehen dort (lib/parameter.py).
|
||||
_P = "kreisel_junctions"
|
||||
CONTACT_TOLERANCE = get_float(_P, "contact_tolerance")
|
||||
SEPARATOR_TOLERANCE = get_float(_P, "separator_tolerance")
|
||||
WEICHE_TOLERANCE = get_float(_P, "weiche_tolerance")
|
||||
SAMPLE_STEP = get_float(_P, "sample_step")
|
||||
MIN_SAMPLES = get_int(_P, "min_samples")
|
||||
CONTACT_TIE = get_float(_P, "contact_tie")
|
||||
MAX_CONTACT_LENGTH = get_float(_P, "max_contact_length")
|
||||
END_TOLERANCE = get_float(_P, "end_tolerance")
|
||||
DEFAULT_RADIUS = get_float(_P, "default_radius")
|
||||
DIRECTION_STEP = get_float(_P, "direction_step")
|
||||
GLEICHLAUF_MIN_COS = get_float(_P, "gleichlauf_min_cos")
|
||||
|
||||
# Abschnitte eines Umlaufs: die beiden Umlenkungen und die beiden Geraden.
|
||||
END_PLUS = "+" # Umlenkung in Achsrichtung
|
||||
END_MINUS = "-" # Umlenkung gegen die Achsrichtung
|
||||
STRAIGHT = "gerade"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Loop:
|
||||
"""Umlauf eines Kreisels als Stadion-Form mit Laufrichtung.
|
||||
|
||||
Lokale Koordinaten: a entlang der Achse ("Drehung"), b quer dazu (links
|
||||
positiv). Die Umlenkungen liegen bei a = +half und a = -half.
|
||||
"""
|
||||
|
||||
kreisel: Element
|
||||
cx: float
|
||||
cy: float
|
||||
ux: float # Achsrichtung (Einheitsvektor)
|
||||
uy: float
|
||||
half: float # halber Umlenkungsabstand L/2 (mm)
|
||||
radius: float # Bahnabstand / 2 (mm)
|
||||
clockwise: bool # True = UZS
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""Umfang des Umlaufs (mm)."""
|
||||
return 4 * self.half + 2 * math.pi * self.radius
|
||||
|
||||
def _local(self, x: float, y: float) -> tuple[float, float]:
|
||||
"""Weltpunkt -> lokale Koordinaten (a, b)."""
|
||||
dx, dy = x - self.cx, y - self.cy
|
||||
return dx * self.ux + dy * self.uy, -dx * self.uy + dy * self.ux
|
||||
|
||||
def lane(self, x: float, y: float) -> str:
|
||||
"""Bahn eines Punkts: LANE_LEFT links der Achse, sonst LANE_RIGHT
|
||||
(dieselbe Konvention wie tro_flow._kreisel_bahn)."""
|
||||
_a, b = self._local(x, y)
|
||||
return LANE_LEFT if b > 0 else LANE_RIGHT
|
||||
|
||||
def project(self, x: float, y: float) -> tuple[float, float, str]:
|
||||
"""Naechster Punkt auf dem Umlauf.
|
||||
|
||||
Parameter:
|
||||
x, y: Weltpunkt (mm).
|
||||
|
||||
Rueckgabe:
|
||||
(s, Abstand, Abschnitt): s = Laufposition in Drehrichtung
|
||||
(0 <= s < length), Abstand des Punkts vom Umlauf (mm), Abschnitt
|
||||
END_PLUS, END_MINUS oder STRAIGHT.
|
||||
"""
|
||||
a, b = self._local(x, y)
|
||||
h, r = self.half, self.radius
|
||||
cands: list[tuple[float, float, str]] = []
|
||||
# Gegen den Uhrzeigersinn (lokal): untere Gerade (b=-r) in +a, rechte
|
||||
# Umlenkung, obere Gerade (b=+r) in -a, linke Umlenkung.
|
||||
if -h <= a <= h:
|
||||
cands.append((abs(b + r), a + h, STRAIGHT))
|
||||
cands.append((abs(b - r), 2 * h + math.pi * r + (h - a), STRAIGHT))
|
||||
if a >= h:
|
||||
theta = math.atan2(b, a - h) # -pi/2 .. pi/2
|
||||
cands.append((abs(math.hypot(a - h, b) - r),
|
||||
2 * h + r * (theta + math.pi / 2), END_PLUS))
|
||||
if a <= -h:
|
||||
phi = math.atan2(b, a + h) % (2 * math.pi) # pi/2 .. 3pi/2
|
||||
cands.append((abs(math.hypot(a + h, b) - r),
|
||||
4 * h + math.pi * r + r * (phi - math.pi / 2), END_MINUS))
|
||||
if not cands: # Innenpunkt nahe Achse
|
||||
cands.append((abs(b + r), a + h, STRAIGHT))
|
||||
dist, s_ccw, section = min(cands)
|
||||
if section == STRAIGHT and a >= h - END_TOLERANCE:
|
||||
section = END_PLUS
|
||||
elif section == STRAIGHT and a <= -h + END_TOLERANCE:
|
||||
section = END_MINUS
|
||||
s = (self.length - s_ccw) % self.length if self.clockwise else s_ccw % self.length
|
||||
return s, dist, section
|
||||
|
||||
def point(self, s: float) -> tuple[float, float]:
|
||||
"""Weltpunkt zur Laufposition s (Umkehrung von project)."""
|
||||
s_ccw = (self.length - s) % self.length if self.clockwise else s % self.length
|
||||
h, r = self.half, self.radius
|
||||
if s_ccw < 2 * h:
|
||||
a, b = s_ccw - h, -r
|
||||
elif s_ccw < 2 * h + math.pi * r:
|
||||
theta = (s_ccw - 2 * h) / r - math.pi / 2
|
||||
a, b = h + r * math.cos(theta), r * math.sin(theta)
|
||||
elif s_ccw < 4 * h + math.pi * r:
|
||||
a, b = h - (s_ccw - 2 * h - math.pi * r), r
|
||||
else:
|
||||
phi = (s_ccw - 4 * h - math.pi * r) / r + math.pi / 2
|
||||
a, b = -h + r * math.cos(phi), r * math.sin(phi)
|
||||
return (self.cx + a * self.ux - b * self.uy, self.cy + a * self.uy + b * self.ux)
|
||||
|
||||
def direction(self, s: float) -> tuple[float, float]:
|
||||
"""Laufrichtung (Einheitsvektor) an der Laufposition s."""
|
||||
x0, y0 = self.point(s - DIRECTION_STEP)
|
||||
x1, y1 = self.point(s + DIRECTION_STEP)
|
||||
n = math.hypot(x1 - x0, y1 - y0) or 1.0
|
||||
return (x1 - x0) / n, (y1 - y0) / n
|
||||
|
||||
|
||||
def kreisel_loop(kreisel: Element) -> Loop | None:
|
||||
"""Umlauf eines Kreisels aus Position, Drehung, Boundingbox, Drehrichtung.
|
||||
|
||||
Parameter:
|
||||
kreisel: Element mit kind "Kreisel".
|
||||
|
||||
Rueckgabe:
|
||||
Loop oder None, wenn Position, Drehrichtung oder Abmessungen fehlen.
|
||||
"""
|
||||
if kreisel.x is None or kreisel.y is None:
|
||||
return None
|
||||
if kreisel.rotation not in (ROTATION_CW, ROTATION_CCW):
|
||||
return None
|
||||
drehung = _as_float(_merkmal(kreisel.merkmale, "Drehung")) or 0.0
|
||||
rad = math.radians(drehung)
|
||||
bw, bh = kreisel.bbox_x, kreisel.bbox_y
|
||||
if bw and bh and abs(drehung) % 90 < 1e-6:
|
||||
short, long_ = min(bw, bh), max(bw, bh)
|
||||
radius, half = short / 2, (long_ - short) / 2
|
||||
else:
|
||||
abstand = _as_float(_merkmal(
|
||||
kreisel.merkmale, "Abstand (Kreiselachse A - Kreiselachse) in Meter"))
|
||||
if abstand is None:
|
||||
return None
|
||||
radius, half = DEFAULT_RADIUS, abstand * 1000 / 2
|
||||
return Loop(kreisel, kreisel.x, kreisel.y, math.cos(rad), math.sin(rad),
|
||||
half, radius, kreisel.rotation == ROTATION_CW)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""Beruehrpunkt zweier Kreisel = eine Weiche."""
|
||||
|
||||
weiche_id: str # TeileId der ILS Weiche oder "W<a>_<b>"
|
||||
kreisel: tuple[str, str] # beide Kreisel-Ids, sortiert
|
||||
x: float
|
||||
y: float
|
||||
exported: bool # True = als ILS Weiche im CSV
|
||||
s: dict[str, float] = field(default_factory=dict) # Kreisel -> s
|
||||
section: dict[str, str] = field(default_factory=dict) # Kreisel -> Abschnitt
|
||||
|
||||
|
||||
@dataclass
|
||||
class Junction:
|
||||
"""Weichenknoten: Beruehrpunkte auf derselben Umlenkung, plus Steuerung."""
|
||||
|
||||
contacts: list[Contact]
|
||||
kreisel: list[str] # beteiligte Kreisel
|
||||
separators: list[str] = field(default_factory=list) # steuernde Separatoren
|
||||
sep_kreisel: dict[str, str] = field(default_factory=dict) # Separator -> Kreisel
|
||||
sep_lane: dict[str, str] = field(default_factory=dict) # Separator -> "<Id>-L/R"
|
||||
|
||||
@property
|
||||
def type_name(self) -> str:
|
||||
"""TRO-Typ aus Separator- und Weichenzahl, z. B. "2Sep1Swi"."""
|
||||
return f"{len(self.separators)}Sep{len(self.contacts)}Swi"
|
||||
|
||||
|
||||
def _find_contact(a: Loop, b: Loop) -> tuple[float, float, float] | None:
|
||||
"""Beruehrpunkt (x, y, Laenge des Beruehrbereichs) oder None."""
|
||||
steps = max(int(a.length / SAMPLE_STEP), MIN_SAMPLES)
|
||||
near: list[tuple[float, float, float]] = []
|
||||
for i in range(steps):
|
||||
x, y = a.point(i * a.length / steps)
|
||||
_s, dist, _sec = b.project(x, y)
|
||||
if dist <= CONTACT_TOLERANCE:
|
||||
near.append((dist, x, y))
|
||||
if not near:
|
||||
return None
|
||||
# Bei tangentialer Beruehrung sind viele Abtastpunkte fast gleich nah -
|
||||
# der Mittelpunkt der naechsten liegt am eigentlichen Beruehrpunkt.
|
||||
best = min(d for d, _x, _y in near)
|
||||
mitte = [(x, y) for d, x, y in near if d <= best + CONTACT_TIE]
|
||||
x = sum(p[0] for p in mitte) / len(mitte)
|
||||
y = sum(p[1] for p in mitte) / len(mitte)
|
||||
return x, y, len(near) * a.length / steps
|
||||
|
||||
|
||||
def find_contacts(elements: list[Element], findings: list[str]) -> tuple[dict[str, Loop], list[Contact]]:
|
||||
"""Alle Beruehrpunkte zwischen Kreiseln, mit Zuordnung der ILS Weichen.
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports (Kreisel, ILS Weichen).
|
||||
findings: Fundliste; Hinweise zu fehlenden/gegenlaeufigen Weichen.
|
||||
|
||||
Rueckgabe:
|
||||
(Kreisel-Id -> Loop, Beruehrpunkte). Ein Beruehrpunkt ohne
|
||||
exportierte ILS Weiche bekommt die Id "W<KreiselA>_<KreiselB>".
|
||||
"""
|
||||
loops = {e.teile_id: lp for e in elements if e.kind == "Kreisel"
|
||||
and (lp := kreisel_loop(e)) is not None}
|
||||
weichen = [e for e in elements if e.kind == "Weiche"]
|
||||
ids = sorted(loops)
|
||||
contacts: list[Contact] = []
|
||||
for i, ka in enumerate(ids):
|
||||
for kb in ids[i + 1:]:
|
||||
hit = _find_contact(loops[ka], loops[kb])
|
||||
if hit is None:
|
||||
continue
|
||||
x, y, laenge = hit
|
||||
if laenge > MAX_CONTACT_LENGTH:
|
||||
findings.append(
|
||||
f"Kreisel {ka}/{kb}: beruehren sich auf {laenge:.0f}mm Laenge "
|
||||
f"(parallele Bahnen?) - keine Weiche angenommen")
|
||||
continue
|
||||
c = Contact("", (ka, kb), x, y, False)
|
||||
for k in (ka, kb):
|
||||
c.s[k], _d, c.section[k] = loops[k].project(x, y)
|
||||
da = loops[ka].direction(c.s[ka])
|
||||
db = loops[kb].direction(c.s[kb])
|
||||
if da[0] * db[0] + da[1] * db[1] < GLEICHLAUF_MIN_COS:
|
||||
findings.append(
|
||||
f"Kreisel {ka}/{kb}: Beruehrpunkt bei {x:.0f}, {y:.0f} laeuft "
|
||||
f"gegenlaeufig (Drehrichtung pruefen) - kein Weichenknoten")
|
||||
continue
|
||||
contacts.append(c)
|
||||
|
||||
for c in contacts:
|
||||
passend = [w for w in weichen if w.x is not None and w.y is not None
|
||||
and math.hypot(w.x - c.x, w.y - c.y) <= WEICHE_TOLERANCE]
|
||||
if passend:
|
||||
w = min(passend, key=lambda w: math.hypot(w.x - c.x, w.y - c.y))
|
||||
c.weiche_id, c.exported = w.teile_id, True
|
||||
else:
|
||||
c.weiche_id = f"W{c.kreisel[0]}_{c.kreisel[1]}"
|
||||
findings.append(
|
||||
f"Kreisel {c.kreisel[0]}/{c.kreisel[1]}: Beruehrpunkt bei "
|
||||
f"{c.x:.0f}, {c.y:.0f} ohne ILS Weiche im Export - als Weiche "
|
||||
f"{c.weiche_id} angenommen")
|
||||
# ILS Weichen ohne passenden Beruehrpunkt (z. B. an gegenlaeufigen
|
||||
# Kreiseln) bleiben unberuehrt - sie wertet tro_flow wie bisher ueber ihre
|
||||
# CSV-Nachbarn aus (_find_weiche_separators).
|
||||
return loops, contacts
|
||||
|
||||
|
||||
def _group(contacts: list[Contact]) -> list[list[Contact]]:
|
||||
"""Beruehrpunkte auf derselben Umlenkung eines Kreisels zusammenfassen."""
|
||||
parent = list(range(len(contacts)))
|
||||
|
||||
def root(i: int) -> int:
|
||||
while parent[i] != i:
|
||||
parent[i] = parent[parent[i]]
|
||||
i = parent[i]
|
||||
return i
|
||||
|
||||
seen: dict[tuple[str, str], int] = {}
|
||||
for i, c in enumerate(contacts):
|
||||
for k, section in c.section.items():
|
||||
if section == STRAIGHT:
|
||||
continue
|
||||
j = seen.setdefault((k, section), i)
|
||||
parent[root(i)] = root(j)
|
||||
groups: dict[int, list[Contact]] = {}
|
||||
for i, c in enumerate(contacts):
|
||||
groups.setdefault(root(i), []).append(c)
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def find_junctions(
|
||||
elements: list[Element],
|
||||
findings: list[str],
|
||||
separators: list[Element] | None = None,
|
||||
) -> list[Junction]:
|
||||
"""Weichenknoten zwischen Kreiseln mit ihren steuernden Separatoren.
|
||||
|
||||
Parameter:
|
||||
elements: alle Elemente des CSV-Exports.
|
||||
findings: Fundliste (siehe find_contacts).
|
||||
separators: Separatoren, die steuern duerfen; None = alle.
|
||||
|
||||
Rueckgabe:
|
||||
Weichenknoten, sortiert nach erster Weichen-Id. Knoten ohne
|
||||
steuernden Separator sind enthalten (separators leer) - der Aufrufer
|
||||
entscheidet, was damit geschieht.
|
||||
"""
|
||||
loops, contacts = find_contacts(elements, findings)
|
||||
if separators is None:
|
||||
separators = [e for e in elements if e.kind == "Separator"]
|
||||
|
||||
# Separator -> (Kreisel, s): auf dem naechstgelegenen Umlauf
|
||||
sep_pos: dict[str, tuple[str, float]] = {}
|
||||
sep_lane: dict[str, str] = {}
|
||||
for sep in separators:
|
||||
if sep.x is None or sep.y is None:
|
||||
continue
|
||||
best: tuple[float, str, float] | None = None
|
||||
for k, lp in loops.items():
|
||||
s, dist, _sec = lp.project(sep.x, sep.y)
|
||||
if dist <= SEPARATOR_TOLERANCE and (best is None or dist < best[0]):
|
||||
best = (dist, k, s)
|
||||
if best is not None:
|
||||
sep_pos[sep.teile_id] = (best[1], best[2])
|
||||
sep_lane[sep.teile_id] = f"{best[1]}-{loops[best[1]].lane(sep.x, sep.y)}"
|
||||
|
||||
junctions: list[Junction] = []
|
||||
for group in _group(contacts):
|
||||
kreisel = sorted({k for c in group for k in c.kreisel})
|
||||
j = Junction(sorted(group, key=lambda c: c.weiche_id), kreisel)
|
||||
in_group = {id(c) for c in group}
|
||||
for k in kreisel:
|
||||
# Ereignisse auf Kreisel k: (s, Art, Id); Art "sep" oder "swi"
|
||||
events = [(s, "sep", sid) for sid, (kk, s) in sep_pos.items() if kk == k]
|
||||
events += [(c.s[k], "own" if id(c) in in_group else "swi", c.weiche_id)
|
||||
for c in contacts if k in c.s]
|
||||
events.sort()
|
||||
n = len(events)
|
||||
for idx, (_s, art, _id) in enumerate(events):
|
||||
if art != "own":
|
||||
continue
|
||||
# gegen die Laufrichtung zurueck bis zum ersten fremden Ereignis
|
||||
for step in range(1, n):
|
||||
_ps, part, pid = events[(idx - step) % n]
|
||||
if part == "own":
|
||||
continue
|
||||
if part == "sep" and pid not in j.separators:
|
||||
j.separators.append(pid)
|
||||
j.sep_kreisel[pid] = k
|
||||
j.sep_lane[pid] = sep_lane[pid]
|
||||
break
|
||||
j.separators.sort()
|
||||
junctions.append(j)
|
||||
junctions.sort(key=lambda j: j.contacts[0].weiche_id)
|
||||
return junctions
|
||||
+914
-103
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,969 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
||||
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
||||
"""
|
||||
merge_hw_layout.py - Layout und Hardwareplanung zusammenfuehren -> <csv>_anlage.json.
|
||||
|
||||
Stufe 3 des Ablaufs in doc/Konzept_Layout_Hardware_SCL.md:
|
||||
|
||||
<csv>_tro.json (tro_flow --json) + <projekt>_hw.json (hw_import)
|
||||
\\ /
|
||||
merge_hw_layout -> <csv>_anlage.json + Bericht .md
|
||||
|
||||
Das CSV wird nicht gelesen: die TRO-JSON traegt die Slots der TROs und die
|
||||
Lage der Layout-Bauteile (``parts``). Ablauf:
|
||||
|
||||
1. Lagebezug E-Zeichnung -> Layout (lib/dxf_registration.py) ueber die
|
||||
Separator-Sensoren (VERW "IN SEP") gegen die Separatoren des Layouts;
|
||||
abgelegt in SKEL_CFG/dxf_registration.json mit Pruefsumme der
|
||||
Positionsdatei - ein neuer Stand gleichen Namens wird neu ermittelt.
|
||||
2. Regeln aus SKEL_CFG/e_planning.ini (raeumlich mit Hoechst-/Sollabstand,
|
||||
Nummernregel MB = BG + 1) ordnen E-Bauteile Layout-Bauteilen zu.
|
||||
3. Jede Zuordnung geht an die Slot-Rolle des TRO, der das Bauteil traegt
|
||||
(Schluessel TRO-ID + Slot + Rolle). Bauteile ohne Slot (Ausschleus-
|
||||
elemente, Weichen) bleiben als ``ohne_slot`` sichtbar.
|
||||
4. Foerderer: die Namen der E-Planung (K0102) und des Layouts (Kreisel3)
|
||||
passen nicht zusammen. Alle Feldgeraete, deren Klartext einen Foerderer
|
||||
nennt, stimmen fuer den naechsten Layout-Foerderer ab; die Mehrheit
|
||||
entscheidet (cfg/parameter.ini [merge_hw_layout]). Daraus Antrieb, Motorschutz
|
||||
und Geber je Layout-Foerderer und die Antriebs-Slots der Vario-TROs.
|
||||
5. Handkorrekturen aus SKEL_CFG/hw_overrides.ini, zuletzt.
|
||||
6. Pflichtrollen ohne BMK werden ``offen`` (spaeter TODO(E-Planung)), ein
|
||||
BMK, das nicht im TIA-Import steht, ``widerspruch``.
|
||||
|
||||
Lieber eine Luecke als ein falscher Name: was keine Regel sicher trifft,
|
||||
bleibt offen und steht im Bericht.
|
||||
|
||||
Die Zuordnungslogik (Regeln, raeumlich, Nummernregel) stand bis 2026-10-15
|
||||
in e_planning.py; e_planning importiert sie jetzt von hier, damit
|
||||
``scl_skeleton --eplan`` bis zur Umstellung auf die Anlagen-JSON weiterlaeuft.
|
||||
|
||||
Aufruf ueber bin/merge_hw_layout.bat bzw. bin/merge_hw_layout.sh.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import fnmatch
|
||||
import math
|
||||
import sys
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError
|
||||
|
||||
from dxf_registration import (
|
||||
RegistrationError,
|
||||
Transform,
|
||||
derive_transform,
|
||||
load_transform,
|
||||
save_transform,
|
||||
verify_transform,
|
||||
)
|
||||
from hw_import import HW_SCHEMA, EDevice, HardwareDocument, HwDevice
|
||||
from model import (
|
||||
AnlageDocument,
|
||||
Binding,
|
||||
ConveyorBinding,
|
||||
LayoutPart,
|
||||
Registration,
|
||||
TroDocument,
|
||||
)
|
||||
from parameter import get_float, get_int
|
||||
|
||||
__all__ = [
|
||||
"RULES_FILE",
|
||||
"OVERRIDES_FILE",
|
||||
"LAYOUT_KINDS",
|
||||
"CONVEYOR_KINDS",
|
||||
"LayoutPoint",
|
||||
"Rule",
|
||||
"Assignment",
|
||||
"load_rules",
|
||||
"assign",
|
||||
"resolve_transform",
|
||||
"points_from_parts",
|
||||
"bind_slots",
|
||||
"assign_conveyors",
|
||||
"load_overrides",
|
||||
"build_anlage",
|
||||
]
|
||||
|
||||
RULES_FILE = "e_planning.ini"
|
||||
OVERRIDES_FILE = "hw_overrides.ini"
|
||||
|
||||
# Zahlenwerte aus cfg/parameter.ini [merge_hw_layout] - Bedeutung und Einheit stehen dort.
|
||||
_P = "merge_hw_layout"
|
||||
DEFAULT_TOLERANCE_MM = get_float(_P, "toleranz_mm")
|
||||
CONVEYOR_MAX_MM = get_float(_P, "conveyor_max_mm")
|
||||
CONVEYOR_MIN_VOTES = get_int(_P, "conveyor_min_votes")
|
||||
CONVEYOR_MIN_SHARE = get_float(_P, "conveyor_min_share")
|
||||
REGISTRATION_MIN_SHARE = get_float(_P, "registration_min_share")
|
||||
|
||||
_CONFIG = ConfigDict(extra="forbid")
|
||||
|
||||
# Punktartige Layout-Bauteile (Element.kind bzw. Teil der TeileArt)
|
||||
LAYOUT_KINDS = ("Separator", "Scanner", "Weiche", "Ausschleus", "Einschleus",
|
||||
"Beladung", "Entladung")
|
||||
# Foerderer, die fuer einen Foerderer der E-Planung in Frage kommen
|
||||
CONVEYOR_KINDS = ("Kreisel", "Strecke", "Gefaellestrecke")
|
||||
# Antriebs-Rollen eines Slots, die aus dem zugeordneten Foerderer folgen
|
||||
_DRIVE_ROLES = {"motor": "motor", "motorProtection": "protection"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regeln und Zuordnung (bis 2026-10-15 in e_planning.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class LayoutPoint:
|
||||
"""Ein punktartiges Bauteil des Layouts in Layout-Koordinaten."""
|
||||
|
||||
layout_id: str
|
||||
kind: str
|
||||
x: float
|
||||
y: float
|
||||
name: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rule:
|
||||
"""Eine Zuordnungsregel aus e_planning.ini (Abschnitt "<Art>.<Rolle>")."""
|
||||
|
||||
role: str # "Separator.sensor"
|
||||
layout: list[str] # Muster fuer die Layout-Art
|
||||
verw: list[str] # Muster fuer VERW
|
||||
max_mm: float | None = None # raeumlich: Hoechstabstand
|
||||
number_from: str = "" # Nummernregel: Rolle des Partners ...
|
||||
number_offset: int = 0 # ... und Versatz der Nummer
|
||||
prefix: str = "" # BMK-Praefix, leer = egal
|
||||
# Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil.
|
||||
# Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand.
|
||||
target_mm: float | None = None
|
||||
tolerance_mm: float = DEFAULT_TOLERANCE_MM
|
||||
|
||||
@property
|
||||
def slot_role(self) -> str:
|
||||
"""Rolle im Slot: "Separator.sensor" -> "sensor"."""
|
||||
return self.role.split(".", 1)[-1]
|
||||
|
||||
def score(self, dist: float) -> float | None:
|
||||
"""Rangfolge eines Kandidaten (kleiner = besser), None = nicht zulaessig."""
|
||||
if self.max_mm is not None and dist > self.max_mm:
|
||||
return None
|
||||
if self.target_mm is None:
|
||||
return dist
|
||||
dev = abs(dist - self.target_mm)
|
||||
return dev if dev <= self.tolerance_mm else None
|
||||
|
||||
|
||||
class Assignment(BaseModel):
|
||||
"""Ein E-Bauteil in einer Rolle an einem Layout-Bauteil."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
layout_id: str
|
||||
layout_kind: str
|
||||
role: str
|
||||
bmk: str
|
||||
verw: str = ""
|
||||
bezeichnung: str = ""
|
||||
controller: str = ""
|
||||
station: str = ""
|
||||
# Abstand nach dem Lagebezug in mm (auch bei der Nummernregel zur Kontrolle)
|
||||
distance: float
|
||||
# "raeumlich" | "nummer"
|
||||
rule: str
|
||||
# Abstand des naechstbesten Kandidaten derselben Rolle (None: keiner)
|
||||
runner_up: float | None = None
|
||||
|
||||
|
||||
def _patterns(text: str) -> list[str]:
|
||||
return [p.strip() for p in text.split(",") if p.strip()]
|
||||
|
||||
|
||||
def _match(text: str, patterns: list[str]) -> bool:
|
||||
return any(fnmatch.fnmatchcase(text.upper(), p.upper()) for p in patterns)
|
||||
|
||||
|
||||
def load_rules(path: Path) -> tuple[list[Rule], list[str]]:
|
||||
"""Regeln und Anker-Verwendungen aus e_planning.ini lesen.
|
||||
|
||||
Abschnitt [Registrierung] -> anker (VERW-Muster fuer den Lagebezug);
|
||||
jeder Abschnitt "<Art>.<Rolle>" ist eine Regel, in Dateireihenfolge.
|
||||
"""
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||
interpolation=None)
|
||||
parser.optionxform = str # Schluessel nicht kleinschreiben
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
parser.read_file(fh)
|
||||
anchors = _patterns(parser.get("Registrierung", "anker", fallback="IN SEP"))
|
||||
rules = []
|
||||
for section in parser.sections():
|
||||
if "." not in section:
|
||||
continue
|
||||
s = parser[section]
|
||||
max_mm = s.get("max_mm")
|
||||
soll = s.get("soll_mm")
|
||||
rules.append(Rule(
|
||||
role=section,
|
||||
layout=_patterns(s.get("layout", section.split(".", 1)[0])),
|
||||
verw=_patterns(s.get("verw", "*")),
|
||||
max_mm=float(max_mm) if max_mm else None,
|
||||
number_from=s.get("nummer_von", "").strip(),
|
||||
number_offset=int(s.get("nummer_versatz", "0")),
|
||||
prefix=s.get("praefix", "").strip(),
|
||||
target_mm=float(soll) if soll else None,
|
||||
tolerance_mm=float(s.get("toleranz_mm", str(DEFAULT_TOLERANCE_MM))),
|
||||
))
|
||||
known = {r.role for r in rules}
|
||||
for r in rules:
|
||||
if r.number_from and r.number_from not in known:
|
||||
raise ValueError(f"{path.name} [{r.role}]: nummer_von = {r.number_from} "
|
||||
f"ist keine Regel")
|
||||
return rules, anchors
|
||||
|
||||
|
||||
@dataclass
|
||||
class _State:
|
||||
positions: dict[str, tuple[float, float]]
|
||||
used: set[str] = field(default_factory=set)
|
||||
result: list[Assignment] = field(default_factory=list)
|
||||
# (layout_id, role) -> Assignment
|
||||
by_slot: dict[tuple[str, str], Assignment] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _make(point: LayoutPoint, rule: Rule, dev: EDevice, dist: float, how: str,
|
||||
runner_up: float | None) -> Assignment:
|
||||
return Assignment(
|
||||
layout_id=point.layout_id, layout_kind=point.kind, role=rule.role,
|
||||
bmk=dev.bmk, verw=dev.verw, bezeichnung=dev.bezeichnung,
|
||||
controller=dev.controller, station=dev.station,
|
||||
distance=round(dist, 1), rule=how,
|
||||
runner_up=None if runner_up is None else round(runner_up, 1),
|
||||
)
|
||||
|
||||
|
||||
def _candidates(rule: Rule, devices: list[EDevice], state: _State) -> list[EDevice]:
|
||||
return [
|
||||
d for d in devices
|
||||
if d.bmk not in state.used and d.bmk in state.positions
|
||||
and _match(d.verw, rule.verw)
|
||||
and (not rule.prefix or d.prefix.upper() == rule.prefix.upper())
|
||||
]
|
||||
|
||||
|
||||
def _assign_spatial(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
|
||||
state: _State) -> None:
|
||||
"""Beste Bewertung zuerst (Rule.score); je Bauteil und E-Bauteil hoechstens einmal."""
|
||||
cands = _candidates(rule, devices, state)
|
||||
pairs = []
|
||||
for p in points:
|
||||
if (p.layout_id, rule.role) in state.by_slot:
|
||||
continue
|
||||
for d in cands:
|
||||
dist = math.dist((p.x, p.y), state.positions[d.bmk])
|
||||
score = rule.score(dist)
|
||||
if score is not None:
|
||||
pairs.append((score, dist, p.layout_id, d.bmk))
|
||||
pairs.sort()
|
||||
# je Bauteil die Kandidaten in Rangfolge - fuer "naechstbester Kandidat"
|
||||
by_point: dict[str, list[tuple[str, float]]] = {}
|
||||
for _, dist, pid, bmk in pairs:
|
||||
by_point.setdefault(pid, []).append((bmk, dist))
|
||||
point_of = {p.layout_id: p for p in points}
|
||||
dev_of = {d.bmk: d for d in cands}
|
||||
for _, dist, pid, bmk in pairs:
|
||||
if (pid, rule.role) in state.by_slot or bmk in state.used:
|
||||
continue
|
||||
others = [x for b, x in by_point[pid] if b != bmk]
|
||||
a = _make(point_of[pid], rule, dev_of[bmk], dist, "raeumlich",
|
||||
others[0] if others else None)
|
||||
state.used.add(bmk)
|
||||
state.by_slot[(pid, rule.role)] = a
|
||||
state.result.append(a)
|
||||
|
||||
|
||||
def _assign_by_number(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
|
||||
state: _State) -> None:
|
||||
"""Nummer = Nummer des Partners (Rolle nummer_von) + Versatz."""
|
||||
cands = {(d.number): d for d in _candidates(rule, devices, state)
|
||||
if d.number is not None}
|
||||
for p in points:
|
||||
if (p.layout_id, rule.role) in state.by_slot:
|
||||
continue
|
||||
partner = state.by_slot.get((p.layout_id, rule.number_from))
|
||||
if partner is None or partner.bmk == "":
|
||||
continue
|
||||
number = next((d.number for d in devices if d.bmk == partner.bmk), None)
|
||||
if number is None:
|
||||
continue
|
||||
dev = cands.get(number + rule.number_offset)
|
||||
if dev is None or dev.bmk in state.used:
|
||||
continue
|
||||
dist = math.dist((p.x, p.y), state.positions[dev.bmk])
|
||||
# Auch die Nummernregel darf nicht quer ueber die Anlage greifen
|
||||
if rule.max_mm is not None and dist > rule.max_mm:
|
||||
continue
|
||||
a = _make(p, rule, dev, dist, "nummer", None)
|
||||
state.used.add(dev.bmk)
|
||||
state.by_slot[(p.layout_id, rule.role)] = a
|
||||
state.result.append(a)
|
||||
|
||||
|
||||
def assign(points: list[LayoutPoint], devices: list[EDevice], rules: list[Rule],
|
||||
transform: Transform) -> list[Assignment]:
|
||||
"""Regeln der Reihe nach anwenden.
|
||||
|
||||
Eine Regel mit nummer_von versucht zuerst die Nummernregel und nimmt fuer
|
||||
die verbleibenden Bauteile den Abstand (wenn max_mm/soll_mm gesetzt ist). So wird
|
||||
das Ventil eines Separators ueber die Nummer gefunden, auch wenn das
|
||||
Ventil des Nachbarn naeher liegt (Speicherlinien im 800-mm-Raster).
|
||||
"""
|
||||
state = _State(positions={
|
||||
d.bmk: transform.to_csv(*d.pos) for d in devices
|
||||
if d.pos is not None and not d.cabinet
|
||||
})
|
||||
for rule in rules:
|
||||
targets = [p for p in points if _match(p.kind, rule.layout)]
|
||||
if rule.number_from:
|
||||
_assign_by_number(rule, targets, devices, state)
|
||||
if rule.max_mm is not None or rule.target_mm is not None:
|
||||
_assign_spatial(rule, targets, devices, state)
|
||||
return sorted(state.result, key=lambda a: (a.layout_id, a.role))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lagebezug
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _anchor_points(points: list[LayoutPoint], devices: list[EDevice],
|
||||
anchors: list[str]):
|
||||
layout_pts = {"Separator": [(p.x, p.y) for p in points if p.kind == "Separator"]}
|
||||
e_pts = {"Separator": [d.pos for d in devices
|
||||
if d.pos is not None and not d.cabinet
|
||||
and _match(d.verw, anchors)]}
|
||||
return layout_pts, e_pts
|
||||
|
||||
|
||||
def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str,
|
||||
eplan_name: str, re_register: bool,
|
||||
warnings: list[str], fingerprint: str = "") -> Transform:
|
||||
"""Abgelegten Lagebezug nachrechnen oder neu ermitteln und ablegen.
|
||||
|
||||
Mit ``fingerprint`` (Pruefsumme der Positionsdatei) gilt ein abgelegter
|
||||
Bezug nur fuer genau diesen Dateistand.
|
||||
"""
|
||||
layout_pts, e_pts = _anchor_points(points, devices, anchors)
|
||||
if not re_register:
|
||||
stored = load_transform(cfg_dir, csv_name, eplan_name, fingerprint)
|
||||
if stored:
|
||||
verify_transform(stored, layout_pts, e_pts)
|
||||
return stored
|
||||
transform = derive_transform(layout_pts, e_pts, csv_name, eplan_name)
|
||||
transform.fingerprint = fingerprint
|
||||
saved = save_transform(cfg_dir, transform)
|
||||
warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}")
|
||||
return transform
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def points_from_parts(parts: list[LayoutPart]) -> list[LayoutPoint]:
|
||||
"""Punktartige Bauteile aus TroDocument.parts."""
|
||||
points = []
|
||||
for p in parts:
|
||||
if p.position is None:
|
||||
continue
|
||||
kind = next((k for k in LAYOUT_KINDS if k.lower() in p.kind.lower()), "")
|
||||
if kind:
|
||||
points.append(LayoutPoint(p.layout_id, kind, p.position[0], p.position[1],
|
||||
p.name))
|
||||
return points
|
||||
|
||||
|
||||
def _binding_from(a: Assignment, device: HwDevice | None, tro: str = "",
|
||||
slot: str = "", role: str = "") -> Binding:
|
||||
status, note = "zugeordnet", ""
|
||||
if device is None or not device.in_tia:
|
||||
status, note = "widerspruch", "BMK nicht im TIA-Import"
|
||||
if not tro:
|
||||
status = "ohne_slot" if status == "zugeordnet" else status
|
||||
note = note or f"{a.layout_kind} {a.layout_id} haengt an keinem TRO-Slot"
|
||||
return Binding(
|
||||
tro=tro, slot=slot, role=role or a.role, layout_id=a.layout_id, bmk=a.bmk,
|
||||
constant=device.constant if device else "",
|
||||
address=device.address if device else "",
|
||||
source="positionsdraw", rule=a.rule, distance=a.distance,
|
||||
runner_up=a.runner_up, status=status, note=note,
|
||||
)
|
||||
|
||||
|
||||
def bind_slots(doc: TroDocument, assignments: list[Assignment],
|
||||
rules: list[Rule], hw: HardwareDocument) -> list[Binding]:
|
||||
"""Zuordnungen an die Slot-Rollen der TROs haengen.
|
||||
|
||||
Ein Bauteil kann in zwei Slots desselben TRO stehen (Speicher:
|
||||
``separator1`` und ``line1``) - dann bekommen beide die Zuordnung.
|
||||
"""
|
||||
devices = hw.by_bmk()
|
||||
slots_of: dict[str, list[tuple[str, str, list[str]]]] = {}
|
||||
for tro in doc.tros:
|
||||
for s in tro.slots:
|
||||
if s.layout_id:
|
||||
roles = [r.rstrip("?") for r in s.roles]
|
||||
slots_of.setdefault(s.layout_id, []).append((tro.tro_id, s.slot, roles))
|
||||
rule_of = {r.role: r for r in rules}
|
||||
bindings: list[Binding] = []
|
||||
for a in assignments:
|
||||
role = rule_of[a.role].slot_role if a.role in rule_of else a.role.split(".")[-1]
|
||||
targets = [(t, s) for t, s, roles in slots_of.get(a.layout_id, []) if role in roles]
|
||||
if not targets:
|
||||
bindings.append(_binding_from(a, devices.get(a.bmk)))
|
||||
continue
|
||||
for tro_id, slot in targets:
|
||||
bindings.append(_binding_from(a, devices.get(a.bmk), tro_id, slot, role))
|
||||
return bindings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Foerderer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def assign_conveyors(doc: TroDocument, hw: HardwareDocument, transform: Transform
|
||||
) -> list[ConveyorBinding]:
|
||||
"""Foerderer der E-Planung (K0102) per Mehrheit einem Layout-Foerderer zuordnen.
|
||||
|
||||
Jedes Feldgeraet, dessen Klartext einen Foerderer nennt, stimmt fuer den
|
||||
Layout-Foerderer, dessen Huellbox ihm am naechsten liegt (hoechstens
|
||||
CONVEYOR_MAX_MM). Zugeordnet wird bei mindestens CONVEYOR_MIN_VOTES
|
||||
Stimmen und CONVEYOR_MIN_SHARE Anteil; je Layout-Foerderer hoechstens ein
|
||||
Foerderer der E-Planung (der mit den meisten Stimmen).
|
||||
"""
|
||||
conveyors = [p for p in doc.parts if p.kind in CONVEYOR_KINDS and p.bbox and p.position]
|
||||
drives = {dr.conveyor: dr for dr in hw.drives if dr.conveyor}
|
||||
kind_of = _conveyor_kinds(hw)
|
||||
votes: dict[str, Counter] = {}
|
||||
totals: Counter = Counter()
|
||||
for d in hw.field_devices():
|
||||
if not d.conveyor:
|
||||
continue
|
||||
totals[d.conveyor] += 1
|
||||
# Die Huellboxen ueberlappen (Speicherlinien reichen bis an den
|
||||
# Kreisel); nennt der Klartext die Art, stimmen nur Foerderer dieser Art.
|
||||
kinds = _LAYOUT_KINDS_FOR.get(kind_of.get(d.conveyor, ""), CONVEYOR_KINDS)
|
||||
x, y = transform.to_csv(*d.pos)
|
||||
best = min(((c.distance_to_box(x, y), c.layout_id) for c in conveyors
|
||||
if c.kind in kinds), default=(None, ""))
|
||||
if best[0] is not None and best[0] <= CONVEYOR_MAX_MM:
|
||||
votes.setdefault(d.conveyor, Counter())[best[1]] += 1
|
||||
names = sorted(set(totals) | set(drives))
|
||||
name_of = {p.layout_id: p.name for p in doc.parts}
|
||||
|
||||
candidates = []
|
||||
for name in names:
|
||||
counter = votes.get(name, Counter())
|
||||
layout_id, n = counter.most_common(1)[0] if counter else ("", 0)
|
||||
candidates.append((n, name, layout_id))
|
||||
taken: dict[str, str] = {}
|
||||
result = []
|
||||
for n, name, layout_id in sorted(candidates, key=lambda c: (-c[0], c[1])):
|
||||
total = totals.get(name, 0)
|
||||
dr = drives.get(name)
|
||||
cb = ConveyorBinding(eplan_name=name, votes=n, total=total, rule="mehrheit",
|
||||
motor=dr.motor if dr else "",
|
||||
protection=dr.protection if dr else "",
|
||||
encoder=dr.encoder if dr else "")
|
||||
share = n / total if total else 0.0
|
||||
if not layout_id:
|
||||
cb.status = "offen"
|
||||
cb.note = ("kein Feldgeraet nennt diesen Foerderer" if not total else
|
||||
f"keine Stimme innerhalb {CONVEYOR_MAX_MM:.0f} mm eines Layout-Foerderers")
|
||||
elif n < CONVEYOR_MIN_VOTES or share < CONVEYOR_MIN_SHARE:
|
||||
cb.status = "offen"
|
||||
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
||||
cb.note = (f"unsicher: {n} von {total} Stimmen fuer {cb.layout_name or layout_id} "
|
||||
f"(noetig {CONVEYOR_MIN_VOTES} und {CONVEYOR_MIN_SHARE:.0%})")
|
||||
elif layout_id in taken:
|
||||
cb.status = "widerspruch"
|
||||
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
||||
cb.note = f"{layout_id} ist schon {taken[layout_id]} zugeordnet (mehr Stimmen)"
|
||||
else:
|
||||
taken[layout_id] = name
|
||||
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
||||
others = votes.get(name, Counter())
|
||||
rest = [f"{name_of.get(k, k)} {v}" for k, v in others.most_common() if k != layout_id]
|
||||
if rest:
|
||||
cb.note = "Gegenstimmen: " + ", ".join(rest)
|
||||
result.append(cb)
|
||||
return sorted(result, key=lambda c: c.eplan_name)
|
||||
|
||||
|
||||
# Art des Foerderers im Klartext der E-Planung -> Arten im Layout
|
||||
_LAYOUT_KINDS_FOR = {"Kreisel": ("Kreisel",), "Strecke": ("Strecke", "Gefaellestrecke")}
|
||||
|
||||
|
||||
def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
|
||||
"""Art je Foerderer der E-Planung: Mehrheit aller Klartexte, die ihn nennen.
|
||||
|
||||
"PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)" und "ANTRIEB
|
||||
KREISEL-K0102" sagen Kreisel; ein Geraet ohne Artwort erbt die Art.
|
||||
"""
|
||||
counts: dict[str, Counter] = {}
|
||||
for d in hw.devices:
|
||||
if d.conveyor and d.conveyor_kind:
|
||||
counts.setdefault(d.conveyor, Counter())[d.conveyor_kind] += 1
|
||||
for dr in hw.drives:
|
||||
if dr.conveyor and dr.conveyor_kind:
|
||||
counts.setdefault(dr.conveyor, Counter())[dr.conveyor_kind] += 1
|
||||
return {name: c.most_common(1)[0][0] for name, c in counts.items()}
|
||||
|
||||
|
||||
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
|
||||
hw: HardwareDocument) -> list[Binding]:
|
||||
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers."""
|
||||
devices = hw.by_bmk()
|
||||
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
|
||||
bindings = []
|
||||
for tro in doc.tros:
|
||||
for s in tro.slots:
|
||||
if not s.layout_id:
|
||||
continue
|
||||
conv = by_layout.get(s.layout_id.split("-", 1)[0])
|
||||
if conv is None:
|
||||
continue
|
||||
roles = [r.rstrip("?") for r in s.roles]
|
||||
for role, attr in _DRIVE_ROLES.items():
|
||||
bmk = getattr(conv, attr)
|
||||
if role not in roles or not bmk:
|
||||
continue
|
||||
dev = devices.get(bmk)
|
||||
bindings.append(Binding(
|
||||
tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id,
|
||||
bmk=bmk, constant=dev.constant if dev else "",
|
||||
address=dev.address if dev else "", source="positionsdraw",
|
||||
rule="mehrheit", status="zugeordnet" if dev and dev.in_tia else "widerspruch",
|
||||
note=f"Antrieb von {conv.eplan_name}"))
|
||||
return bindings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Handkorrekturen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Overrides:
|
||||
"""Handkorrekturen aus hw_overrides.ini fuer eine Anlage."""
|
||||
|
||||
# (tro, slot, role) -> BMK ("" = Zuordnung loeschen, Rolle offen)
|
||||
bindings: dict[tuple[str, str, str], str] = field(default_factory=dict)
|
||||
# Foerderer der E-Planung -> Layout-ID ("" = Zuordnung loeschen)
|
||||
conveyors: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
def load_overrides(path: Path, layout_source: str) -> Overrides:
|
||||
"""Abschnitte ``[<csv>.bindings]`` und ``[<csv>.conveyors]`` lesen.
|
||||
|
||||
``TRO01.separator1.sensor = BG0532`` setzt ein BMK, ``= -`` loescht die
|
||||
Zuordnung (Rolle bleibt offen); ``K0102 = 0010`` setzt einen Foerderer.
|
||||
"""
|
||||
result = Overrides()
|
||||
if not path.is_file():
|
||||
return result
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
||||
interpolation=None)
|
||||
parser.optionxform = str
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
parser.read_file(fh)
|
||||
section = f"{layout_source}.bindings"
|
||||
if parser.has_section(section):
|
||||
for key, value in parser[section].items():
|
||||
parts = key.split(".")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"{path.name} [{section}] '{key}': erwartet "
|
||||
f"<TRO-ID>.<Slot>.<Rolle>")
|
||||
result.bindings[(parts[0], parts[1], parts[2].rstrip("?"))] = \
|
||||
"" if value.strip() == "-" else value.strip()
|
||||
section = f"{layout_source}.conveyors"
|
||||
if parser.has_section(section):
|
||||
for key, value in parser[section].items():
|
||||
result.conveyors[key.strip()] = "" if value.strip() == "-" else value.strip()
|
||||
return result
|
||||
|
||||
|
||||
def _apply_overrides(doc: TroDocument, bindings: list[Binding],
|
||||
conveyors: list[ConveyorBinding], overrides: Overrides,
|
||||
hw: HardwareDocument, warnings: list[str]) -> list[Binding]:
|
||||
devices = hw.by_bmk()
|
||||
slots = {(t.tro_id, s.slot): s for t in doc.tros for s in t.slots}
|
||||
for (tro, slot, role), bmk in overrides.bindings.items():
|
||||
s = slots.get((tro, slot))
|
||||
if s is None:
|
||||
warnings.append(f"{OVERRIDES_FILE}: {tro}.{slot} gibt es in der TRO-JSON nicht "
|
||||
f"- Korrektur nicht angewendet")
|
||||
continue
|
||||
bindings = [b for b in bindings
|
||||
if not (b.tro == tro and b.slot == slot and b.role == role)]
|
||||
if not bmk:
|
||||
continue
|
||||
# dasselbe BMK nicht zusaetzlich automatisch an anderer Stelle
|
||||
bindings = [b for b in bindings if b.bmk != bmk or b.rule == "hand"]
|
||||
dev = devices.get(bmk)
|
||||
bindings.append(Binding(
|
||||
tro=tro, slot=slot, role=role, layout_id=s.layout_id, bmk=bmk,
|
||||
constant=dev.constant if dev else "", address=dev.address if dev else "",
|
||||
source=OVERRIDES_FILE, rule="hand",
|
||||
status="zugeordnet" if dev and dev.in_tia else "widerspruch",
|
||||
note="" if dev and dev.in_tia else
|
||||
("BMK nicht im TIA-Import" if dev else "BMK unbekannt in der Hardwareplanung")))
|
||||
name_of = {p.layout_id: p.name for p in doc.parts}
|
||||
for name, layout_id in overrides.conveyors.items():
|
||||
cb = next((c for c in conveyors if c.eplan_name == name), None)
|
||||
if cb is None:
|
||||
cb = ConveyorBinding(eplan_name=name)
|
||||
conveyors.append(cb)
|
||||
if layout_id and doc.part(layout_id) is None:
|
||||
warnings.append(f"{OVERRIDES_FILE}: Foerderer {name} -> {layout_id}: Layout-ID "
|
||||
f"unbekannt - Korrektur nicht angewendet")
|
||||
continue
|
||||
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
||||
cb.rule = "hand"
|
||||
cb.status = "zugeordnet" if layout_id else "offen"
|
||||
cb.note = "von Hand"
|
||||
return bindings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zusammenbau
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _open_and_conflicts(doc: TroDocument, bindings: list[Binding]) -> list[Binding]:
|
||||
"""Pflichtrollen ohne BMK als ``offen``; doppelt vergebene BMK als Widerspruch."""
|
||||
have = {(b.tro, b.slot, b.role) for b in bindings if b.tro}
|
||||
for tro in doc.tros:
|
||||
for s in tro.slots:
|
||||
for role in s.roles:
|
||||
if role.endswith("?") or (tro.tro_id, s.slot, role) in have:
|
||||
continue
|
||||
bindings.append(Binding(tro=tro.tro_id, slot=s.slot, role=role,
|
||||
layout_id=s.layout_id, status="offen"))
|
||||
# Ein BMK an zwei verschiedenen Bauteilen ist ein Widerspruch. Derselbe
|
||||
# Separator in zwei Slots eines Speichers (separator1/line1) ist keiner.
|
||||
places: dict[str, set[tuple[str, str]]] = {}
|
||||
for b in bindings:
|
||||
if b.bmk and b.status != "offen":
|
||||
places.setdefault(b.bmk, set()).add((b.layout_id, b.role))
|
||||
for b in bindings:
|
||||
if b.bmk and len(places.get(b.bmk, ())) > 1 and b.status == "zugeordnet":
|
||||
b.status = "widerspruch"
|
||||
b.note = "BMK mehrfach vergeben"
|
||||
order = {t.tro_id: i for i, t in enumerate(doc.tros)}
|
||||
return sorted(bindings, key=lambda b: (b.tro == "", order.get(b.tro, 0), b.slot,
|
||||
b.role, b.layout_id))
|
||||
|
||||
|
||||
def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
|
||||
anchors: list[str], cfg_dir: Path, layout_file: str, hw_file: str,
|
||||
overrides: Overrides | None = None, re_register: bool = False,
|
||||
transform: Transform | None = None) -> AnlageDocument:
|
||||
"""Kompletter Lauf ohne Ausgabe: TRO-JSON + Hardware-JSON -> AnlageDocument.
|
||||
|
||||
``transform`` vorgeben (Tests) ueberspringt die Ermittlung des Lagebezugs.
|
||||
"""
|
||||
warnings: list[str] = []
|
||||
if not doc.parts:
|
||||
raise ValueError(f"{layout_file} enthaelt keine 'parts' (Lage der Bauteile) - "
|
||||
f"mit tro_flow --json neu erzeugen")
|
||||
points = points_from_parts(doc.parts)
|
||||
devices: list[EDevice] = list(hw.devices)
|
||||
|
||||
assignments: list[Assignment] = []
|
||||
conveyors: list[ConveyorBinding] = []
|
||||
if hw.field_devices():
|
||||
if transform is None:
|
||||
transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source,
|
||||
hw.positions_file, re_register, warnings,
|
||||
hw.positions_sha256)
|
||||
share = transform.matched / transform.candidates if transform.candidates else 1.0
|
||||
if share < REGISTRATION_MIN_SHARE:
|
||||
warnings.append(
|
||||
f"Lagebezug passt nur fuer {transform.matched} von {transform.candidates} "
|
||||
f"Separatoren ({share:.0%}) - Layout und E-Zeichnung sind vermutlich "
|
||||
f"verschiedene Planungsstaende; raeumliche Zuordnungen pruefen")
|
||||
assignments = assign(points, devices, rules, transform)
|
||||
conveyors = assign_conveyors(doc, hw, transform)
|
||||
else:
|
||||
warnings.append("Hardwareplanung ohne Lage (keine Positionsdatei) - keine "
|
||||
"raeumliche Zuordnung, alle Rollen offen")
|
||||
|
||||
bindings = bind_slots(doc, assignments, rules, hw)
|
||||
if overrides is not None:
|
||||
bindings = _apply_overrides(doc, bindings, conveyors, overrides, hw, warnings)
|
||||
bindings += bind_drives(doc, conveyors, hw)
|
||||
bindings = _open_and_conflicts(doc, bindings)
|
||||
|
||||
used = {b.bmk for b in bindings if b.bmk}
|
||||
unassigned = sorted(d.bmk for d in hw.field_devices() if d.bmk not in used)
|
||||
registration = None
|
||||
if transform is not None:
|
||||
registration = Registration(
|
||||
dx=round(transform.dx, 1), dy=round(transform.dy, 1),
|
||||
rotation=round(transform.rotation, 4), source=transform.source,
|
||||
matched=transform.matched, candidates=transform.candidates,
|
||||
residual_max=round(transform.residual_max, 1),
|
||||
description=transform.describe())
|
||||
return AnlageDocument(
|
||||
layout_file=layout_file, hw_file=hw_file, project=hw.project,
|
||||
registration=registration, layout=doc, bindings=bindings,
|
||||
conveyors=sorted(conveyors, key=lambda c: c.eplan_name),
|
||||
unassigned=unassigned, warnings=warnings + hw.checks,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bericht
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def render_report(anlage: AnlageDocument, hw: HardwareDocument) -> str:
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
devices = hw.by_bmk()
|
||||
add(f"# Anlage: {anlage.layout.source} + {anlage.project}")
|
||||
add("")
|
||||
add(f"- Layout: `{anlage.layout_file}`")
|
||||
add(f"- Hardwareplanung: `{anlage.hw_file}` (TIA `{hw.tia_file or '-'}`, "
|
||||
f"Positionen `{hw.positions_file or '-'}`)")
|
||||
add(f"- Erzeugt: {anlage.created}")
|
||||
if anlage.registration:
|
||||
add(f"- Lagebezug: {anlage.registration.description}")
|
||||
add("")
|
||||
|
||||
add("## Slot-Rollen")
|
||||
add("")
|
||||
add("| Slot-Art | Rolle | zugeordnet | offen | Widerspruch |")
|
||||
add("|---|---|---|---|---|")
|
||||
stats: dict[tuple[str, str], Counter] = {}
|
||||
for b in anlage.bindings:
|
||||
if not b.tro:
|
||||
continue
|
||||
kind = b.slot.rstrip("0123456789")
|
||||
stats.setdefault((kind, b.role), Counter())[b.status] += 1
|
||||
for (kind, role), c in sorted(stats.items()):
|
||||
add(f"| {kind} | {role} | {c['zugeordnet']} | {c['offen']} | {c['widerspruch']} |")
|
||||
add("")
|
||||
|
||||
add("## Foerderer (E-Planung -> Layout)")
|
||||
add("")
|
||||
if anlage.conveyors:
|
||||
add("| E-Planung | Layout | Stimmen | Motor | Motorschutz | Geber | Status | Hinweis |")
|
||||
add("|---|---|---|---|---|---|---|---|")
|
||||
for c in anlage.conveyors:
|
||||
layout = f"{c.layout_name} ({c.layout_id})" if c.layout_id else "-"
|
||||
add(f"| {c.eplan_name} | {layout} | {c.votes}/{c.total} | {c.motor or '-'} "
|
||||
f"| {c.protection or '-'} | {c.encoder or '-'} | {c.status} | {c.note} |")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
|
||||
add("## Zuordnungen je TRO")
|
||||
add("")
|
||||
add("Abstand nach dem Lagebezug; \"2.\" = naechstbester Kandidat (knapp = pruefen).")
|
||||
add("")
|
||||
add("| TRO | Slot | Rolle | Layout | BMK | Konstante | Adresse | VERW | Abstand mm "
|
||||
"| 2. | Regel | Status |")
|
||||
add("|---|---|---|---|---|---|---|---|---|---|---|---|")
|
||||
for b in anlage.bindings:
|
||||
if not b.tro or b.status == "offen":
|
||||
continue
|
||||
d = devices.get(b.bmk)
|
||||
dist = "" if b.distance is None else f"{b.distance:.0f}"
|
||||
second = "" if b.runner_up is None else f"{b.runner_up:.0f}"
|
||||
status = b.status + (f" ({b.note})" if b.note and b.status != "zugeordnet" else "")
|
||||
add(f"| {b.tro} | {b.slot} | {b.role} | {b.layout_id} | {b.bmk} | {b.constant} "
|
||||
f"| {b.address} | {d.verw if d else ''} | {dist} | {second} | {b.rule} | {status} |")
|
||||
add("")
|
||||
|
||||
add("## Offene Pflichtrollen")
|
||||
add("")
|
||||
open_by: dict[str, list[str]] = {}
|
||||
for b in anlage.bindings:
|
||||
if b.status == "offen":
|
||||
open_by.setdefault(b.tro, []).append(f"{b.slot}.{b.role}")
|
||||
if open_by:
|
||||
for tro, roles in open_by.items():
|
||||
add(f"- **{tro}**: {', '.join(roles)}")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
|
||||
loose = [b for b in anlage.bindings if not b.tro]
|
||||
add("## Zugeordnet, aber ohne TRO-Slot")
|
||||
add("")
|
||||
if loose:
|
||||
add("Diese Bauteile haengen (noch) an keinem Slot der TRO-JSON, z. B. "
|
||||
"Ausschleuselemente: welcher TRO-Ausgang zu welchem Element gehoert, "
|
||||
"weiss tro_flow noch nicht.")
|
||||
add("")
|
||||
add("| Regel | Layout | BMK | Abstand mm | Status |")
|
||||
add("|---|---|---|---|---|")
|
||||
for b in loose:
|
||||
dist = "" if b.distance is None else f"{b.distance:.0f}"
|
||||
add(f"| {b.role} | {b.layout_id} | {b.bmk} | {dist} | {b.status} |")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
|
||||
add("## Feldgeraete ohne Zuordnung")
|
||||
add("")
|
||||
if anlage.unassigned:
|
||||
add("| BMK | VERW | Bezeichnung | Station |")
|
||||
add("|---|---|---|---|")
|
||||
for bmk in anlage.unassigned:
|
||||
d = devices[bmk]
|
||||
add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |")
|
||||
else:
|
||||
add("- keine")
|
||||
add("")
|
||||
if anlage.warnings:
|
||||
add("## Hinweise")
|
||||
add("")
|
||||
for w in anlage.warnings:
|
||||
add(f"- {w}")
|
||||
add("")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _in_results(name: str, results: Path) -> Path:
|
||||
path = Path(name)
|
||||
if path.is_absolute() or path.parent != Path(".") or path.is_file():
|
||||
return path
|
||||
return results / name
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
from material_flow import env_dir
|
||||
|
||||
args = parse_args(argv)
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||
tro_path = _in_results(args.tro, results)
|
||||
hw_path = _in_results(args.hw, results)
|
||||
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
|
||||
overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE
|
||||
for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path),
|
||||
("Regeln", rules_path)):
|
||||
if not path.is_file():
|
||||
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
doc = TroDocument.from_json(tro_path.read_text(encoding="utf-8"))
|
||||
except ValidationError as exc:
|
||||
print(f"FEHLER: {tro_path} ist keine gueltige TRO-JSON ({exc.error_count()} "
|
||||
f"Abweichungen) - mit tro_flow --json neu erzeugen.", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
hw = HardwareDocument.from_json(hw_path.read_text(encoding="utf-8"))
|
||||
except ValidationError as exc:
|
||||
print(f"FEHLER: {hw_path} passt nicht zum Schema '{HW_SCHEMA}' "
|
||||
f"({exc.error_count()} Abweichungen) - mit hw_import neu erzeugen.",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
rules, anchors = load_rules(rules_path)
|
||||
overrides = load_overrides(overrides_path, doc.source)
|
||||
anlage = build_anlage(doc, hw, rules, anchors, cfg_dir, tro_path.name, hw_path.name,
|
||||
overrides, args.re_register)
|
||||
except RegistrationError as exc:
|
||||
print(f"FEHLER: Lagebezug E-Zeichnung -> Layout nicht nachweisbar: {exc}",
|
||||
file=sys.stderr)
|
||||
print(f" Passen Layout und E-Zeichnung zusammen? Anker-VERW in "
|
||||
f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr)
|
||||
return 1
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
stem = Path(doc.source).stem or tro_path.stem.removesuffix("_tro")
|
||||
results.mkdir(parents=True, exist_ok=True)
|
||||
json_file = results / f"{stem}_anlage.json"
|
||||
md_file = results / f"{stem}_anlage.md"
|
||||
json_file.write_text(anlage.to_json(), encoding="utf-8")
|
||||
md_file.write_text(render_report(anlage, hw), encoding="utf-8")
|
||||
|
||||
status = Counter(b.status for b in anlage.bindings)
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("LAYOUT + HARDWAREPLANUNG -> ANLAGE")
|
||||
print("================================================================")
|
||||
print(f"TRO-JSON = {tro_path}")
|
||||
print(f"Hardware-JSON = {hw_path}")
|
||||
print(f"Lagebezug = {anlage.registration.description if anlage.registration else '-'}")
|
||||
print(f"Slot-Rollen = {status['zugeordnet']} zugeordnet, {status['offen']} offen, "
|
||||
f"{status['widerspruch']} Widerspruch")
|
||||
print(f"Ohne Slot = {status['ohne_slot']} Zuordnungen an Bauteilen ohne TRO-Slot")
|
||||
conv_ok = [c for c in anlage.conveyors if c.status == "zugeordnet"]
|
||||
print(f"Foerderer = {len(conv_ok)} von {len(anlage.conveyors)} zugeordnet"
|
||||
+ (": " + ", ".join(f"{c.eplan_name}={c.layout_name or c.layout_id}"
|
||||
for c in conv_ok) if conv_ok else ""))
|
||||
print(f"Ohne Zuordnung = {len(anlage.unassigned)} Feldgeraete")
|
||||
for w in anlage.warnings:
|
||||
print(f" ! {w}")
|
||||
print(f"JSON = {json_file}")
|
||||
print(f"Bericht = {md_file}")
|
||||
print("================================================================")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="merge_hw_layout",
|
||||
description="Fuehrt die TRO-JSON (tro_flow --json) und die Hardware-JSON "
|
||||
"(hw_import) zusammen: ordnet Sensoren, Ventile und Antriebe den "
|
||||
"Slots der TROs zu und schreibt <csv>_anlage.json plus einen "
|
||||
"Bericht <csv>_anlage.md.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Beispiele:\n"
|
||||
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json\n"
|
||||
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json --re-register\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
|
||||
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
|
||||
)
|
||||
parser.add_argument("--tro", required=True, metavar="NAME",
|
||||
help="TRO-JSON <csv>_tro.json in %%SKEL_RESULTS%% (oder ein Pfad)")
|
||||
parser.add_argument("--hw", required=True, metavar="NAME",
|
||||
help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% "
|
||||
"(oder ein Pfad)")
|
||||
parser.add_argument("--rules", metavar="DATEI",
|
||||
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
|
||||
parser.add_argument("--overrides", metavar="DATEI",
|
||||
help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} "
|
||||
f"(fehlt sie, keine)")
|
||||
parser.add_argument("--re-register", action="store_true",
|
||||
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
||||
"einer abgelegt ist")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+681
@@ -0,0 +1,681 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
model.py - Gemeinsames Datenmodell der TRO-Instanzen und der TRO-JSON.
|
||||
|
||||
Schichten (wer darf wen importieren):
|
||||
|
||||
tro_catalog.py Typ-Ebene: was ein TRO-*Typ* ist (Bauteile, FB, Farbe, Symbol)
|
||||
model.py Instanz-Ebene: ein konkreter TRO, die ganze TRO-JSON <- hier
|
||||
importiert nur tro_catalog
|
||||
tro_flow.py Ableitung: CSV-Graph -> Analysis -> TroDocument
|
||||
scl_skeleton.py Ausgabe: TroDocument -> FB_Main.scl
|
||||
tro_annotate.py Ausgabe: Analysis -> annotierte Zeichnung (nur Ansicht)
|
||||
|
||||
Klassen:
|
||||
|
||||
TroBase was jeder TRO hat, egal woher er stammt: ID, Typ, Separatoren,
|
||||
Vorgaenger/Nachfolger, Position. FB-Baustein und Bauteile
|
||||
werden aus dem Katalog gelesen, nie gespeichert.
|
||||
DerivedTro TroBase + was tro_flow bei der Ableitung zusaetzlich weiss
|
||||
(Hosts, Scanner, Begruendung, Positionsquelle ...)
|
||||
Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen
|
||||
TroCandidate eine im Review gemeldete Stelle, an der ein TRO erwartet
|
||||
wird, die aber (noch) keiner ist
|
||||
Slot eine Stelle eines TRO, an die Hardware gehoert (Separator,
|
||||
Weiche, Scanner, Antrieb) mit den Rollen, die die
|
||||
Hardwareplanung dort fuellen muss
|
||||
JamArea ein Staubereich: Eingang genau eines TRO (je Separator
|
||||
laut Katalog), mit allen Vorgaengern, die dort einfahren
|
||||
LayoutPart ein Bauteil des Layouts mit Lage und Huellbox - das Ziel
|
||||
der raeumlichen Zuordnung in merge_hw_layout (die TRO-JSON traegt
|
||||
es, damit merge_hw_layout das CSV nicht lesen muss)
|
||||
Analysis Arbeitsergebnis von tro_flow.analyse_tros
|
||||
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
|
||||
geschrieben, von scl_skeleton gelesen und geprueft
|
||||
Binding eine Zuordnung Hardware -> TRO-Slot-Rolle (merge_hw_layout)
|
||||
ConveyorBinding ein Foerderer der Hardwareplanung (K0102) an einem
|
||||
Layout-Foerderer, mit Antrieb, Motorschutz, Geber
|
||||
AnlageDocument die Anlagen-JSON (<csv>_anlage.json): Layout unveraendert
|
||||
plus bindings[] - von merge_hw_layout geschrieben
|
||||
|
||||
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
|
||||
`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter
|
||||
`tro_id`/`type_name` (Aliase, `populate_by_name`). Gleiche Daten heissen in
|
||||
allen Klassen gleich (auch in tro_overrides.py): `type_name`, `separators`,
|
||||
`hosts`, `position`, `reason`, `confidence`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
computed_field,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from tro_catalog import TroDefinition, TroItem, get_tro
|
||||
|
||||
__all__ = [
|
||||
"SCHEMA",
|
||||
"TroBase",
|
||||
"Slot",
|
||||
"JamArea",
|
||||
"LayoutPart",
|
||||
"DerivedTro",
|
||||
"Connection",
|
||||
"TroCandidate",
|
||||
"Analysis",
|
||||
"TroDocument",
|
||||
"Registration",
|
||||
"Binding",
|
||||
"ConveyorBinding",
|
||||
"AnlageDocument",
|
||||
"ANLAGE_SCHEMA",
|
||||
"BINDING_STATUS",
|
||||
"derive_slots",
|
||||
"derive_jam_areas",
|
||||
"storage_lines",
|
||||
]
|
||||
|
||||
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
|
||||
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit
|
||||
# `candidates` statt `openPoints`, v4 wie v3 mit `slots` je TRO und `jamAreas`.
|
||||
# Eine v3-Datei liest sich weiter (die neuen Felder fehlen dann und werden beim
|
||||
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived). Seit 2026-10-15
|
||||
# traegt v4 optional `parts` (Lage der Layout-Bauteile fuer merge_hw_layout); eine
|
||||
# v4-Datei ohne `parts` liest sich weiter, nur merge_hw_layout braucht sie.
|
||||
SCHEMA = "sps_skel-tro-v4"
|
||||
|
||||
# Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout)
|
||||
ANLAGE_SCHEMA = "sps_skel-anlage-v1"
|
||||
|
||||
# Zustand einer Zuordnung (doc/Konzept_Layout_Hardware_SCL.md, Umbau 3):
|
||||
# zugeordnet ein BMK steht fest
|
||||
# offen Pflichtrolle eines Slots ohne BMK -> TODO(E-Planung)
|
||||
# widerspruch BMK steht fest, passt aber nicht (fehlt im TIA-Import,
|
||||
# doppelt vergeben, Handkorrektur auf unbekanntes BMK)
|
||||
# ohne_slot E-Bauteil einem Layout-Bauteil zugeordnet, das (noch) an
|
||||
# keinem TRO-Slot haengt (z. B. Ausschleuselemente)
|
||||
BINDING_STATUS = ("zugeordnet", "offen", "widerspruch", "ohne_slot")
|
||||
|
||||
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
|
||||
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
|
||||
# still auf einen Standardwert zurueckzufallen), Aliase und Feldnamen gelten
|
||||
# beide.
|
||||
_CONFIG = ConfigDict(
|
||||
validate_assignment=True,
|
||||
extra="forbid",
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
|
||||
class _Positioned(BaseModel):
|
||||
"""
|
||||
Anlagenkoordinate (mm) als Feld ``position``, in der JSON {"x", "y", "z"}.
|
||||
|
||||
Das Feld selbst deklariert jede Unterklasse, damit es dort in der
|
||||
JSON-Reihenfolge an seiner Stelle steht.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
position: Any
|
||||
|
||||
@field_validator("position", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _position_from_json(cls, value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return (value["x"], value["y"], value.get("z", 0.0))
|
||||
return value
|
||||
|
||||
@field_serializer("position", when_used="json-unless-none", check_fields=False)
|
||||
def _position_to_json(self, value: tuple[float, float, float]) -> dict[str, float]:
|
||||
return {"x": round(value[0], 2), "y": round(value[1], 2), "z": round(value[2], 2)}
|
||||
|
||||
@property
|
||||
def x(self) -> float | None:
|
||||
return self.position[0] if self.position else None
|
||||
|
||||
@property
|
||||
def y(self) -> float | None:
|
||||
return self.position[1] if self.position else None
|
||||
|
||||
@property
|
||||
def z(self) -> float | None:
|
||||
return self.position[2] if self.position else None
|
||||
|
||||
def describe_position(self) -> str:
|
||||
"""Koordinate als Text, z. B. '1030, 4700'."""
|
||||
if not self.position:
|
||||
return "-"
|
||||
return f"{self.position[0]:.0f}, {self.position[1]:.0f}"
|
||||
|
||||
|
||||
class TroBase(_Positioned):
|
||||
"""
|
||||
Kern eines TRO (Instanz in einer Anlage), unabhaengig von seiner Quelle.
|
||||
|
||||
Gegenstueck zu :class:`tro_catalog.TroDefinition`: die Definition beschreibt
|
||||
den *Typ* (FB-Baustein, Bauteile), ein TRO die konkrete Stelle im Layout.
|
||||
Verknuepft ueber ``type_name``, siehe :meth:`definition`.
|
||||
|
||||
FB-Baustein und Bauteile haengen nur am Typ. Sie werden nicht gespeichert,
|
||||
sondern bei jedem Zugriff aus dem Katalog gelesen (``fb_block``, ``items``)
|
||||
und so auch in die JSON geschrieben. Steht beim Einlesen einer JSON ein
|
||||
Wert drin, der nicht zum Katalog passt, ist das ein Fehler - den Typ
|
||||
aendern, nicht die Bauteile.
|
||||
|
||||
Listenfelder werden beim Pruefen kopiert; ``tro.hosts.append(...)`` aendert
|
||||
also nur diesen TRO.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
tro_id: str = Field(alias="id")
|
||||
type_name: str = Field(alias="type")
|
||||
confidence: str = ""
|
||||
separators: list[str] = Field(default_factory=list)
|
||||
predecessors: list[str] = Field(default_factory=list)
|
||||
successors: list[str] = Field(default_factory=list)
|
||||
# Anlagenkoordinate (mm); in der JSON als {"x": .., "y": .., "z": ..}
|
||||
position: tuple[float, float, float] | None = None
|
||||
|
||||
# -- aus dem Katalog -----------------------------------------------------
|
||||
|
||||
def definition(self) -> TroDefinition | None:
|
||||
"""Typdefinition aus tro_catalog (None wenn Typ unbekannt)."""
|
||||
return get_tro(self.type_name)
|
||||
|
||||
@computed_field(alias="fbType") # type: ignore[prop-decorator]
|
||||
@property
|
||||
def fb_block(self) -> str:
|
||||
"""FB-Baustein laut Katalog, z. B. 'FB_ILS_MTRO_1Sep' ("" wenn Typ unbekannt)."""
|
||||
definition = self.definition()
|
||||
return definition.get_fb_block() if definition else ""
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def items(self) -> dict[str, int]:
|
||||
"""Bauteile laut Katalog, z. B. {'Separator': 1} ({} wenn Typ unbekannt)."""
|
||||
definition = self.definition()
|
||||
return definition.get_items() if definition else {}
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _check_catalog_fields(cls, data: Any) -> Any:
|
||||
"""`items`/`fbType` aus einer JSON gegen den Katalog pruefen und entfernen."""
|
||||
if not isinstance(data, dict) or not ({"items", "fbType", "fb_block"} & data.keys()):
|
||||
return data
|
||||
data = dict(data)
|
||||
items = data.pop("items", None)
|
||||
fb_block = data.pop("fbType", data.pop("fb_block", None))
|
||||
type_name = data.get("type", data.get("type_name"))
|
||||
definition = get_tro(type_name) if isinstance(type_name, str) else None
|
||||
expected_items = definition.get_items() if definition else {}
|
||||
expected_fb = definition.get_fb_block() if definition else ""
|
||||
if items is not None and dict(items) != expected_items:
|
||||
raise ValueError(
|
||||
f"items {items} passt nicht zum Typ '{type_name}' laut tro_catalog "
|
||||
f"({expected_items}) - den Typ aendern, nicht die Bauteile"
|
||||
)
|
||||
if fb_block is not None and (fb_block or "") != expected_fb:
|
||||
raise ValueError(
|
||||
f"fbType '{fb_block}' passt nicht zum Typ '{type_name}' laut "
|
||||
f"tro_catalog ('{expected_fb}')"
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class Slot(BaseModel):
|
||||
"""
|
||||
Eine Stelle eines TRO, an die Hardware gehoert (Konzept Umbau 1).
|
||||
|
||||
``slot`` ist der stabile Schluessel innerhalb des TRO (``separator1``,
|
||||
``switch1``, ``scanner1``, ``drive``); zusammen mit der TRO-ID adressiert er
|
||||
die Zuordnung der Hardwareplanung. ``layout_id`` ist die TeileId des
|
||||
Bauteils im aktuellen Export - nur zur Nachverfolgung, nicht stabil ("",
|
||||
wenn das Bauteil im Layout nicht bekannt ist). Rollen mit ``?`` sind
|
||||
optional: fehlt das Geraet, wird der zugehoerige xPlugged*-Schalter FALSE.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
slot: str
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
roles: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class JamArea(_Positioned):
|
||||
"""
|
||||
Ein Staubereich: die Warteschlange vor einem Separator, Eingang genau eines
|
||||
TRO (``tro``, Eingang Nummer ``entry``).
|
||||
|
||||
Kennung ``J<Separator>`` (Mubea: Sep1024 -> J1024). Mehrere Vorgaenger
|
||||
koennen in denselben Staubereich einfahren (Zusammenfuehrung); deren
|
||||
Ausgang zeigt dann auf diesen Staubereich. Der SCL-Name
|
||||
``"DB_JamArea<NNNN>".stJam<NNNN>_<k>`` (Standard UH01-UH05) braucht die
|
||||
Foerderernummer aus der Hardwareplanung und steht deshalb nicht hier.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
jam_id: str = Field(alias="id")
|
||||
tro: str
|
||||
entry: int = 1
|
||||
separator: str = ""
|
||||
# tragender Foerderer (erster Host des TRO im Layout)
|
||||
host: str = ""
|
||||
predecessors: list[str] = Field(default_factory=list)
|
||||
note: str = ""
|
||||
position: tuple[float, float, float] | None = None
|
||||
|
||||
|
||||
class LayoutPart(_Positioned):
|
||||
"""
|
||||
Ein Bauteil des Layouts (eine CSV-Zeile mit Lage), z. B. Separator,
|
||||
Ausschleuselement, Kreisel.
|
||||
|
||||
Ziel der raeumlichen Zuordnung in merge_hw_layout: die Slots nennen nur die
|
||||
``layout_id``, die Lage steht hier. ``bbox`` ist die achsparallele
|
||||
Ausdehnung (mm) um ``position`` aus der CSV-Spalte "Boundingbox" -
|
||||
gebraucht, um Geraete einem langen Foerderer (Kreisel) zuzuordnen.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
layout_id: str = Field(alias="layoutId")
|
||||
kind: str
|
||||
name: str = ""
|
||||
position: tuple[float, float, float] | None = None
|
||||
bbox: tuple[float, float, float] | None = None
|
||||
|
||||
def distance_to_box(self, x: float, y: float) -> float | None:
|
||||
"""Abstand eines Punkts zur Huellbox (0 innerhalb); None ohne Lage/Box."""
|
||||
if not self.position or not self.bbox:
|
||||
return None
|
||||
dx = max(abs(x - self.position[0]) - self.bbox[0] / 2, 0.0)
|
||||
dy = max(abs(y - self.position[1]) - self.bbox[1] / 2, 0.0)
|
||||
return (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
|
||||
class DerivedTro(TroBase):
|
||||
"""
|
||||
Ein von tro_flow aus dem CSV abgeleiteter TRO (oder eine Systemgrenze).
|
||||
|
||||
Zusaetzlich zum Kern: aus welchen Bauteilen er gebildet wurde, warum er
|
||||
diesen Typ hat, und woher seine Position stammt.
|
||||
"""
|
||||
|
||||
label: str
|
||||
hosts: list[str]
|
||||
scanners: list[str]
|
||||
reason: str
|
||||
colocated: list[str] = Field(default_factory=list)
|
||||
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde
|
||||
position_from: str = Field(default="", alias="positionFrom")
|
||||
position_points: int = Field(default=0, alias="positionPoints")
|
||||
# Stellen fuer die Hardwareplanung (derive_slots); leer bei Systemgrenzen
|
||||
slots: list[Slot] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return bool(self.successors or self.predecessors or self.colocated)
|
||||
|
||||
|
||||
class Connection(BaseModel):
|
||||
"""Gerichtete Verbindung zwischen zwei TROs/Stationen (JSON: from/to/kind)."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
src: str = Field(alias="from")
|
||||
dst: str = Field(alias="to")
|
||||
# transfer / umlauf / weiche / unbestimmt / normal (siehe tro_flow.link_tros)
|
||||
kind: str = ""
|
||||
|
||||
|
||||
class TroCandidate(_Positioned):
|
||||
"""
|
||||
Eine im Review gemeldete Stelle, an der ein TRO erwartet wird.
|
||||
|
||||
Kein TRO: keine Separatoren, keine Verbindungen, keine SCL. Das Bauteil
|
||||
steckt heute in einem anderen TRO (``belongs_to``). ``type_name`` ist der
|
||||
hier erwartete Katalogtyp.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
name: str
|
||||
type_name: str = Field(default="", alias="type")
|
||||
position: tuple[float, float, float]
|
||||
belongs_to: str = Field(default="", alias="belongsTo")
|
||||
note: str = ""
|
||||
|
||||
|
||||
class Analysis(BaseModel):
|
||||
"""Ergebnis von tro_flow.analyse_tros: TROs, Kanten, Befunde."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
tros: list[DerivedTro] = Field(default_factory=list)
|
||||
edges: list[tuple[str, str]] = Field(default_factory=list)
|
||||
# (von, nach) -> Kantenart (transfer / umlauf / weiche / unbestimmt / normal)
|
||||
edge_kinds: dict[tuple[str, str], str] = Field(default_factory=dict)
|
||||
isolated: list[DerivedTro] = Field(default_factory=list)
|
||||
unknown_types: list[str] = Field(default_factory=list)
|
||||
findings: list[str] = Field(default_factory=list)
|
||||
# Wie Host/Fanout ermittelt wurden (Doku/Bericht)
|
||||
method: str = "Materialfluss-Graph (Zuordnung/Nachbarn)"
|
||||
# Systemgrenzen (BTMT Be-/Entladestationen) - siehe derive_boundaries; kein
|
||||
# TRO, nur damit der tatsaechliche Materialein-/ausgang sichtbar wird.
|
||||
boundaries: list[DerivedTro] = Field(default_factory=list)
|
||||
|
||||
def by_id(self, tro_id: str) -> DerivedTro | None:
|
||||
for tro in self.tros + self.boundaries:
|
||||
if tro.tro_id == tro_id:
|
||||
return tro
|
||||
return None
|
||||
|
||||
|
||||
class TroDocument(BaseModel):
|
||||
"""
|
||||
Die TRO-JSON einer Anlage (``<csv>_tro.json``).
|
||||
|
||||
Geschrieben von ``tro_flow --json`` (:meth:`from_analysis`, :meth:`to_json`),
|
||||
gelesen von scl_skeleton (:meth:`from_json`). Weil das Modell beide Seiten
|
||||
definiert, faellt ein umbenannter oder falscher Schluessel beim Einlesen
|
||||
als Fehler auf.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
schema_id: str = Field(default=SCHEMA, alias="schema")
|
||||
source: str
|
||||
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
note: str = ""
|
||||
tros: list[DerivedTro] = Field(default_factory=list)
|
||||
# Systemgrenzen (SYSn): Materialein-/ausgang, kein TRO - keine SCL
|
||||
boundaries: list[DerivedTro] = Field(default_factory=list)
|
||||
connections: list[Connection] = Field(default_factory=list)
|
||||
candidates: list[TroCandidate] = Field(default_factory=list)
|
||||
jam_areas: list[JamArea] = Field(default_factory=list, alias="jamAreas")
|
||||
# Layout-Bauteile mit Lage (fuer merge_hw_layout); leer in aelteren Dateien
|
||||
parts: list[LayoutPart] = Field(default_factory=list)
|
||||
findings: list[str] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_analysis(cls, analysis: Analysis, source: str, note: str = "",
|
||||
parts: list[LayoutPart] | None = None) -> TroDocument:
|
||||
"""Dokument aus dem Ergebnis von tro_flow.analyse_tros bilden.
|
||||
|
||||
Slots und Staubereiche werden hier abgeleitet, damit die JSON sie
|
||||
traegt und nicht erst das Rendern sie kennt. ``parts`` (Lage der
|
||||
Layout-Bauteile) gibt tro_flow aus dem CSV mit.
|
||||
"""
|
||||
tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros]
|
||||
return cls(
|
||||
source=source,
|
||||
note=note,
|
||||
tros=tros,
|
||||
boundaries=analysis.boundaries,
|
||||
jam_areas=derive_jam_areas(tros),
|
||||
parts=list(parts or []),
|
||||
connections=[
|
||||
Connection(src=src, dst=dst, kind=analysis.edge_kinds.get((src, dst), ""))
|
||||
for src, dst in analysis.edges
|
||||
],
|
||||
findings=analysis.findings,
|
||||
)
|
||||
|
||||
def part(self, layout_id: str) -> LayoutPart | None:
|
||||
"""Layout-Bauteil zur TeileId; ein Bahn-Suffix ("0010-R") zaehlt nicht."""
|
||||
base = layout_id.split("-", 1)[0]
|
||||
return next((p for p in self.parts if p.layout_id == base), None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> TroDocument:
|
||||
"""JSON-Text einlesen und pruefen (pydantic.ValidationError bei Fehlern)."""
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
def jam_areas_or_derived(self) -> list[JamArea]:
|
||||
"""jamAreas aus der Datei; bei einer v3-Datei (ohne jamAreas) abgeleitet."""
|
||||
return self.jam_areas or derive_jam_areas(self.tros)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Als JSON-Text mit den Schluesseln der TRO-JSON (id, type, fbType ...)."""
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
def by_id(self, tro_id: str) -> DerivedTro | None:
|
||||
for tro in self.tros + self.boundaries:
|
||||
if tro.tro_id == tro_id:
|
||||
return tro
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anlagen-JSON: Layout + Zuordnungen der Hardwareplanung (merge_hw_layout)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Registration(BaseModel):
|
||||
"""Lagebezug E-Zeichnung -> Layout, mit dem die Zuordnung lief (Nachweis).
|
||||
|
||||
Kopie der Kennwerte aus dxf_registration.Transform - model.py importiert
|
||||
dxf_registration bewusst nicht (Schichten, siehe Kopf).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
dx: float
|
||||
dy: float
|
||||
rotation: float = 0.0
|
||||
source: str = ""
|
||||
matched: int = 0
|
||||
candidates: int = 0
|
||||
residual_max: float = Field(default=0.0, alias="residualMax")
|
||||
description: str = ""
|
||||
|
||||
|
||||
class Binding(BaseModel):
|
||||
"""
|
||||
Eine Zuordnung: Rolle ``role`` im Slot ``slot`` des TRO ``tro`` <- BMK.
|
||||
|
||||
Stabiler Schluessel ist TRO-ID + Slot + Rolle (TRO-IDs aus der ID-Datei);
|
||||
``layout_id`` dient nur zur Nachverfolgung im aktuellen Layoutstand. Bei
|
||||
``status == "ohne_slot"`` sind ``tro``/``slot`` leer und ``role`` ist die
|
||||
Regel (``Ausschleus.stau``). ``constant``/``address`` kommen aus dem
|
||||
TIA-Import und sind das, was im SCL steht (``cInBG0532``, ``%E053.2``).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
tro: str = ""
|
||||
slot: str = ""
|
||||
role: str
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
bmk: str = ""
|
||||
constant: str = ""
|
||||
address: str = ""
|
||||
# positionsdraw | hw_overrides.ini
|
||||
source: str = ""
|
||||
# raeumlich | nummer | hand
|
||||
rule: str = ""
|
||||
distance: float | None = None
|
||||
runner_up: float | None = Field(default=None, alias="runnerUp")
|
||||
status: str = "zugeordnet"
|
||||
note: str = ""
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _known_status(cls, value: str) -> str:
|
||||
if value not in BINDING_STATUS:
|
||||
raise ValueError(f"status '{value}' unbekannt, erlaubt: {', '.join(BINDING_STATUS)}")
|
||||
return value
|
||||
|
||||
|
||||
class ConveyorBinding(BaseModel):
|
||||
"""
|
||||
Ein Foerderer der Hardwareplanung (``K0102``, ``K0106``) an einem
|
||||
Layout-Foerderer (``layout_id`` "0010" = Kreisel3).
|
||||
|
||||
Die Namenswelten passen nicht zusammen (Layout: Kreisel1..3, E-Planung:
|
||||
K0102/K0104/K0105), deshalb raeumlich: alle Feldgeraete, deren
|
||||
Bezeichnung den Foerderer nennt, stimmen fuer den naechsten
|
||||
Layout-Foerderer ab (``votes`` von ``total``).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
eplan_name: str = Field(alias="eplanName")
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
layout_name: str = Field(default="", alias="layoutName")
|
||||
votes: int = 0
|
||||
total: int = 0
|
||||
motor: str = ""
|
||||
protection: str = ""
|
||||
encoder: str = ""
|
||||
# mehrheit | hand
|
||||
rule: str = ""
|
||||
status: str = "zugeordnet"
|
||||
note: str = ""
|
||||
|
||||
|
||||
class AnlageDocument(BaseModel):
|
||||
"""
|
||||
Die Anlagen-JSON ``<csv>_anlage.json``: das Layout (TRO-JSON) unveraendert
|
||||
unter ``layout``, daneben die Zuordnungen der Hardwareplanung.
|
||||
|
||||
Geschrieben von merge_hw_layout, kuenftig die eine Eingabe von scl_skeleton
|
||||
(doc/Konzept_Layout_Hardware_SCL.md, Umbau 4).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
schema_id: str = Field(default=ANLAGE_SCHEMA, alias="schema")
|
||||
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
layout_file: str = Field(default="", alias="layoutFile")
|
||||
hw_file: str = Field(default="", alias="hwFile")
|
||||
project: str = ""
|
||||
registration: Registration | None = None
|
||||
layout: TroDocument
|
||||
bindings: list[Binding] = Field(default_factory=list)
|
||||
conveyors: list[ConveyorBinding] = Field(default_factory=list)
|
||||
# Feldgeraete mit Lage, die keine Zuordnung bekommen haben (BMK)
|
||||
unassigned: list[str] = Field(default_factory=list)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
def binding(self, tro_id: str, slot: str, role: str) -> Binding | None:
|
||||
"""Zuordnung einer Slot-Rolle (Rolle ohne "?"), None wenn keine."""
|
||||
role = role.rstrip("?")
|
||||
return next((b for b in self.bindings
|
||||
if b.tro == tro_id and b.slot == slot and b.role == role), None)
|
||||
|
||||
def conveyor_for(self, layout_id: str) -> ConveyorBinding | None:
|
||||
"""Foerderer der Hardwareplanung an einem Layout-Foerderer ("0010-R" geht)."""
|
||||
base = layout_id.split("-", 1)[0]
|
||||
return next((c for c in self.conveyors
|
||||
if c.layout_id == base and c.status == "zugeordnet"), None)
|
||||
|
||||
def to_json(self) -> str:
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> AnlageDocument:
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ableitungen aus Katalogtyp und Topologie
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Rollen je Slot-Art (doc/Konzept_Layout_Hardware_SCL.md, Umbau 1); "?" = optional
|
||||
_SEPARATOR_ROLES = ("sensor", "stopper", "jamSensor?", "partSensor?")
|
||||
_SWITCH_ROLES = ("valve", "exitSensor?")
|
||||
_SCANNER_ROLES = ("reader",)
|
||||
_DRIVE_ROLES = ("motor", "motorProtection", "senCarrInPos", "senFinger",
|
||||
"senLastPos", "senJam")
|
||||
_STORAGE_LINE_ROLES = ("sensor", "stopper", "jamSensor?")
|
||||
|
||||
|
||||
def _at(values: list[str], index: int) -> str:
|
||||
return values[index] if index < len(values) else ""
|
||||
|
||||
|
||||
def storage_lines(tro: TroBase) -> list[str]:
|
||||
"""Separatoren der Lagerlinien eines Speicher-TRO.
|
||||
|
||||
tro_flow legt sie als [steuernde ..., Linien ...] ab; die Linien sind die
|
||||
letzten len(hosts) (eine je Speicherbahn). Fehlen Hosts, zaehlt jeder
|
||||
Separator als Linie - dieselbe Regel wie scl_context._store.
|
||||
"""
|
||||
hosts = list(getattr(tro, "hosts", []) or [])
|
||||
seps = list(tro.separators)
|
||||
count = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
|
||||
return seps[len(seps) - count:]
|
||||
|
||||
|
||||
def derive_slots(tro: DerivedTro) -> list[Slot]:
|
||||
"""Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO.
|
||||
|
||||
Weichen tragen noch keine Layout-ID - tro_flow speichert das Weichen-
|
||||
bzw. Ausschleuselement heute nicht am TRO. Ein Speicher bekommt je
|
||||
Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und
|
||||
``line1`` koennen dasselbe Bauteil sein (wie im PinStore-Template).
|
||||
"""
|
||||
items = tro.items
|
||||
slots: list[Slot] = []
|
||||
for i in range(items.get(TroItem.SEPARATOR, 0)):
|
||||
slots.append(Slot(slot=f"separator{i + 1}", layout_id=_at(tro.separators, i),
|
||||
roles=list(_SEPARATOR_ROLES)))
|
||||
if items.get(TroItem.STORAGE_LINE):
|
||||
for n, sep in enumerate(storage_lines(tro), start=1):
|
||||
slots.append(Slot(slot=f"line{n}", layout_id=sep,
|
||||
roles=list(_STORAGE_LINE_ROLES)))
|
||||
for i in range(items.get(TroItem.SWITCH, 0)):
|
||||
slots.append(Slot(slot=f"switch{i + 1}", roles=list(_SWITCH_ROLES)))
|
||||
for i in range(items.get(TroItem.SCANNER, 0)):
|
||||
slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i),
|
||||
roles=list(_SCANNER_ROLES)))
|
||||
if items.get(TroItem.VARIO_DRIVE):
|
||||
slots.append(Slot(slot="drive", layout_id=_at(tro.hosts, 0),
|
||||
roles=list(_DRIVE_ROLES)))
|
||||
return slots
|
||||
|
||||
|
||||
def derive_jam_areas(tros: list[DerivedTro]) -> list[JamArea]:
|
||||
"""Staubereiche: je Separator laut Katalog ein Eingang des TRO.
|
||||
|
||||
Kennung J<Separator>; fehlt der Separator im Layout, J<TRO-ID>_<n>. Alle
|
||||
Vorgaenger fahren in den ersten Eingang ein - bei zwei Eingaengen
|
||||
(2Sep-Typen) ist die Aufteilung aus dem Layout nicht ableitbar und als
|
||||
Hinweis vermerkt.
|
||||
"""
|
||||
areas: list[JamArea] = []
|
||||
for tro in tros:
|
||||
n_entries = max(1, tro.items.get(TroItem.SEPARATOR, 1))
|
||||
if n_entries > 1 and tro.predecessors:
|
||||
note = "Aufteilung der Vorgaenger auf die Eingaenge offen"
|
||||
elif tro.items.get(TroItem.STORAGE_LINE):
|
||||
# Die Separatoren eines Speichers sitzen auf den Lagerlinien; der
|
||||
# Eingang liegt davor und hat im Layout keinen eigenen Separator.
|
||||
note = "Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator"
|
||||
else:
|
||||
note = ""
|
||||
for n in range(1, n_entries + 1):
|
||||
sep = _at(tro.separators, n - 1)
|
||||
areas.append(JamArea(
|
||||
jam_id=f"J{sep}" if sep else f"J{tro.tro_id}_{n}",
|
||||
tro=tro.tro_id,
|
||||
entry=n,
|
||||
separator=sep,
|
||||
host=_at(tro.hosts, 0),
|
||||
predecessors=list(tro.predecessors) if n == 1 else [],
|
||||
note=note,
|
||||
position=tro.position if n == 1 else None,
|
||||
))
|
||||
return areas
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
parameter.py - Zahlenwerte der Bibliotheken aus cfg/parameter.ini.
|
||||
|
||||
Toleranzen, Schwellen, Massstaebe und Zeichnungsgroessen stehen nicht im Code,
|
||||
sondern in einer INI-Datei: ein Abschnitt je Modul, der Schluessel ist der
|
||||
Name der Konstanten in Kleinbuchstaben ([kreisel_junctions]
|
||||
contact_tolerance -> kreisel_junctions.CONTACT_TOLERANCE).
|
||||
|
||||
Gelesen wird zuerst die Grunddatei des Repositorys (<Projekt>/cfg/
|
||||
parameter.ini, versioniert, muss vollstaendig sein). Liegt in %SKEL_CFG% eine
|
||||
weitere parameter.ini, ueberschreibt sie einzelne Werte - so kann eine Anlage
|
||||
abweichende Toleranzen bekommen, ohne die Grunddatei zu aendern.
|
||||
|
||||
Fehlt die Grunddatei oder ein Schluessel, bricht der Import mit
|
||||
ParameterError ab: einen stillen Ersatzwert im Code gibt es bewusst nicht,
|
||||
sonst stuende die Zahl doch wieder im Code.
|
||||
|
||||
Unterste Schicht: importiert nichts aus lib/, damit jedes Modul es nutzen kann.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
PARAMETER_FILE = "parameter.ini"
|
||||
|
||||
__all__ = ["PARAMETER_FILE", "ParameterError", "files", "get_float", "get_int"]
|
||||
|
||||
|
||||
class ParameterError(RuntimeError):
|
||||
"""Grunddatei fehlt oder ein Wert fehlt bzw. ist keine Zahl."""
|
||||
|
||||
|
||||
def _base_file() -> Path:
|
||||
"""Grunddatei im Repository: <Projekt>/cfg/parameter.ini."""
|
||||
return Path(__file__).resolve().parent.parent / "cfg" / PARAMETER_FILE
|
||||
|
||||
|
||||
def files() -> list[Path]:
|
||||
"""Gelesene Dateien in Lesereihenfolge (Grunddatei, ggf. %SKEL_CFG%)."""
|
||||
result = [_base_file()]
|
||||
cfg = os.environ.get("SKEL_CFG")
|
||||
if cfg:
|
||||
override = Path(cfg) / PARAMETER_FILE
|
||||
if override.is_file() and override.resolve() != result[0].resolve():
|
||||
result.append(override)
|
||||
return result
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _parser() -> configparser.ConfigParser:
|
||||
"""Alle Parameterdateien einlesen (einmal je Prozess)."""
|
||||
base = _base_file()
|
||||
if not base.is_file():
|
||||
raise ParameterError(f"Parameterdatei fehlt: {base}")
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
|
||||
for path in files():
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
parser.read_file(handle, source=str(path))
|
||||
return parser
|
||||
|
||||
|
||||
def _raw(section: str, key: str) -> str:
|
||||
"""Rohwert; ParameterError, wenn Abschnitt oder Schluessel fehlt."""
|
||||
parser = _parser()
|
||||
if not parser.has_option(section, key):
|
||||
raise ParameterError(
|
||||
f"[{section}] {key} fehlt in {', '.join(str(p) for p in files())}")
|
||||
return parser.get(section, key)
|
||||
|
||||
|
||||
def get_float(section: str, key: str) -> float:
|
||||
"""Zahl aus [section] key.
|
||||
|
||||
Parameter:
|
||||
section: Abschnitt, in der Regel der Modulname (z. B. "tro_flow").
|
||||
key: Schluessel, der Konstantenname in Kleinbuchstaben.
|
||||
|
||||
Rueckgabe:
|
||||
Der Wert als float.
|
||||
"""
|
||||
raw = _raw(section, key)
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine Zahl") from None
|
||||
|
||||
|
||||
def get_int(section: str, key: str) -> int:
|
||||
"""Ganze Zahl aus [section] key (Anzahlen, Schrittweiten)."""
|
||||
raw = _raw(section, key)
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
raise ParameterError(f"[{section}] {key} = {raw!r} ist keine ganze Zahl") from None
|
||||
@@ -0,0 +1,649 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
scl_context.py - Render-Kontext fuer das FB_Main-Geruest.
|
||||
|
||||
Schritt 2 aus doc/SCL_Templates.md: alles, was scl_skeleton aus der TRO-JSON
|
||||
*ableitet*, steht hier - Reihenfolge, Bauteilbloecke, Staubereiche,
|
||||
Weichenziele, Namen. Die Ausgabe (heute die emit_*-Funktionen, spaeter die
|
||||
Templates in lib/scl_templates/) liest nur noch diesen Kontext und rechnet
|
||||
nichts mehr selbst.
|
||||
|
||||
Schichten:
|
||||
|
||||
model.py TroDocument (die TRO-JSON)
|
||||
scl_context.py TroDocument -> SclFile <- hier
|
||||
scl_skeleton.py SclFile -> SCL-Text
|
||||
|
||||
Namen (Separator-, Weichen-, MainTro-Nummer, Adressen ...) sind ein
|
||||
:class:`SclName`: Wert plus Merker, ob er noch aus der E-Planung kommen muss.
|
||||
Ohne E-Planung sind alle Platzhalter ``todo=True``. Mit einer Zuordnung aus
|
||||
lib/e_planning.py (``eplan``) werden Separator-Sensor und Stopperventil der
|
||||
zugeordneten Separatoren eingesetzt und ``todo`` geloescht; alles andere
|
||||
bleibt offen. Die Namensregeln aus Schritt 4 (cfg/scl_naming.ini) arbeiten
|
||||
spaeter genauso: Wert ersetzen, Merker loeschen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from model import JamArea, TroBase, TroDocument
|
||||
from tro_catalog import TroItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from e_planning import EplanDocument
|
||||
|
||||
__all__ = [
|
||||
"TODO",
|
||||
"SclName",
|
||||
"SclSeparator",
|
||||
"SclSwitch",
|
||||
"SclSwitchExit",
|
||||
"SclScanner",
|
||||
"SclJam",
|
||||
"SclSscc",
|
||||
"SclWorkStation",
|
||||
"SclStoreLine",
|
||||
"SclStore",
|
||||
"SclTro",
|
||||
"SclFile",
|
||||
"build_context",
|
||||
"flow_order",
|
||||
"reachable_via",
|
||||
]
|
||||
|
||||
TODO = "TODO(E-Planung)"
|
||||
|
||||
_CONFIG = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class SclName(BaseModel):
|
||||
"""Ein Name/eine Adresse in der SCL und ob er noch offen ist."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
value: str
|
||||
# True: Platzhalter, muss aus der E-Planung kommen -> TODO-Kommentar
|
||||
todo: bool = True
|
||||
|
||||
|
||||
def _offen(platzhalter: str) -> SclName:
|
||||
return SclName(value=platzhalter, todo=True)
|
||||
|
||||
|
||||
def _bekannt(wert: str) -> SclName:
|
||||
return SclName(value=wert, todo=False)
|
||||
|
||||
|
||||
# Rolle in e_planning.ini -> wie der Name in der SCL geschrieben wird
|
||||
# (Sensor als Konstante der DB_Inputs-Sensorliste, Ventil als Variable)
|
||||
_SENSOR = "cIn{bmk}"
|
||||
_VENTIL = "{bmk}"
|
||||
|
||||
|
||||
class SclSeparator(BaseModel):
|
||||
"""Ein stInSeparator<n>-Block."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Separatornummer aus dem Layout ("?" wenn der Typ mehr Separatoren hat
|
||||
# als im Layout gefunden wurden)
|
||||
layout: str
|
||||
no: SclName
|
||||
sensor: SclName
|
||||
stopper: SclName
|
||||
# Teilesensor (stSenPart) - nur in Templates, deren Referenz ihn hat (1Sep2Swi)
|
||||
part_sensor: SclName
|
||||
|
||||
|
||||
class SclSwitchExit(BaseModel):
|
||||
"""Ein Abgang eines Weichen-TRO und die darueber erreichbaren TROs."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
via: str
|
||||
targets: list[str]
|
||||
|
||||
|
||||
class SclSwitch(BaseModel):
|
||||
"""Ein stInSwitch<n>-Block plus sein FC_Direction-Aufruf."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
no: SclName
|
||||
valve: SclName
|
||||
# None: mehrere Weichen teilen sich die Abgaenge - Aufteilung ist eine
|
||||
# Planungsentscheidung und wird nicht geraten
|
||||
to1_destinations: int | None
|
||||
# Ziel von nOutDirection, z. B. "#TRO07.nInDirection" bzw. "...nInDirection2"
|
||||
direction_target: str
|
||||
# Staubereich, dessen erster Traeger in FC_Direction ausgewertet wird
|
||||
jam_db: SclName
|
||||
|
||||
|
||||
class SclScanner(BaseModel):
|
||||
"""Ein stInBarcodeReader-Block ("" als Suffix, wenn es nur einen gibt).
|
||||
|
||||
Die Cognex-Verdrahtung (nur in den Templates): ``interface`` ist der
|
||||
Schluessel in #arCognexInterface (z. B. "cScannerBX2100"), ``tag`` der
|
||||
Praefix der Hardware-Symbole ("bx2100-TriggerReady" ...), ``hw_results``
|
||||
das Ergebnis-Symbol der Kamera.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
suffix: str
|
||||
interface: SclName
|
||||
tag: SclName
|
||||
hw_results: SclName
|
||||
|
||||
|
||||
class SclJam(BaseModel):
|
||||
"""Ein Staubereich am Ein- oder Ausgang (JamEntr<n> / JamExit<n>).
|
||||
|
||||
Ein Eingang ist ein Staubereich des TRO selbst (TroDocument.jamAreas), ein
|
||||
Ausgang der Eingangs-Staubereich des Nachfolgers.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Kennung aus der TRO-JSON (J<Separator>); "" am Ausgang zu einer Station
|
||||
jam_id: str = ""
|
||||
# Eingang: die Vorgaenger, Ausgang: der Nachfolger - als Text fuer den
|
||||
# Kommentar, SYSn als Station markiert
|
||||
neighbour: str
|
||||
db: SclName
|
||||
# Stausensor des Ausgangs (stInSenJam<n>); None an Eingaengen
|
||||
sensor: SclName | None = None
|
||||
# Traeger-Array und Staudaten des Bereichs - im Normalfall <db>.arCarrier
|
||||
# bzw. <db>.stData; an Uebergaben auch eigene Variablen (#arCarrierHandover...)
|
||||
carriers: SclName
|
||||
data: SclName
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Fuer den Kommentar: 'Staubereich J0003' (ohne Kennung nur 'Staubereich')."""
|
||||
return f"Staubereich {self.jam_id}" if self.jam_id else "Staubereich"
|
||||
|
||||
|
||||
class SclSscc(BaseModel):
|
||||
"""SSCC-Scanner eines 1Sep_SSCC: zweite Kamera, Endmessung, HMI-Anzeige."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
# interface ist hier die Struktur selbst (z. B. "#stCognexIntfTro105")
|
||||
scanner: SclScanner
|
||||
end_sensor: SclName
|
||||
# Schluessel der HMI-Anzeige, z. B. "SSCC1107"
|
||||
hmi: SclName
|
||||
|
||||
|
||||
class SclWorkStation(BaseModel):
|
||||
"""Arbeitsplatz eines Vario_workStation."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
station_sensor: SclName
|
||||
station_release: SclName
|
||||
loaded_error_sensor: SclName
|
||||
|
||||
|
||||
class SclStoreLine(BaseModel):
|
||||
"""Eine Speicherlinie eines PinStore_Auto (arInSep[n])."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Separatornummer der Linie aus dem Layout
|
||||
layout: str
|
||||
jam_length: SclName
|
||||
sensor: SclName
|
||||
jam_sensor: SclName
|
||||
stopper: SclName
|
||||
|
||||
|
||||
class SclStore(BaseModel):
|
||||
"""Alles fuer ein automatisches Pin-Lager (PinStore_Auto)."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
encoder: SclName
|
||||
dest: SclName
|
||||
conv_stations: SclName
|
||||
stations_max: SclName
|
||||
threshold: SclName
|
||||
reference_sensor: SclName
|
||||
station: SclName
|
||||
quit_local: SclName
|
||||
test_target: SclName
|
||||
run_in: SclName
|
||||
run_out: SclName
|
||||
db: SclName
|
||||
removal: SclName
|
||||
conv_db: SclName
|
||||
# Einschleus-Separator (Pin) - aus dem Layout, wenn tro_flow einen
|
||||
# steuernden Separator vor den Linien gefunden hat, sonst "?"
|
||||
pin_layout: str
|
||||
pin_sensor: SclName
|
||||
pin_stopper: SclName
|
||||
# Eine Linie je Speicherbahn im Layout (nicht die 19 aus dem Katalog)
|
||||
lines: list[SclStoreLine]
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Fuer den Kommentar: 'Staubereich J0003' (ohne Kennung nur 'Staubereich')."""
|
||||
return f"Staubereich {self.jam_id}" if self.jam_id else "Staubereich"
|
||||
|
||||
|
||||
class SclTro(BaseModel):
|
||||
"""Alles, was eine REGION fuer einen TRO braucht."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
id: str
|
||||
# Nummernteil der ID, wie geschrieben ("TRO01" -> "01"); "" ohne Ziffern
|
||||
nr: str
|
||||
type: str
|
||||
fb: str
|
||||
region_title: str
|
||||
layout_separators: list[str]
|
||||
position: tuple[float, float] | None
|
||||
predecessors: list[str]
|
||||
successors: list[str]
|
||||
confidence: str
|
||||
|
||||
separators: list[SclSeparator]
|
||||
switches: list[SclSwitch]
|
||||
switch_exits: list[SclSwitchExit]
|
||||
has_vario: bool
|
||||
# Vario-Sensoren in Aufrufreihenfolge (Parametername -> Sensor); leer ohne Vario
|
||||
vario_sensors: dict[str, SclName]
|
||||
scanners: list[SclScanner]
|
||||
storage_lines: int
|
||||
jam_entries: list[SclJam]
|
||||
jam_exits: list[SclJam]
|
||||
|
||||
main_tro: SclName
|
||||
safety: SclName
|
||||
release: str
|
||||
motor_protection: SclName
|
||||
vario_motor: SclName
|
||||
machine_state: SclName
|
||||
# Nur in den Templates (Referenz-Aufrufstellen), nicht im generischen Aufbau:
|
||||
# UDT des Prioritaetsmanagers ("udt_TRO107") und ob Richtung 2 Vorrang hat
|
||||
priority_udt: SclName
|
||||
prio_dir2: SclName
|
||||
# Kreisel-Freigabe (xInCarouselRun1). Ob ein TRO am Kreisel liegt, steht
|
||||
# nicht in der TRO-JSON - deshalb immer offen (doc/SCL_Templates.md §6).
|
||||
carousel_run: SclName
|
||||
|
||||
# -- nur fuer die Templates (Referenz-Aufrufstellen, doc/SCL_Templates.md §6)
|
||||
# Einziger Staubereich am Ein-/Ausgang fuer FBs ohne Nummerierung
|
||||
# (Vario: JamEntr/JamFinger, PinStore: JamEntr/JamExit): der erste
|
||||
# Vorgaenger bzw. Nachfolger, sonst ein offener Platzhalter
|
||||
jam_in: SclJam
|
||||
jam_out: SclJam
|
||||
error_quit: SclName
|
||||
automatic: SclName
|
||||
# Vario: stInSettings.xIsBorner (Bedeutung mit SPS klaeren)
|
||||
is_border: SclName
|
||||
# Station fuer FC_CARR_GLOB_CORRECTION (1Sep_SSCC, PinStore_Auto)
|
||||
correction_station: SclName
|
||||
sscc: SclSscc | None = None
|
||||
work_station: SclWorkStation | None = None
|
||||
store: SclStore | None = None
|
||||
|
||||
@property
|
||||
def has_scanner(self) -> bool:
|
||||
return bool(self.scanners)
|
||||
|
||||
|
||||
class SclFile(BaseModel):
|
||||
"""Kontext der ganzen Datei: Kopf und die TROs in Fliessrichtung."""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
source: str
|
||||
created: str
|
||||
start_reason: str
|
||||
tros: list[SclTro] = Field(default_factory=list)
|
||||
# Zuordnung zur E-Planung, aus der Namen eingesetzt wurden ("" = keine)
|
||||
eplan_source: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Graph
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def flow_order(tros: list[TroBase], start_id: str = "") -> tuple[list[TroBase], str]:
|
||||
"""TROs in Fliessrichtung sortieren.
|
||||
|
||||
Startpunkt ist ein TRO ohne Vorgaenger - also die Stelle, an der Material
|
||||
in die Anlage kommt. Solange Be-/Entladestation fehlen, ist der Fluss ein
|
||||
geschlossener Kreis und es gibt keinen solchen TRO; dann muss der Kreis
|
||||
irgendwo aufgetrennt werden. Das ist eine willkuerliche Wahl, deshalb wird
|
||||
sie zurueckgegeben und im Kopf der SCL vermerkt (und laesst sich mit
|
||||
--start ueberschreiben). Nicht erreichte TROs haengt die Funktion hinten
|
||||
an, damit nie einer verlorengeht.
|
||||
"""
|
||||
nach_id = {t.tro_id: t for t in tros}
|
||||
ohne_vorgaenger = sorted(t.tro_id for t in tros if not t.predecessors)
|
||||
# Materialeingang ueber eine Station (SYSn von tro_flow.py): der TRO, den
|
||||
# sie speist, ist der natuerliche Anfang - auch wenn er zusaetzlich einen
|
||||
# TRO-Vorgaenger hat (Kreisel-Umlauf).
|
||||
eingaenge = sorted({
|
||||
t.tro_id for t in tros
|
||||
for vor in t.predecessors if vor not in nach_id
|
||||
})
|
||||
|
||||
if start_id and start_id in nach_id:
|
||||
start, grund = [start_id], "per --start gesetzt"
|
||||
elif ohne_vorgaenger:
|
||||
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
|
||||
elif eingaenge:
|
||||
stationen = sorted({
|
||||
vor for i in eingaenge for vor in nach_id[i].predecessors
|
||||
if vor not in nach_id
|
||||
})
|
||||
start = eingaenge
|
||||
grund = f"Materialeingang ueber {', '.join(stationen)}"
|
||||
else:
|
||||
start = [min(nach_id)]
|
||||
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
|
||||
"Be-/Entladestation im Export stehen, ergibt sich der Anfang")
|
||||
|
||||
gesehen: list[str] = []
|
||||
queue = deque(start)
|
||||
while queue:
|
||||
tro_id = queue.popleft()
|
||||
if tro_id in gesehen:
|
||||
continue
|
||||
gesehen.append(tro_id)
|
||||
for nxt in nach_id[tro_id].successors if tro_id in nach_id else []:
|
||||
# SYSn (Materialausgang) ist kein TRO - keine REGION, keine Instanz.
|
||||
if nxt in nach_id and nxt not in gesehen:
|
||||
queue.append(nxt)
|
||||
for tro_id in sorted(nach_id):
|
||||
if tro_id not in gesehen:
|
||||
gesehen.append(tro_id)
|
||||
return [nach_id[i] for i in gesehen], grund
|
||||
|
||||
|
||||
def reachable_via(start: str, first: str, tros: dict[str, TroBase]) -> list[str]:
|
||||
"""Welche TROs sind ueber diesen Abgang erreichbar?
|
||||
|
||||
Das ist die Zielliste eines Weichenabgangs: von `first` aus weiterlaufen,
|
||||
ohne ueber die Weiche selbst zurueckzugehen. Genau die Information, die
|
||||
FC_Direction braucht, um zu entscheiden - und sie steht im Graphen.
|
||||
"""
|
||||
gesehen: set[str] = set()
|
||||
queue = deque([first])
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node in gesehen or node == start:
|
||||
continue
|
||||
gesehen.add(node)
|
||||
if node in tros:
|
||||
queue.extend(tros[node].successors)
|
||||
return sorted(gesehen)
|
||||
|
||||
|
||||
def _nachbar(node: str, alle: dict[str, TroBase]) -> str:
|
||||
"""Nachbar fuer Kommentare; SYSn als Station kenntlich machen."""
|
||||
return node if node in alle else f"{node} (Station, kein TRO)"
|
||||
|
||||
|
||||
def _region_title(tro: TroBase) -> str:
|
||||
seps = ", ".join(tro.separators)
|
||||
teile = f" Sep {seps}" if seps else ""
|
||||
return f"{tro.tro_id} ({tro.type_name}){teile}"
|
||||
|
||||
|
||||
def _nummer(tro_id: str) -> str:
|
||||
treffer = re.search(r"(\d+)$", tro_id)
|
||||
return treffer.group(1) if treffer else ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kontext bauen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _scanner(n: int, suffix: str, interface: str = "cScanner____") -> SclScanner:
|
||||
return SclScanner(
|
||||
n=n, suffix=suffix, interface=_offen(interface), tag=_offen("bx____"),
|
||||
hw_results=_offen("BX____~Ergebnisse_\u2013_64_Bytes_1"),
|
||||
)
|
||||
|
||||
|
||||
def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None,
|
||||
jam_id: str = "") -> SclJam:
|
||||
return SclJam(
|
||||
n=n, jam_id=jam_id, neighbour=neighbour, db=_offen(jam_db), sensor=sensor,
|
||||
carriers=_offen(f"{jam_db}.arCarrier"), data=_offen(f"{jam_db}.stData"),
|
||||
)
|
||||
|
||||
|
||||
def _store(tro: TroBase, eplan: EplanDocument | None = None) -> SclStore:
|
||||
"""PinStore_Auto: eine Linie je Speicherbahn (Host) des Layouts.
|
||||
|
||||
tro_flow legt die Separatoren als [steuernde ..., Linien ...] ab; die
|
||||
Linien sind die letzten len(hosts). Fehlen Hosts (fremde JSON), zaehlt
|
||||
jeder Separator als Linie. Sensor und Stopper jeder Linie (und des
|
||||
Pin-Separators, falls tro_flow einen gefunden hat) kommen aus der
|
||||
E-Planung wie bei den Separatorbloecken.
|
||||
"""
|
||||
hosts = list(getattr(tro, "hosts", []) or [])
|
||||
seps = list(tro.separators)
|
||||
anzahl = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
|
||||
steuer, linien = seps[:len(seps) - anzahl], seps[len(seps) - anzahl:]
|
||||
pin_sensor, pin_stopper = _sep_namen(steuer[0], eplan) if steuer else (
|
||||
_offen("cInBG____"), _offen("MB____"))
|
||||
zeilen = []
|
||||
for n, sep in enumerate(linien, start=1):
|
||||
sensor, stopper = _sep_namen(sep, eplan)
|
||||
zeilen.append(SclStoreLine(n=n, layout=sep,
|
||||
jam_length=_offen('"cStorage____ExitJam"'),
|
||||
sensor=sensor, jam_sensor=_offen("cInBG____"),
|
||||
stopper=stopper))
|
||||
return SclStore(
|
||||
encoder=_offen("#fbEncoder____"),
|
||||
dest=_offen("cDest____"),
|
||||
conv_stations=_offen("cHooksSorterMaxPin____"),
|
||||
stations_max=_offen("cStationsMax____"),
|
||||
threshold=_offen("cNoPositionsBlock____"),
|
||||
reference_sensor=_offen("cInBG____"),
|
||||
station=_offen("____"),
|
||||
quit_local=_offen('"SF____"'),
|
||||
test_target=_offen('"IBN".nTestTarget____'),
|
||||
run_in=_offen("#fbSortConv____.xOutConveyorFullSpeed"),
|
||||
run_out=_offen("#fbExitConv____.xOutConveyorFullSpeed"),
|
||||
db=_offen('"DB_Storage____"'),
|
||||
removal=_offen('"DB_HandshakeStorage".Str_StorageRemoval.'
|
||||
'arrudt_StorageRemovalHandshakeTROsLvl1[____]'),
|
||||
conv_db=_offen('"DB_ConveyorStations".stConveyorStations____'),
|
||||
pin_layout=", ".join(steuer) or "?",
|
||||
pin_sensor=pin_sensor,
|
||||
pin_stopper=pin_stopper,
|
||||
lines=zeilen,
|
||||
)
|
||||
|
||||
|
||||
def _sep_namen(layout: str, eplan: EplanDocument | None) -> tuple[SclName, SclName]:
|
||||
"""Sensor und Stopperventil eines Layout-Separators aus der E-Planung.
|
||||
|
||||
Gilt fuer die Separatorbloecke (stInSeparator<n>) und genauso fuer die
|
||||
Linien eines PinStore_Auto - beide sind Separatoren im Layout. Ohne
|
||||
Zuordnung bleiben beide offen.
|
||||
"""
|
||||
rollen = eplan.for_layout(layout) if eplan is not None and layout != "?" else {}
|
||||
sensor = rollen.get("sensor")
|
||||
stopper = rollen.get("stopper")
|
||||
return (_bekannt(_SENSOR.format(bmk=sensor.bmk)) if sensor else _offen("cInBG____"),
|
||||
_bekannt(_VENTIL.format(bmk=stopper.bmk)) if stopper else _offen("MB____"))
|
||||
|
||||
|
||||
def _separator(i: int, layout: str, eplan: EplanDocument | None) -> SclSeparator:
|
||||
sensor, stopper = _sep_namen(layout, eplan)
|
||||
return SclSeparator(
|
||||
n=i,
|
||||
layout=layout,
|
||||
no=_offen("cSep_____"),
|
||||
sensor=sensor,
|
||||
stopper=stopper,
|
||||
# Teilesensor kennt die E-Planung-Bruecke noch nicht - offen
|
||||
part_sensor=_offen("cInBG____"),
|
||||
)
|
||||
|
||||
|
||||
def _entries(tro: TroBase, eigene: list[JamArea],
|
||||
alle: dict[str, TroBase], jam_db: str) -> list[SclJam]:
|
||||
"""Ein JamEntr je eigenem Staubereich - nicht je Vorgaenger: mehrere
|
||||
Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05)."""
|
||||
eingaenge = []
|
||||
for area in eigene:
|
||||
von = ", ".join(_nachbar(v, alle) for v in area.predecessors) or "-"
|
||||
if area.note:
|
||||
von = f"{von} ({area.note})"
|
||||
eingaenge.append(_jam(area.entry, von, jam_db, jam_id=area.jam_id))
|
||||
return eingaenge
|
||||
|
||||
|
||||
def _exit_area(tro_id: str, nach: str,
|
||||
bereiche: dict[str, list[JamArea]]) -> str:
|
||||
"""Kennung des Staubereichs, in den tro_id beim Nachfolger einfaehrt."""
|
||||
ziel = bereiche.get(nach, [])
|
||||
passend = [a for a in ziel if tro_id in a.predecessors] or ziel
|
||||
return passend[0].jam_id if passend else ""
|
||||
|
||||
|
||||
def _tro_context(tro: TroBase, alle: dict[str, TroBase],
|
||||
eplan: EplanDocument | None = None,
|
||||
bereiche: dict[str, list[JamArea]] | None = None) -> SclTro:
|
||||
items = tro.items
|
||||
n_sep = items.get(TroItem.SEPARATOR, 1)
|
||||
n_swi = items.get(TroItem.SWITCH, 0)
|
||||
n_scan = items.get(TroItem.SCANNER, 0)
|
||||
tro_id = tro.tro_id
|
||||
jam_db = '"DB_JamArea____".stJam____'
|
||||
bereiche = bereiche or {}
|
||||
|
||||
separators = [
|
||||
_separator(i, tro.separators[i - 1] if i <= len(tro.separators) else "?", eplan)
|
||||
for i in range(1, n_sep + 1)
|
||||
]
|
||||
switches = [
|
||||
SclSwitch(
|
||||
n=j,
|
||||
no=_offen("cSwi_____"),
|
||||
valve=_offen("MB____"),
|
||||
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
|
||||
to1_destinations=len(tro.successors) if n_swi == 1 else None,
|
||||
direction_target=(f"#{tro_id}.nInDirection" if n_swi == 1
|
||||
else f"#{tro_id}.nInDirection{j}"),
|
||||
jam_db=_offen(jam_db),
|
||||
)
|
||||
for j in range(1, n_swi + 1)
|
||||
]
|
||||
has_vario = bool(items.get(TroItem.VARIO_DRIVE))
|
||||
vario_sensors = {
|
||||
name: _offen("cInBG____")
|
||||
for name in ("stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam")
|
||||
} if has_vario else {}
|
||||
switch_exits = [
|
||||
SclSwitchExit(via=abgang, targets=reachable_via(tro_id, abgang, alle))
|
||||
for abgang in tro.successors
|
||||
] if n_swi else []
|
||||
# Staubereiche aus der TRO-JSON (jamAreas): Eingaenge sind die eigenen
|
||||
# (ein JamEntr je eigenem Staubereich, nicht je Vorgaenger), Ausgaenge die
|
||||
# Eingaenge der Nachfolger. Der DB-Name braucht die Foerderernummer aus der
|
||||
# Hardwareplanung und bleibt offen.
|
||||
jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, jam_db)
|
||||
jam_exits = [
|
||||
_jam(n, _nachbar(nach, alle), jam_db, sensor=_offen("cInBG____"),
|
||||
jam_id=_exit_area(tro_id, nach, bereiche))
|
||||
for n, nach in enumerate(tro.successors, start=2)
|
||||
]
|
||||
|
||||
return SclTro(
|
||||
id=tro_id,
|
||||
nr=_nummer(tro_id),
|
||||
type=tro.type_name,
|
||||
fb=tro.fb_block or "FB_UNBEKANNT",
|
||||
region_title=_region_title(tro),
|
||||
layout_separators=list(tro.separators),
|
||||
position=(tro.x, tro.y) if tro.position else None,
|
||||
predecessors=list(tro.predecessors),
|
||||
successors=list(tro.successors),
|
||||
confidence=tro.confidence,
|
||||
separators=separators,
|
||||
switches=switches,
|
||||
switch_exits=switch_exits,
|
||||
has_vario=has_vario,
|
||||
vario_sensors=vario_sensors,
|
||||
scanners=[
|
||||
_scanner(i, "" if n_scan == 1 else str(i))
|
||||
for i in range(1, n_scan + 1)
|
||||
],
|
||||
storage_lines=items.get(TroItem.STORAGE_LINE, 0),
|
||||
jam_entries=jam_entries,
|
||||
jam_exits=jam_exits,
|
||||
main_tro=_offen("cMainTro____"),
|
||||
safety=_offen('"DB_InterfaceSafety".stFromSafety.EStopMain.Q'),
|
||||
release=f'"DB_TRO_TEST".{tro_id}',
|
||||
motor_protection=_offen('"FC____"'),
|
||||
vario_motor=_offen("MA____"),
|
||||
# Schaltschrank ist anlagenspezifisch ("...OF1" in der Referenz)
|
||||
machine_state=_offen("#stInOutControlUnitCabinet"),
|
||||
priority_udt=_offen("udt_____"),
|
||||
# Vorschlag aus der Referenz TRO107, Prioritaet bleibt Planungsentscheidung
|
||||
prio_dir2=_offen("TRUE"),
|
||||
carousel_run=_offen("#fbCarousel____.xOutConveyorFullSpeed"),
|
||||
# dasselbe Objekt wie der erste Ein-/Ausgang: ein Name, den die
|
||||
# Namensregeln dort setzen, gilt damit auch hier
|
||||
jam_in=jam_entries[0] if jam_entries else _jam(1, "-", jam_db),
|
||||
jam_out=jam_exits[0] if jam_exits else _jam(2, "-", jam_db),
|
||||
error_quit=_offen('"SF____"'),
|
||||
automatic=_offen("#t_automatik"),
|
||||
is_border=_offen("TRUE"),
|
||||
correction_station=_offen("0"),
|
||||
sscc=(SclSscc(scanner=_scanner(1, "SSCC", "#stCognexIntf____"),
|
||||
end_sensor=_offen("cInBG____"), hmi=_offen("SSCC____"))
|
||||
if items.get(TroItem.SSCC) else None),
|
||||
work_station=(SclWorkStation(station_sensor=_offen("cInBG____"),
|
||||
station_release=_offen('#"rTrig_Workstation ____".CLK'),
|
||||
loaded_error_sensor=_offen("cInBG____"))
|
||||
if items.get(TroItem.WORK_STATION) else None),
|
||||
store=_store(tro, eplan) if items.get(TroItem.STORAGE_LINE) else None,
|
||||
)
|
||||
|
||||
|
||||
def build_context(document: TroDocument, source: str, start_id: str = "",
|
||||
created: str | None = None,
|
||||
eplan: EplanDocument | None = None) -> SclFile:
|
||||
"""TroDocument -> SclFile (TROs in Fliessrichtung).
|
||||
|
||||
eplan: Zuordnung Layout -> E-Planung (e_planning.py); ohne sie bleiben
|
||||
alle Namen offen und die Ausgabe ist die bisherige.
|
||||
"""
|
||||
alle = {t.tro_id: t for t in document.tros}
|
||||
bereiche: dict[str, list[JamArea]] = {}
|
||||
for area in document.jam_areas_or_derived():
|
||||
bereiche.setdefault(area.tro, []).append(area)
|
||||
geordnet, startgrund = flow_order(document.tros, start_id)
|
||||
return SclFile(
|
||||
source=source,
|
||||
created=created or datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
start_reason=startgrund,
|
||||
tros=[_tro_context(t, alle, eplan, bereiche) for t in geordnet],
|
||||
eplan_source=(f"{eplan.eplan_file} (Zuordnung vom {eplan.created})"
|
||||
if eplan is not None else ""),
|
||||
)
|
||||
+345
-204
@@ -7,11 +7,14 @@ scl_skeleton.py - FB_Main-Geruest als SCL aus der TRO-Liste erzeugen.
|
||||
Letzter Schritt der Kette:
|
||||
|
||||
material_flow.py Objekte -> Materialfluss
|
||||
tro_flow.py Fluss -> TROs
|
||||
tro_annotate.py TROs -> Marker in der Zeichnung
|
||||
tro_extract.py Zeichnung -> JSON
|
||||
tro_flow.py Fluss -> TROs -> <name>_tro.json (--json)
|
||||
scl_skeleton.py JSON -> FB_Main.scl <- hier
|
||||
|
||||
Die TRO-JSON wird ueber das gemeinsame Modell lib/model.py (TroDocument)
|
||||
eingelesen und dabei geprueft: ein falscher oder umbenannter Schluessel bricht
|
||||
mit einer Fehlermeldung ab, statt still einen Standardwert zu nehmen. Bauteile
|
||||
(items) und FB-Typ kommen aus tro_catalog, ueber den TRO-Typ.
|
||||
|
||||
Erzeugt wird ein **Geruest**, kein fertiges Programm: je TRO eine REGION mit
|
||||
Instanzaufruf und allen Parameterzeilen in der richtigen Reihenfolge, so wie es
|
||||
die realen Aufrufstellen in doc/TRO_Katalog/scl_templates/*.scl zeigen.
|
||||
@@ -37,13 +40,61 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from material_flow import env_dir
|
||||
from jinja2 import (
|
||||
Environment,
|
||||
FileSystemLoader,
|
||||
StrictUndefined,
|
||||
TemplateError,
|
||||
TemplateNotFound,
|
||||
)
|
||||
from pydantic import ValidationError
|
||||
|
||||
TODO = "TODO(E-Planung)"
|
||||
from e_planning import EPLAN_SCHEMA, EplanDocument
|
||||
from material_flow import env_dir
|
||||
from model import SCHEMA, TroDocument
|
||||
from scl_context import TODO, SclFile, SclName, SclTro, build_context
|
||||
|
||||
# Parametrisierte Aufrufstellen je FB (doc/SCL_Templates.md); Dateiname = fbType.
|
||||
TEMPLATE_DIR = Path(__file__).resolve().parent / "scl_templates"
|
||||
TEMPLATE_SUFFIX = ".scl.j2"
|
||||
|
||||
|
||||
class SclTemplates:
|
||||
"""Die FB-Templates eines Ordners; ohne Template -> generischer Aufbau."""
|
||||
|
||||
def __init__(self, folder: Path) -> None:
|
||||
self.folder = folder
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(str(folder)),
|
||||
# Tippfehler im Template -> Fehler statt leerer SCL-Stelle
|
||||
undefined=StrictUndefined,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=True,
|
||||
autoescape=False,
|
||||
)
|
||||
self.env.globals["TODO"] = TODO
|
||||
|
||||
def has(self, fb: str) -> bool:
|
||||
return (self.folder / f"{fb}{TEMPLATE_SUFFIX}").is_file()
|
||||
|
||||
def render(self, tro: SclTro) -> str:
|
||||
"""REGION eines TRO aus seinem Template (ohne abschliessenden Umbruch)."""
|
||||
try:
|
||||
template = self.env.get_template(f"{tro.fb}{TEMPLATE_SUFFIX}")
|
||||
except TemplateNotFound as exc:
|
||||
raise TemplateError(f"Template fuer {tro.fb} fehlt: {exc}") from exc
|
||||
# Leerzeilen hier zusammenfassen statt im Template mit Whitespace-
|
||||
# Steuerung ({%- ... -%}) - die macht Templates schwer lesbar und
|
||||
# bricht leicht die Einrueckung.
|
||||
zeilen: list[str] = []
|
||||
for zeile in template.render(t=tro).split("\n"):
|
||||
zeile = zeile.rstrip()
|
||||
if zeile or (zeilen and zeilen[-1]):
|
||||
zeilen.append(zeile)
|
||||
return "\n".join(zeilen).strip("\n")
|
||||
|
||||
# Zeitwerte aus den Referenz-Aufrufstellen. Sie sind plausible Startwerte, KEINE
|
||||
# fuer diese Anlage ermittelten - deshalb ueberall mit Pruefhinweis.
|
||||
@@ -55,123 +106,63 @@ DEFAULT_SEP_TIMES = (
|
||||
)
|
||||
|
||||
|
||||
def load_tros(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
def load_document(path: Path) -> TroDocument:
|
||||
"""TRO-JSON einlesen und gegen das Modell pruefen."""
|
||||
return TroDocument.from_json(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def flow_order(tros: list[dict], start_id: str = "") -> tuple[list[dict], str]:
|
||||
"""TROs in Fliessrichtung sortieren.
|
||||
|
||||
Startpunkt ist ein TRO ohne Vorgaenger - also die Stelle, an der Material
|
||||
in die Anlage kommt. Solange Be-/Entladestation fehlen, ist der Fluss ein
|
||||
geschlossener Kreis und es gibt keinen solchen TRO; dann muss der Kreis
|
||||
irgendwo aufgetrennt werden. Das ist eine willkuerliche Wahl, deshalb wird
|
||||
sie zurueckgegeben und im Kopf der SCL vermerkt (und laesst sich mit
|
||||
--start ueberschreiben). Nicht erreichte TROs haengt die Funktion hinten
|
||||
an, damit nie einer verlorengeht.
|
||||
"""
|
||||
nach_id = {t["id"]: t for t in tros}
|
||||
ohne_vorgaenger = sorted(t["id"] for t in tros if not t.get("predecessors"))
|
||||
|
||||
if start_id and start_id in nach_id:
|
||||
start, grund = [start_id], "per --start gesetzt"
|
||||
elif ohne_vorgaenger:
|
||||
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
|
||||
else:
|
||||
start = [min(nach_id)]
|
||||
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
|
||||
"Be-/Entladestation im Export stehen, ergibt sich der Anfang")
|
||||
|
||||
gesehen: list[str] = []
|
||||
queue = deque(start)
|
||||
while queue:
|
||||
tro_id = queue.popleft()
|
||||
if tro_id in gesehen:
|
||||
continue
|
||||
gesehen.append(tro_id)
|
||||
for nxt in nach_id.get(tro_id, {}).get("successors", []):
|
||||
if nxt not in gesehen:
|
||||
queue.append(nxt)
|
||||
for tro_id in sorted(nach_id):
|
||||
if tro_id not in gesehen:
|
||||
gesehen.append(tro_id)
|
||||
return [nach_id[i] for i in gesehen], grund
|
||||
def _todo(name: SclName, abstand: str) -> str:
|
||||
"""TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist."""
|
||||
return f"{abstand}// {TODO}" if name.todo else ""
|
||||
|
||||
|
||||
def reachable_via(start: str, first: str, tros: dict[str, dict]) -> list[str]:
|
||||
"""Welche TROs sind ueber diesen Abgang erreichbar?
|
||||
|
||||
Das ist die Zielliste eines Weichenabgangs: von `first` aus weiterlaufen,
|
||||
ohne ueber die Weiche selbst zurueckzugehen. Genau die Information, die
|
||||
FC_Direction braucht, um zu entscheiden - und sie steht im Graphen.
|
||||
"""
|
||||
gesehen: set[str] = set()
|
||||
queue = deque([first])
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node in gesehen or node == start:
|
||||
continue
|
||||
gesehen.add(node)
|
||||
queue.extend(tros.get(node, {}).get("successors", []))
|
||||
return sorted(gesehen)
|
||||
|
||||
|
||||
def region_title(tro: dict) -> str:
|
||||
seps = ", ".join(tro.get("separators", []))
|
||||
teile = f" Sep {seps}" if seps else ""
|
||||
return f"{tro['id']} ({tro['type']}){teile}"
|
||||
|
||||
|
||||
def emit_separators(add, tro: dict, anzahl: int, hat_scanner: bool) -> None:
|
||||
seps = tro.get("separators", [])
|
||||
for i in range(1, anzahl + 1):
|
||||
pfad = f"#{tro['id']}.stInSeparator{i}"
|
||||
layout = seps[i - 1] if i <= len(seps) else "?"
|
||||
add(f" // Separator {i} = Bauteil {layout} aus dem Layout")
|
||||
add(f' {pfad}.Settings.nSeparatorNo := "cSep_____"; // {TODO}')
|
||||
def emit_separators(add, tro: SclTro) -> None:
|
||||
for s in tro.separators:
|
||||
pfad = f"#{tro.id}.stInSeparator{s.n}"
|
||||
add(f" // Separator {s.n} = Bauteil {s.layout} aus dem Layout")
|
||||
add(f' {pfad}.Settings.nSeparatorNo := "{s.no.value}";{_todo(s.no, " ")}')
|
||||
for name, wert, hinweis in DEFAULT_SEP_TIMES:
|
||||
kommentar = f" // {TODO}: {hinweis}" if hinweis else ""
|
||||
add(f" {pfad}.Settings.{name} := {wert};{kommentar}")
|
||||
add(f" {pfad}.Settings.xPluggedJam := TRUE;")
|
||||
add(f" {pfad}.Settings.xPluggedPart := FALSE;")
|
||||
add(f" {pfad}.Settings.xPluggedScanner := "
|
||||
f"{'TRUE' if hat_scanner else 'FALSE'};")
|
||||
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
f"{'TRUE' if tro.has_scanner else 'FALSE'};")
|
||||
add(f' {pfad}.stSenInSep := "DB_Inputs".Sensors["{s.sensor.value}"];'
|
||||
f'{_todo(s.sensor, " ")}')
|
||||
add(f" {pfad}.Settings.tSenFree := T#1500ms; // {TODO}: pruefen")
|
||||
add(f" {pfad}.Settings.tSenWait := T#4000ms; // {TODO}: pruefen")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_switches(add, tro: dict, anzahl: int, alle: dict[str, dict]) -> None:
|
||||
abgaenge = tro.get("successors", [])
|
||||
for j in range(1, anzahl + 1):
|
||||
pfad = f"#{tro['id']}.stInSwitch{j}"
|
||||
add(f' {pfad}.Settings.nSwitchNo := "cSwi_____"; // {TODO}')
|
||||
def emit_switches(add, tro: SclTro) -> None:
|
||||
for w in tro.switches:
|
||||
pfad = f"#{tro.id}.stInSwitch{w.n}"
|
||||
add(f' {pfad}.Settings.nSwitchNo := "{w.no.value}";{_todo(w.no, " ")}')
|
||||
add(f" {pfad}.Settings.xPluggedExSen3 := FALSE;")
|
||||
add(f" {pfad}.Settings.xPluggedExSen4 := FALSE;")
|
||||
if anzahl == 1:
|
||||
# Eine Weiche bedient alle Abgaenge - direkt ableitbar.
|
||||
add(f" {pfad}.Settings.nTo1Destinations := {len(abgaenge)};"
|
||||
if w.to1_destinations is not None:
|
||||
add(f" {pfad}.Settings.nTo1Destinations := {w.to1_destinations};"
|
||||
f" // Abgaenge laut Topologie")
|
||||
else:
|
||||
# Bei mehreren Weichen ist es eine Planungsentscheidung, welche
|
||||
# Weiche welche Abgaenge bedient - die Summe ist bekannt, die
|
||||
# Aufteilung nicht. Deshalb hier kein geratener Wert.
|
||||
add(f" {pfad}.Settings.nTo1Destinations := _;"
|
||||
f" // {TODO}: dieser TRO hat {len(abgaenge)} "
|
||||
f"Abgaenge auf {anzahl} Weichen - Aufteilung festlegen")
|
||||
f" // {TODO}: dieser TRO hat {len(tro.successors)} "
|
||||
f"Abgaenge auf {len(tro.switches)} Weichen - Aufteilung festlegen")
|
||||
add("")
|
||||
if abgaenge:
|
||||
if tro.switch_exits:
|
||||
add(" // Zielliste je Abgang (aus dem TRO-Graphen berechnet) -")
|
||||
add(" // Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:")
|
||||
for abgang in abgaenge:
|
||||
ziele = reachable_via(tro["id"], abgang, alle)
|
||||
add(f" // ueber {abgang}: {', '.join(ziele) if ziele else '-'}")
|
||||
for abgang in tro.switch_exits:
|
||||
ziele = ", ".join(abgang.targets) if abgang.targets else "-"
|
||||
add(f" // ueber {abgang.via}: {ziele}")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_vario(add, tro: dict) -> None:
|
||||
pfad = f"#{tro['id']}.stInVario"
|
||||
def emit_vario(add, tro: SclTro) -> None:
|
||||
pfad = f"#{tro.id}.stInVario"
|
||||
add(f" {pfad}.Settings.xContinousEmptying := FALSE;")
|
||||
add(f" {pfad}.Settings.tWaitForJog := T#300ms; // {TODO}: pruefen")
|
||||
add(f" {pfad}.Settings.tConvEmpty := T#45000ms; // {TODO}: pruefen")
|
||||
@@ -181,17 +172,18 @@ def emit_vario(add, tro: dict) -> None:
|
||||
add(f" {pfad}.Settings.tCarrierInPosEdgeDelay := t#100ms;")
|
||||
add(f" {pfad}.Settings.xJamSenDelayActive := TRUE;")
|
||||
add(f" {pfad}.Settings.tJamSenDelay := T#2s;")
|
||||
add(f' {pfad}.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
add(f' {pfad}.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // {TODO}')
|
||||
# Der Kommentar steht bei allen Sensoren in derselben Spalte
|
||||
# (Parametername + Abstand = 16 Zeichen).
|
||||
for name, sensor in tro.vario_sensors.items():
|
||||
abstand = " " * (16 - len(name))
|
||||
add(f' {pfad}.{name} := "DB_Inputs".Sensors["{sensor.value}"];'
|
||||
f"{_todo(sensor, abstand)}")
|
||||
add("")
|
||||
|
||||
|
||||
def emit_barcode(add, tro: dict, anzahl: int) -> None:
|
||||
for i in range(1, anzahl + 1):
|
||||
suffix = "" if anzahl == 1 else str(i)
|
||||
pfad = f"#{tro['id']}.stInBarcodeReader{suffix}"
|
||||
def emit_barcode(add, tro: SclTro) -> None:
|
||||
for scanner in tro.scanners:
|
||||
pfad = f"#{tro.id}.stInBarcodeReader{scanner.suffix}"
|
||||
add(f" {pfad}.Settings.xActivateCheckASCI := TRUE;")
|
||||
add(f" {pfad}.Settings.tTimeout := T#3000ms;")
|
||||
add(f' // {TODO}: Cognex-Interface verdrahten '
|
||||
@@ -199,49 +191,55 @@ def emit_barcode(add, tro: dict, anzahl: int) -> None:
|
||||
add("")
|
||||
|
||||
|
||||
def emit_call(add, tro: dict, items: dict[str, int]) -> None:
|
||||
def emit_call(add, tro: SclTro) -> None:
|
||||
"""Aufrufstelle schreiben.
|
||||
|
||||
Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim
|
||||
Zusammensetzen gesetzt - sonst landet es hinter dem "//" und ist Teil des
|
||||
Kommentars, womit die Parameterliste nicht mehr uebersetzt.
|
||||
"""
|
||||
tro_id = tro["id"]
|
||||
def offen(name: SclName, text: str = "") -> str:
|
||||
if not name.todo:
|
||||
return text
|
||||
return f"{TODO} - {text}" if text else TODO
|
||||
|
||||
zeilen: list[tuple[str, str]] = [
|
||||
('nInMainTroNo := "cMainTro____"', TODO),
|
||||
('xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q', TODO),
|
||||
(f'nInMainTroNo := "{tro.main_tro.value}"', offen(tro.main_tro)),
|
||||
(f"xInSftyOk := {tro.safety.value}", offen(tro.safety)),
|
||||
("xInAllRdyToStart := TRUE", ""),
|
||||
(f'xInRelease := "DB_TRO_TEST".{tro_id}', ""),
|
||||
(f"xInRelease := {tro.release}", ""),
|
||||
]
|
||||
if items.get("VarioDrive"):
|
||||
zeilen.append(('xInMotorProtection := "FC____"', TODO))
|
||||
if tro.has_vario:
|
||||
zeilen.append((f"xInMotorProtection := {tro.motor_protection.value}",
|
||||
offen(tro.motor_protection)))
|
||||
zeilen.append(("xInAutomatic := #t_automatik", ""))
|
||||
zeilen.append(("stInOutMachineState := #stInOutControlUnitCabinet", ""))
|
||||
# Der generische Aufbau hat den Schaltschrank nie als offen markiert;
|
||||
# das bleibt so, damit seine Ausgabe stabil ist (Golden-Files). Die
|
||||
# Templates markieren ihn.
|
||||
zeilen.append((f"stInOutMachineState := {tro.machine_state.value}", ""))
|
||||
zeilen.append(("stInOutCognexInterface := #stCognexInterfaceDummy", ""))
|
||||
|
||||
# Staubereiche: je ankommender/abgehender Verbindung einer. Welche
|
||||
# DB-Struktur dahintersteht, ist eine Planungsentscheidung - hier steht
|
||||
# nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren.
|
||||
for n, vor in enumerate(tro.get("predecessors", []), start=1):
|
||||
zeilen.append((f'arInOutJamEntr{n} := "DB_JamArea____".stJam____.arCarrier',
|
||||
f"{TODO} - Staubereich von {vor}"))
|
||||
zeilen.append((f'stInOutJamEntr{n} := "DB_JamArea____".stJam____.stData', ""))
|
||||
for n, nach in enumerate(tro.get("successors", []), start=2):
|
||||
zeilen.append((f'arInOutJamExit{n} := "DB_JamArea____".stJam____.arCarrier',
|
||||
f"{TODO} - Staubereich nach {nach}"))
|
||||
zeilen.append((f'stInOutJamExit{n} := "DB_JamArea____".stJam____.stData', ""))
|
||||
for jam in tro.jam_entries:
|
||||
zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"{jam.title} von {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamEntr{jam.n} := {jam.db.value}.stData", ""))
|
||||
for jam in tro.jam_exits:
|
||||
zeilen.append((f"arInOutJamExit{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"{jam.title} vor {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", ""))
|
||||
|
||||
for i in range(1, items.get("Separator", 1) + 1):
|
||||
zeilen.append((f'xOutStopper{i} => "MB____"', TODO))
|
||||
if items.get("VarioDrive"):
|
||||
zeilen.append(('xOutVarioMotor => "MA____"', TODO))
|
||||
for j in range(1, items.get("Switch", 0) + 1):
|
||||
zeilen.append((f'xOutSw{j}ExTo3 => "MB____"', TODO))
|
||||
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro_id}', ""))
|
||||
zeilen.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]',
|
||||
TODO))
|
||||
for s in tro.separators:
|
||||
zeilen.append((f'xOutStopper{s.n} => "{s.stopper.value}"', offen(s.stopper)))
|
||||
if tro.has_vario:
|
||||
zeilen.append((f'xOutVarioMotor => "{tro.vario_motor.value}"',
|
||||
offen(tro.vario_motor)))
|
||||
for w in tro.switches:
|
||||
zeilen.append((f'xOutSw{w.n}ExTo3 => "{w.valve.value}"', offen(w.valve)))
|
||||
zeilen.append((f'stInOutHMI := "DB_Interface_HMI".stTRO.{tro.id}', ""))
|
||||
zeilen.append((f'nOutStateLast => "DB_GLOB_TroState".arTroState["{tro.main_tro.value}"]',
|
||||
offen(tro.main_tro)))
|
||||
|
||||
kopf = f" #{tro_id}("
|
||||
kopf = f" #{tro.id}("
|
||||
einzug = " " * len(kopf)
|
||||
for index, (code, kommentar) in enumerate(zeilen):
|
||||
trenner = ");" if index == len(zeilen) - 1 else ","
|
||||
@@ -250,34 +248,75 @@ def emit_call(add, tro: dict, items: dict[str, int]) -> None:
|
||||
add(f"{text} // {kommentar}" if kommentar else text)
|
||||
|
||||
|
||||
def emit_direction(add, tro: dict, anzahl: int) -> None:
|
||||
def emit_direction(add, tro: SclTro) -> None:
|
||||
"""Weichenlogik - je Weiche ein FC_Direction-Aufruf."""
|
||||
add("")
|
||||
add(" REGION Direction")
|
||||
for j in range(1, anzahl + 1):
|
||||
ziel = (f"#{tro['id']}.nInDirection" if anzahl == 1
|
||||
else f"#{tro['id']}.nInDirection{j}")
|
||||
add(f' "FC_Direction"(nInSwitchNo := "cSwi_____", // {TODO}')
|
||||
add(f" nOutDirection => {ziel},")
|
||||
add(f' InOutCarrier := "DB_JamArea____".stJam____'
|
||||
f".arCarrier[1]);")
|
||||
if j < anzahl:
|
||||
for w in tro.switches:
|
||||
add(f' "FC_Direction"(nInSwitchNo := "{w.no.value}",{_todo(w.no, " ")}')
|
||||
add(f" nOutDirection => {w.direction_target},")
|
||||
add(f" InOutCarrier := {w.jam_db.value}.arCarrier[1]);")
|
||||
if w.n < len(tro.switches):
|
||||
add("")
|
||||
add(" END_REGION")
|
||||
|
||||
|
||||
def render(daten: dict, quelle: str, start_id: str = "") -> str:
|
||||
tros = daten["tros"]
|
||||
alle = {t["id"]: t for t in tros}
|
||||
geordnet, startgrund = flow_order(tros, start_id)
|
||||
def emit_region(add, tro: SclTro) -> None:
|
||||
"""Eine REGION je TRO (heutiger, generischer Aufbau aus den Bauteilen)."""
|
||||
add("")
|
||||
add(f"REGION {tro.region_title}")
|
||||
if tro.position:
|
||||
add(f" // Anlagenkoordinate x/y: {tro.position[0]:.0f} / {tro.position[1]:.0f} mm")
|
||||
add(f" // Vorgaenger: {', '.join(tro.predecessors) or '-'}")
|
||||
add(f" // Nachfolger: {', '.join(tro.successors) or '-'}")
|
||||
if tro.confidence:
|
||||
add(f" // Einstufung: {tro.confidence}")
|
||||
add("")
|
||||
|
||||
emit_separators(add, tro)
|
||||
if tro.switches:
|
||||
emit_switches(add, tro)
|
||||
if tro.has_vario:
|
||||
emit_vario(add, tro)
|
||||
if tro.scanners:
|
||||
emit_barcode(add, tro)
|
||||
if tro.storage_lines:
|
||||
add(f" // {TODO}: Linienspeicher mit {tro.storage_lines} Linien - "
|
||||
f"Parametrierung je Linie")
|
||||
add("")
|
||||
|
||||
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro.id)
|
||||
add("")
|
||||
emit_call(add, tro)
|
||||
if tro.switches:
|
||||
emit_direction(add, tro)
|
||||
add("END_REGION")
|
||||
|
||||
|
||||
def template_usage(context: SclFile, templates: SclTemplates | None
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""FB-Typen mit bzw. ohne Template (je sortiert, ohne Doppelte)."""
|
||||
fbs = sorted({t.fb for t in context.tros})
|
||||
if templates is None:
|
||||
return [], fbs
|
||||
return ([fb for fb in fbs if templates.has(fb)],
|
||||
[fb for fb in fbs if not templates.has(fb)])
|
||||
|
||||
|
||||
def render_context(context: SclFile, templates: SclTemplates | None = None) -> str:
|
||||
"""SCL-Text aus dem fertigen Kontext.
|
||||
|
||||
Mit ``templates`` wird jeder TRO, fuer dessen FB ein Template existiert, aus
|
||||
dem Template erzeugt, alle anderen generisch. Ohne ``templates`` ist die
|
||||
Ausgabe die bisherige (Golden-Files in tests/fixtures/scl/).
|
||||
"""
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
|
||||
add("// ============================================================")
|
||||
add("// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py")
|
||||
add(f"// Quelle : {quelle}")
|
||||
add(f"// Erzeugt: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
add(f"// Quelle : {context.source}")
|
||||
add(f"// Erzeugt: {context.created}")
|
||||
add("//")
|
||||
add("// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum")
|
||||
add("// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem")
|
||||
@@ -290,58 +329,39 @@ def render(daten: dict, quelle: str, start_id: str = "") -> str:
|
||||
add("// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.")
|
||||
add("//")
|
||||
add("// Reihenfolge der REGIONs = Materialfluss.")
|
||||
add(f"// Anfang bei {geordnet[0]['id']}: {startgrund}.")
|
||||
add(f"// Anfang bei {context.tros[0].id}: {context.start_reason}.")
|
||||
if context.eplan_source:
|
||||
add("//")
|
||||
add(f"// E-Planung: {context.eplan_source}")
|
||||
add("// Separator-Sensoren und Stopperventile aus der Zuordnung sind")
|
||||
add("// eingesetzt (ohne Marker); was die Zuordnung nicht sicher fand,")
|
||||
add(f"// bleibt '{TODO}'. Details: <csv>_eplan.md (lib/e_planning.py).")
|
||||
if templates is not None:
|
||||
mit, ohne = template_usage(context, templates)
|
||||
add("//")
|
||||
add(f"// Aus Template : {', '.join(mit) or '-'}")
|
||||
add(f"// Generisch : {', '.join(ohne) or '-'} (kein Template - von Hand pruefen)")
|
||||
add("// ============================================================")
|
||||
add("")
|
||||
add('FUNCTION_BLOCK "FB_Main"')
|
||||
add("{ S7_Optimized_Access := 'TRUE' }")
|
||||
add("VERSION : 0.1")
|
||||
add(" VAR ")
|
||||
for tro in geordnet:
|
||||
fb = tro.get("fbType") or "FB_UNBEKANNT"
|
||||
kommentar = f" // {tro['type']}"
|
||||
if tro.get("separators"):
|
||||
kommentar += f", Sep {', '.join(tro['separators'])}"
|
||||
add(f' {tro["id"]} : "{fb}";{kommentar}')
|
||||
for tro in context.tros:
|
||||
kommentar = f" // {tro.type}"
|
||||
if tro.layout_separators:
|
||||
kommentar += f", Sep {', '.join(tro.layout_separators)}"
|
||||
add(f' {tro.id} : "{tro.fb}";{kommentar}')
|
||||
add(" END_VAR")
|
||||
add("")
|
||||
add("BEGIN")
|
||||
|
||||
for tro in geordnet:
|
||||
items = tro.get("items", {})
|
||||
n_sep = items.get("Separator", 1)
|
||||
n_swi = items.get("Switch", 0)
|
||||
n_scan = items.get("Scanner", 0)
|
||||
|
||||
add("")
|
||||
add(f"REGION {region_title(tro)}")
|
||||
pos = tro.get("position")
|
||||
if pos:
|
||||
add(f" // Anlagenkoordinate x/y: {pos['x']:.0f} / {pos['y']:.0f} mm")
|
||||
add(f" // Vorgaenger: {', '.join(tro.get('predecessors', [])) or '-'}")
|
||||
add(f" // Nachfolger: {', '.join(tro.get('successors', [])) or '-'}")
|
||||
if tro.get("confidence"):
|
||||
add(f" // Einstufung: {tro['confidence']}")
|
||||
add("")
|
||||
|
||||
emit_separators(add, tro, n_sep, hat_scanner=bool(n_scan))
|
||||
if n_swi:
|
||||
emit_switches(add, tro, n_swi, alle)
|
||||
if items.get("VarioDrive"):
|
||||
emit_vario(add, tro)
|
||||
if n_scan:
|
||||
emit_barcode(add, tro, n_scan)
|
||||
if items.get("StorageLine"):
|
||||
add(f" // {TODO}: Linienspeicher mit {items['StorageLine']} Linien - "
|
||||
f"Parametrierung je Linie")
|
||||
for tro in context.tros:
|
||||
if templates is not None and templates.has(tro.fb):
|
||||
add("")
|
||||
|
||||
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro["id"])
|
||||
add("")
|
||||
emit_call(add, tro, items)
|
||||
if n_swi:
|
||||
emit_direction(add, tro, n_swi)
|
||||
add("END_REGION")
|
||||
out.extend(templates.render(tro).split("\n"))
|
||||
else:
|
||||
emit_region(add, tro)
|
||||
|
||||
add("")
|
||||
add("END_FUNCTION_BLOCK")
|
||||
@@ -349,41 +369,106 @@ def render(daten: dict, quelle: str, start_id: str = "") -> str:
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def render(document: TroDocument, quelle: str, start_id: str = "",
|
||||
templates: SclTemplates | None = None) -> str:
|
||||
"""TRO-JSON -> SCL-Text (Kontext bauen, dann ausgeben)."""
|
||||
return render_context(build_context(document, quelle, start_id), templates)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
|
||||
json_file = Path(args.json)
|
||||
if not json_file.is_absolute() and json_file.parent == Path("."):
|
||||
json_file = results / args.json
|
||||
json_file = _in_results(args.json, results)
|
||||
if not json_file.is_file():
|
||||
print(f"FEHLER: JSON nicht gefunden: {json_file}", file=sys.stderr)
|
||||
print(" Zuerst bin/tro_extract.bat laufen lassen.", file=sys.stderr)
|
||||
print(" Zuerst bin/tro_flow.bat --file <name>.csv --json laufen lassen.",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
daten = load_tros(json_file)
|
||||
except (OSError, ValueError) as exc:
|
||||
document = load_document(json_file)
|
||||
except OSError as exc:
|
||||
print(f"FEHLER: {json_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
if not daten.get("tros"):
|
||||
except ValidationError as exc:
|
||||
schema = _schema_of(json_file)
|
||||
if schema and schema != SCHEMA:
|
||||
# Anderes Format (z. B. die alte tro_extract-JSON) - die einzelnen
|
||||
# Feldfehler helfen dann nicht weiter.
|
||||
print(f"FEHLER: {json_file} hat das Schema '{schema}', erwartet wird "
|
||||
f"'{SCHEMA}' ({exc.error_count()} Abweichungen).", file=sys.stderr)
|
||||
print(" Mit bin/tro_flow.bat --file <name>.csv --json neu erzeugen.",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
print(f"FEHLER: {json_file} passt nicht zum TRO-Modell (lib/model.py):",
|
||||
file=sys.stderr)
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
if not document.tros:
|
||||
print(f"FEHLER: {json_file} enthaelt keine TROs.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = render(daten, json_file.name, args.start)
|
||||
templates = None
|
||||
if args.templates is not None:
|
||||
ordner = Path(args.templates)
|
||||
if not ordner.is_dir():
|
||||
print(f"FEHLER: Template-Ordner nicht gefunden: {ordner}", file=sys.stderr)
|
||||
return 1
|
||||
templates = SclTemplates(ordner)
|
||||
|
||||
eplan = None
|
||||
if args.eplan:
|
||||
eplan_file = _in_results(args.eplan, results)
|
||||
try:
|
||||
eplan = EplanDocument.from_json(eplan_file.read_text(encoding="utf-8"))
|
||||
except OSError as exc:
|
||||
print(f"FEHLER: E-Planungs-Zuordnung nicht lesbar: {exc}", file=sys.stderr)
|
||||
print(" Zuerst bin/e_planning.bat --file <name>.csv --eplan "
|
||||
"<zeichnung>_positionsdraw.json laufen lassen.", file=sys.stderr)
|
||||
return 1
|
||||
except ValidationError as exc:
|
||||
print(f"FEHLER: {eplan_file} passt nicht zum Schema '{EPLAN_SCHEMA}' "
|
||||
f"({exc.error_count()} Abweichungen).", file=sys.stderr)
|
||||
return 1
|
||||
# Eine Zuordnung zu einem anderen Layout setzt falsche Namen ein -
|
||||
# Separatornummern sind nur innerhalb eines CSV-Exports eindeutig.
|
||||
if Path(eplan.csv_file).name != Path(document.source).name:
|
||||
print(f"FEHLER: {eplan_file.name} gehoert zu '{eplan.csv_file}', die "
|
||||
f"TRO-JSON zu '{document.source}'.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
context = build_context(document, json_file.name, args.start, eplan=eplan)
|
||||
mit, ohne = template_usage(context, templates)
|
||||
if args.strict and ohne:
|
||||
print(f"FEHLER: kein Template fuer {', '.join(ohne)} (--strict) - "
|
||||
f"keine SCL geschrieben.", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
text = render_context(context, templates)
|
||||
except TemplateError as exc:
|
||||
# StrictUndefined, Syntaxfehler, fehlendes Makro ... - lieber keine SCL
|
||||
# als eine mit stillen Luecken.
|
||||
print(f"FEHLER im Template: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
out_file = results / (args.out or f"{json_file.stem.replace('_tro', '')}_FB_Main.scl")
|
||||
out_file.write_text(text, encoding="utf-8")
|
||||
|
||||
offen = text.count(TODO)
|
||||
geordnet, startgrund = flow_order(daten["tros"], args.start)
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("FB_MAIN-GERUEST")
|
||||
print("================================================================")
|
||||
print(f"Quelle = {json_file}")
|
||||
print(f"TROs = {len(daten['tros'])}")
|
||||
print(f"Reihenfolge = {' -> '.join(t['id'] for t in geordnet)}")
|
||||
print(f"Anfang = {geordnet[0]['id']} ({startgrund})")
|
||||
print(f"TROs = {len(context.tros)}")
|
||||
print(f"Reihenfolge = {' -> '.join(t.id for t in context.tros)}")
|
||||
print(f"Anfang = {context.tros[0].id} ({context.start_reason})")
|
||||
if templates is not None:
|
||||
print(f"Templates = {len(mit)} von {len(mit) + len(ohne)} Typen "
|
||||
f"(generisch: {', '.join(ohne) or '-'})")
|
||||
if eplan is not None:
|
||||
print(f"E-Planung = {eplan_file}")
|
||||
print(f"Eingesetzt = {_eingesetzt(context)} Namen aus der E-Planung")
|
||||
print(f"Offene Stellen = {offen} (Marker '{TODO}')")
|
||||
print(f"SCL = {out_file}")
|
||||
print("================================================================")
|
||||
@@ -391,25 +476,58 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _in_results(name: str, results: Path) -> Path:
|
||||
"""Blosser Dateiname -> in SKEL_RESULTS; Pfadangaben bleiben unveraendert."""
|
||||
path = Path(name)
|
||||
if not path.is_absolute() and path.parent == Path("."):
|
||||
return results / name
|
||||
return path
|
||||
|
||||
|
||||
def _eingesetzt(context: SclFile) -> int:
|
||||
"""Wie viele Separator-Namen aus der E-Planung kamen (auch Speicherlinien)."""
|
||||
namen = []
|
||||
for tro in context.tros:
|
||||
if tro.store is not None:
|
||||
# Speicher: Pin-Separator und Linien; der Separatorblock wiederholt
|
||||
# nur die erste Linie und wird im PinStore-Template nicht geschrieben
|
||||
namen += [tro.store.pin_sensor, tro.store.pin_stopper]
|
||||
namen += [n for linie in tro.store.lines for n in (linie.sensor, linie.stopper)]
|
||||
else:
|
||||
namen += [n for s in tro.separators for n in (s.sensor, s.stopper)]
|
||||
return sum(not name.todo for name in namen)
|
||||
|
||||
|
||||
def _schema_of(path: Path) -> str:
|
||||
"""Schema-Kennung einer JSON-Datei, nur fuer die Fehlermeldung ("" wenn unlesbar)."""
|
||||
try:
|
||||
return str(json.loads(path.read_text(encoding="utf-8")).get("schema", ""))
|
||||
except (OSError, ValueError, AttributeError):
|
||||
return ""
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="scl_skeleton",
|
||||
description="Erzeugt aus der extrahierten TRO-Liste ein FB_Main-Geruest "
|
||||
description="Erzeugt aus der TRO-JSON (tro_flow --json) ein FB_Main-Geruest "
|
||||
"als SCL zum Import in TIA Portal.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Beispiele:\n"
|
||||
" scl_skeleton.bat\n"
|
||||
" scl_skeleton.bat --json export_tro.json --out FB_Main.scl\n"
|
||||
" scl_skeleton.bat --json mubea_tro.json --templates\n"
|
||||
" scl_skeleton.bat --json mubea_tro.json --eplan mubea_eplan.json\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
" 1 Eingabe- oder Aufruffehler\n",
|
||||
" 0 ok (auch wenn TROs ohne Template generisch erzeugt wurden)\n"
|
||||
" 1 Eingabe-, Aufruf- oder Templatefehler; mit --strict auch\n"
|
||||
" ein FB-Typ ohne Template\n",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
default="export_tro.json",
|
||||
metavar="NAME",
|
||||
help="TRO-JSON aus tro_extract.py in %%SKEL_RESULTS%% "
|
||||
help="TRO-JSON aus tro_flow.py --json in %%SKEL_RESULTS%% "
|
||||
"(oder ein vollstaendiger Pfad). Standard: %(default)s",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -424,6 +542,29 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
help="TRO, bei dem die Reihenfolge beginnt. Nur noetig, solange der "
|
||||
"Fluss ein geschlossener Kreis ohne Be-/Entladestation ist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--templates",
|
||||
nargs="?",
|
||||
const=str(TEMPLATE_DIR),
|
||||
default=None,
|
||||
metavar="DIR",
|
||||
help="REGIONs aus den FB-Templates erzeugen (doc/SCL_Templates.md). "
|
||||
"Ohne DIR: lib/scl_templates. FB-Typen ohne Template bleiben "
|
||||
"generisch. Ohne diesen Schalter: bisherige Ausgabe.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--eplan",
|
||||
metavar="NAME",
|
||||
help="Zuordnung zur E-Planung aus e_planning.py (<csv>_eplan.json in "
|
||||
"%%SKEL_RESULTS%% oder ein Pfad): Separator-Sensoren und "
|
||||
"Stopperventile werden eingesetzt statt als TODO markiert.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Nur mit --templates: abbrechen (Exit 1), wenn ein FB-Typ der "
|
||||
"Anlage kein Template hat.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_1Sep - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep.scl
|
||||
(TRO 100, =A01+UH01-KF00 FB_Main). Zeilen und Reihenfolge folgen
|
||||
dieser Aufrufstelle; geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Separatorzeiten 600/200 ms wie Referenz, "pruefen"
|
||||
tSenFree/tSenWait 1500/4000 ms wie Referenz, "pruefen"
|
||||
stInSenJam<n> je Ausgang einer, Sensor offen
|
||||
xReleaseOk TRUE wie Referenz (braucht keine Vorfahrtsregelung)
|
||||
xInCarouselRun immer, offen - ob der TRO am Kreisel liegt, steht
|
||||
nicht in der TRO-JSON; sonst Zeile loeschen
|
||||
xInRelease "DB_TRO_TEST".<id> wie Referenz
|
||||
stInOutMachineState Schaltschrank offen (anlagenspezifisch)
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#600ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
|
||||
{% endfor %}
|
||||
{{ m.jam_sensors(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1500ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4000ms; // {{ TODO }}: pruefen
|
||||
{% endfor %}
|
||||
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(("xInCarouselRun := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% for s in t.separators %}
|
||||
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{{ m.call(t, z) }}
|
||||
END_REGION
|
||||
@@ -0,0 +1,64 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_1Sep1Swi - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep1Swi.scl
|
||||
(TRO 107, =A01+UH01-KF00 FB_Main). Zeilen und Reihenfolge folgen
|
||||
dieser Aufrufstelle; geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
stInSenJam<n> je Ausgang einer, Sensor offen
|
||||
stInSettings.xPrioDir2 TRUE wie Referenz, aber offen (Planungsentscheidung)
|
||||
PriorityManagerDir2 immer, UDT-Name offen
|
||||
xInCarouselRun1 immer, offen - ob der TRO am Kreisel liegt, steht
|
||||
nicht in der TRO-JSON; sonst Zeile loeschen
|
||||
xInRelease TRUE wie Referenz (generisch: "DB_TRO_TEST")
|
||||
Separatorzeiten 100/50 ms wie Referenz, "pruefen"
|
||||
stInOutMachineState Schaltschrank offen (anlagenspezifisch)
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
{{ m.separator(t, s) }}
|
||||
{% endfor %}
|
||||
{{ m.jam_sensors(t) }}
|
||||
|
||||
{% for w in t.switches %}
|
||||
{{ m.switch(t, w) }}
|
||||
{% endfor %}
|
||||
|
||||
{{ m.ziele(t) }}
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('stInPriorityManagerDir2 := "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroIn', m.note(t.priority_udt))) %}
|
||||
{% set _ = z.append(('stOutPriorityManagerDir2 => "DB_Prozessdaten".stPriorityManager.' ~ t.priority_udt.value ~ '.TroOut', m.note(t.priority_udt))) %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(("xInCarouselRun1 := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
|
||||
{% set _ = z.append(("xInRelease := TRUE", "")) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " vor " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% for s in t.separators %}
|
||||
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
|
||||
{% endfor %}
|
||||
{% for w in t.switches %}
|
||||
{% set _ = z.append(("xOutSw" ~ w.n ~ 'ExTo3 => "' ~ w.valve.value ~ '"', m.note(w.valve))) %}
|
||||
{% endfor %}
|
||||
#{{ t.id }}.stInSettings.xPrioDir2 := {{ t.prio_dir2.value }};{{ m.todo(t.prio_dir2) }}
|
||||
{{ m.call(t, z) }}
|
||||
|
||||
{{ m.direction(t) }}
|
||||
END_REGION
|
||||
@@ -0,0 +1,105 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_1Sep2Swi - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep2Swi.scl
|
||||
(TRO 267 mit Scanner BX2100). Zeilen und Reihenfolge folgen dieser
|
||||
Aufrufstelle; geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Separator tDelayToNextItem "_Medium", xPluggedPart TRUE und
|
||||
stSenPart wie Referenz; keine Separatorzeiten
|
||||
(stehen in der Referenz nicht)
|
||||
stInSenJam<n> je Ausgang einer, Sensor offen
|
||||
nSwitchNo nur Weiche 1 wie Referenz; fuer Weiche 2 fehlt die
|
||||
Zeile in der Referenz - als Kommentar vorgeschlagen
|
||||
xNewSwitch TRUE wie Referenz
|
||||
nTo1Destinations Aufteilung der Abgaenge auf die Weichen offen
|
||||
tTimeout Scanner 5 s wie Referenz
|
||||
stInSettings.bSepType.%X2 TRUE wie Referenz, Bedeutung klaeren ("pruefen")
|
||||
xInCarouselRun<n> je Weiche einer, offen (Kreisel steht nicht in der JSON)
|
||||
nOutStateLast fehlt in der Referenz - nicht geschrieben
|
||||
FC_Direction steht in der Referenz nicht an dieser Stelle -
|
||||
nur die Zielliste als Kommentar
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Medium";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenPart := "DB_Inputs".Sensors["{{ s.part_sensor.value }}"];{{ m.todo(s.part_sensor) }}
|
||||
{% endfor %}
|
||||
{{ m.jam_sensors(t) }}
|
||||
|
||||
{% for w in t.switches %}
|
||||
{% if loop.first %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}";{{ m.todo(w.no) }}
|
||||
{% else %}
|
||||
// #{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}"; // {{ TODO }}: pruefen - fehlt in der Referenz TRO267
|
||||
{% endif %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen3 := FALSE;
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen4 := FALSE;
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xNewSwitch := TRUE;
|
||||
{% if w.to1_destinations is not none %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := {{ w.to1_destinations }}; // Info noetig fuer HMI - {{ TODO }}: pruefen
|
||||
{% else %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := _; // {{ TODO }}: {{ t.successors|length }} Abgaenge auf {{ t.switches|length }} Weichen - Aufteilung festlegen
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{{ m.ziele(t) }}
|
||||
|
||||
{% for sc in t.scanners %}
|
||||
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.xActivateCheckASCI := TRUE;
|
||||
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.tTimeout := T#5s;
|
||||
|
||||
{{ m.cognex_in(m.cognex(sc), sc) }}
|
||||
|
||||
{% endfor %}
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
|
||||
|
||||
#{{ t.id }}.stInSettings.bSepType.%X2 := TRUE; // {{ TODO }}: pruefen (Bedeutung)
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
|
||||
{% for w in t.switches %}
|
||||
{% set _ = z.append(("xInCarouselRun" ~ w.n ~ " := " ~ t.carousel_run.value, m.note(t.carousel_run, "nur am Kreisel, sonst Zeile loeschen"))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% if t.scanners %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := " ~ m.cognex(t.scanners[0]), m.note(t.scanners[0].interface))) %}
|
||||
{% else %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% endif %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% for s in t.separators %}
|
||||
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
|
||||
{% endfor %}
|
||||
{% for w in t.switches %}
|
||||
{% set _ = z.append(("xOutSw" ~ w.n ~ 'ExTo3 => "' ~ w.valve.value ~ '"', m.note(w.valve))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{{ m.call(t, z) }}
|
||||
|
||||
{% for sc in t.scanners %}
|
||||
{{ m.cognex_out(m.cognex(sc), sc) }}
|
||||
{% endfor %}
|
||||
END_REGION
|
||||
@@ -0,0 +1,112 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_1Sep_SSCC - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_1Sep_SSCC.scl
|
||||
(TRO 105, =A01+UH01-KF00 FB_Main, Scanner BX1104 + SSCC-Scanner
|
||||
BX1107). Zeilen und Reihenfolge folgen dieser Aufrufstelle;
|
||||
geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Separatorzeiten 600/200 ms, xPluggedConfirm/NullBT FALSE wie Referenz, "pruefen"
|
||||
tSenFree/tSenWait 1500/4000 ms wie Referenz, "pruefen"
|
||||
Scanner/SSCC-Scanner Cognex-Verdrahtung mit offenen Symbolen (E)
|
||||
stSenEndMeasurement Sensor offen (Referenz: derselbe wie stInSenJam2)
|
||||
xInCarouselRun offen (Referenz: TRUE "Gefaelle")
|
||||
xInRelease "DB_TRO_TEST".<id> - in der Referenz haengt die
|
||||
Freigabe an der WCS-Verbindung (Kommentar, offen)
|
||||
Carr_Glob_Correction nInStation offen (Referenz 0), Traeger vom Eingang
|
||||
SSCC an HMI Schluessel der HMI-Anzeige offen (Referenz SSCC1107)
|
||||
WCS-Telegramm in der Referenz nur angedeutet - als TODO vermerkt
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
{% set ss = t.sscc %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#600ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedConfirm := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedNullBT := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
|
||||
{% endfor %}
|
||||
{{ m.jam_sensors(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1500ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4000ms; // {{ TODO }}: pruefen
|
||||
{% endfor %}
|
||||
|
||||
{% for sc in t.scanners %}
|
||||
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.xActivateCheckASCI := TRUE;
|
||||
#{{ t.id }}.stInBarcodeReader{{ sc.suffix }}.Settings.tTimeout := T#3000ms;
|
||||
|
||||
{{ m.cognex_in(m.cognex(sc), sc) }}
|
||||
|
||||
{% endfor %}
|
||||
//SSCC
|
||||
{{ m.cognex_in(ss.scanner.interface.value, ss.scanner) }}
|
||||
|
||||
#{{ t.id }}.stInBarcodeReaderSSCC.Settings.xActivateCheckASCI := TRUE;
|
||||
#{{ t.id }}.stInBarcodeReaderSSCC.Settings.tTimeout := T#3s;
|
||||
#{{ t.id }}.stInBarcodeReaderSSCC.xInPluggedSSCC := TRUE;
|
||||
#{{ t.id }}.stInBarcodeReaderSSCC.stSenEndMeasurement := "DB_Inputs".Sensors["{{ ss.end_sensor.value }}"];{{ m.todo(ss.end_sensor) }}
|
||||
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
|
||||
{% set _ = z.append(("xInCarouselRun := " ~ t.carousel_run.value, m.note(t.carousel_run, "am Kreisel dessen Freigabe, im Gefaelle TRUE"))) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, TODO ~ " - Referenz: Freigabe ueber die WCS-Verbindung")) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% if t.scanners %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := " ~ m.cognex(t.scanners[0]), m.note(t.scanners[0].interface))) %}
|
||||
{% else %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% endif %}
|
||||
{% set _ = z.append(("stInOutCognexInterfaceSSCC := " ~ ss.scanner.interface.value, m.note(ss.scanner.interface))) %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.carriers.value, m.note(j.carriers, j.title ~ " vor " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.data.value, m.note(j.data))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% for s in t.separators %}
|
||||
{% set _ = z.append(("xOutStopper" ~ s.n ~ ' => "' ~ s.stopper.value ~ '"', m.note(s.stopper))) %}
|
||||
{% endfor %}
|
||||
{{ m.call(t, z) }}
|
||||
|
||||
{% for sc in t.scanners %}
|
||||
{{ m.cognex_out(m.cognex(sc), sc) }}
|
||||
|
||||
{% endfor %}
|
||||
//SSCC
|
||||
{{ m.cognex_out(ss.scanner.interface.value, ss.scanner) }}
|
||||
|
||||
{{ m.correction(t, "Carr_Glob_Correction") }}
|
||||
|
||||
REGION Write SSCC result TO HMI Interface
|
||||
IF #{{ t.id }}.xOutNewScanSSCC THEN
|
||||
"DB_Interface_HMI".stSSCC.{{ ss.hmi.value }}.stLastMeasurement.nBarcodeTrolley := {{ t.jam_in.carriers.value }}[1].nBarcode;{{ m.todo(ss.hmi) }}
|
||||
"DB_Interface_HMI".stSSCC.{{ ss.hmi.value }}.stLastMeasurement.sSSCC := LINT_TO_STRING(#{{ t.id }}.nOutScanResultSSCC);{{ m.todo(ss.hmi) }}
|
||||
END_IF;
|
||||
END_REGION
|
||||
|
||||
// {{ TODO }}: WCS-Telegramm (SSCC) - in der Referenz eigene REGION
|
||||
// "Send DReqM to Fortna WCS (SSCC)", siehe Json_Layout-Konzept.md Abschnitt 4
|
||||
END_REGION
|
||||
@@ -0,0 +1,134 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_PinStore_Auto - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_PinStore_Auto.scl
|
||||
(BLOCK 1.1.1 / TRO 208, =A01+UH02-KF00 FB_Main, Scanner BX2104).
|
||||
Die Referenz schreibt nur 3 der 19 Linien aus; der Rest folgt
|
||||
demselben Schema. Geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Anzahl Linien aus dem Layout: eine je Speicherbahn (nNoOfLines),
|
||||
nicht die 19 der Referenz
|
||||
Encoder, Stationen,
|
||||
Ziele, Schwellen offen (anlagenspezifische Konstanten)
|
||||
Sep Pin Einschleus-Separator; Zeiten 0/50 ms wie Referenz, "pruefen"
|
||||
Sep Store n nJamLength offen (Referenz: 4 bzw. "cStorage111ExitJam<n>"),
|
||||
Zeiten ueber die Konstanten cTimeSep...StorageLine
|
||||
arInSep[7..19].stSimu Simulationsmodus der Referenz - nicht geschrieben
|
||||
JamEntr/JamExit je einer: der eigene Eingangs-Staubereich (mehrere
|
||||
Zulaeufe laufen dort zusammen, TRO-JSON v4) und der
|
||||
des ersten Nachfolgers. Hat das Lager mehr
|
||||
Staubereiche, steht ein TODO im Kopf.
|
||||
JamBypass Platzhalter wie Referenz
|
||||
Carr_Glob_Correction nInStation offen (Referenz 1), Traeger vom Eingang
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
{% set st = t.store %}
|
||||
{% set sc = t.scanners[0] %}
|
||||
{#- Kameraname im REGION-Titel nur, wenn bekannt (kein TODO-Kommentar im Titel) -#}
|
||||
{% set kamera = "" if sc.tag.todo else " (" ~ (sc.tag.value|upper) ~ ")" %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
{% if t.jam_entries|length > 1 or t.jam_exits|length > 1 %}
|
||||
// {{ TODO }}: {{ t.jam_entries|length }} Eingangs- / {{ t.jam_exits|length }} Ausgangs-Staubereiche im Layout -
|
||||
// der FB hat je nur einen (JamEntr/JamExit, dazu JamBypass)
|
||||
{% endif %}
|
||||
// Linien (Separatoren aus dem Layout): {{ st.lines|map(attribute="layout")|join(", ") or "-" }}
|
||||
|
||||
REGION Encoder Settings
|
||||
#{{ t.id }}.stInEncoderParameter.nPosActDif := {{ st.encoder.value }}.nOutPosActDif;{{ m.todo(st.encoder) }}
|
||||
#{{ t.id }}.stInEncoderParameter.nPosPitchPart := {{ st.encoder.value }}.nOutPosPitchPart;{{ m.todo(st.encoder) }}
|
||||
END_REGION
|
||||
|
||||
REGION ConveyorStation Settings
|
||||
#{{ t.id }}.stInSettings.nDestGeneral := "{{ st.dest.value }}";{{ m.todo(st.dest) }}
|
||||
#{{ t.id }}.stInSettings.nConvStations := "{{ st.conv_stations.value }}";{{ m.todo(st.conv_stations) }}
|
||||
#{{ t.id }}.stInSettings.nStationsMax := "{{ st.stations_max.value }}";{{ m.todo(st.stations_max) }}
|
||||
#{{ t.id }}.stInSettings.nStorageThreshold := "{{ st.threshold.value }}";{{ m.todo(st.threshold) }}
|
||||
#{{ t.id }}.stInSettings.nNoOfLines := {{ st.lines|length }};
|
||||
END_REGION
|
||||
|
||||
REGION Pin Query / "Zappler"
|
||||
#{{ t.id }}.stInSensorReference := "DB_Inputs".Sensors["{{ st.reference_sensor.value }}"];{{ m.todo(st.reference_sensor) }}
|
||||
END_REGION
|
||||
|
||||
REGION Barcode Reader In{{ kamera }}
|
||||
#{{ t.id }}.stInBarcodeReader.Settings.nStation := {{ st.station.value }};{{ m.todo(st.station) }}
|
||||
#{{ t.id }}.stInBarcodeReader.Settings.nScannerPurpose := 0;
|
||||
#{{ t.id }}.stInBarcodeReader.Settings.tTimeout := T#5s;
|
||||
#{{ t.id }}.stInBarcodeReader.Settings.xActivateCheckASCI := TRUE;
|
||||
|
||||
{{ m.cognex_in(m.cognex(sc), sc, " ") }}
|
||||
END_REGION
|
||||
|
||||
REGION Separator Settings
|
||||
REGION Sep Pin
|
||||
// Einschleus-Separator = Bauteil {{ st.pin_layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparatorPin.stSenTrolley := "DB_Inputs".Sensors["{{ st.pin_sensor.value }}"];{{ m.todo(st.pin_sensor) }}
|
||||
#{{ t.id }}.stInSeparatorPin.stSettings.tHandlingTime := T#0ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparatorPin.stSettings.tTrailingTime := T#50ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparatorPin.stSettings.xPluggedScanner := TRUE;
|
||||
END_REGION
|
||||
{% for l in st.lines %}
|
||||
|
||||
REGION Sep Store {{ l.n }}
|
||||
// Linie {{ l.n }} = Separator {{ l.layout }} aus dem Layout
|
||||
#nActNo := {{ l.n }};
|
||||
#{{ t.id }}.arInSep[#nActNo].stSettings.nJamLength := {{ l.jam_length.value }};{{ m.todo(l.jam_length) }}
|
||||
#{{ t.id }}.arInSep[#nActNo].stSenTrolley := "DB_Inputs".Sensors["{{ l.sensor.value }}"];{{ m.todo(l.sensor) }}
|
||||
#{{ t.id }}.arInSep[#nActNo].stSettings.tHandlingTime := "cTimeSepHandlingStorageLine";
|
||||
#{{ t.id }}.arInSep[#nActNo].stSettings.tTrailingTime := "cTimeSepTrailingStorageLine";
|
||||
#{{ t.id }}.arInSep[#nActNo].stSettings.xPluggedScanner := FALSE;
|
||||
END_REGION
|
||||
{% endfor %}
|
||||
END_REGION
|
||||
|
||||
REGION UnloadingStations Settings
|
||||
#{{ t.id }}.xInQuitLocal := {{ st.quit_local.value }};{{ m.todo(st.quit_local) }}
|
||||
|
||||
{% for l in st.lines %}
|
||||
#{{ t.id }}.arInJamSensor[{{ l.n }}] := "DB_Inputs".Sensors["{{ l.jam_sensor.value }}"];{{ m.todo(l.jam_sensor) }}
|
||||
{% endfor %}
|
||||
END_REGION
|
||||
|
||||
REGION Aufruf: {{ t.id }}
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("nInTestTarget := " ~ st.test_target.value, m.note(st.test_target))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(('xInCancel := "cTodoFalse"', "")) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
|
||||
{% set _ = z.append(("xInConveyorRunIn := " ~ st.run_in.value, m.note(st.run_in, "Foerderer am Zulauf"))) %}
|
||||
{% set _ = z.append(("xInConveyorRunOut := " ~ st.run_out.value, m.note(st.run_out, "Foerderer am Ablauf"))) %}
|
||||
{% set _ = z.append(("stInOutControlUnitCabinet := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("arInOutStorageLines := " ~ st.db.value ~ ".arLine", m.note(st.db))) %}
|
||||
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
|
||||
{% set _ = z.append(("arInOutJamExit := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
|
||||
{% set _ = z.append(("arInOutJamBypass := #arCarrierPlaceHolder", "")) %}
|
||||
{% set _ = z.append(("stInOutJamBypass := #stJamDataPlaceHolder", "")) %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(("stMFRStorageRemovalInterface := " ~ st.removal.value, m.note(st.removal))) %}
|
||||
{% set _ = z.append(('xOutSeparatorPin => "' ~ st.pin_stopper.value ~ '"', m.note(st.pin_stopper))) %}
|
||||
{% set _ = z.append(("stCognexInterfacePin := " ~ m.cognex(sc), m.note(sc.interface))) %}
|
||||
{% set _ = z.append(("stConveyorStations := " ~ st.conv_db.value, m.note(st.conv_db))) %}
|
||||
{{ m.call(t, z) }}
|
||||
END_REGION
|
||||
|
||||
{{ m.correction(t, "CARR_GLOB_CORRECTION") }}
|
||||
|
||||
REGION Barcode Reader Out{{ kamera }}
|
||||
{{ m.cognex_out(m.cognex(sc), sc, " ") }}
|
||||
END_REGION
|
||||
|
||||
REGION Separator Store Out
|
||||
{% for l in st.lines %}
|
||||
"{{ l.stopper.value }}" := #{{ t.id }}.arOutSeparatorStore[{{ l.n }}];{{ m.todo(l.stopper) }}
|
||||
{% endfor %}
|
||||
END_REGION
|
||||
// stMFRStorageRemovalInterface koppelt an FB_StockRemovalBLKModul / FB_StorageManagement
|
||||
END_REGION
|
||||
@@ -0,0 +1,83 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_Vario - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_Vario.scl
|
||||
(TRO 115). Zeilen und Reihenfolge folgen dieser Aufrufstelle;
|
||||
geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Separatorzeiten 800/300 ms, xPluggedJam FALSE wie Referenz, "pruefen"
|
||||
tSenFree/tSenWait 2000/5000 ms wie Referenz, "pruefen"
|
||||
Vario-Zeiten wie Referenz; tWaitForJog/tConvEmpty/nCountCarrier "pruefen"
|
||||
tRasterHigh/Low 5 s wie Referenz, "pruefen"
|
||||
stInSettings.xIsBorner TRUE wie Referenz, aber offen (Bedeutung klaeren)
|
||||
arInOutJamEntr Staubereich zum (ersten) Vorgaenger
|
||||
arInOutJamFinger Staubereich zum (ersten) Nachfolger
|
||||
xInErrorQuit/xInAutomatic offen (Quittier-Taster, Automatik des Schranks)
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
{% set v = t.vario_sensors %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#800ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#300ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
|
||||
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := t#2000ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#5000ms; // {{ TODO }}: pruefen
|
||||
{% endfor %}
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#{{ t.id }}.stInVario.Settings.tWaitForJog := T#300ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.tConvEmpty := T#45000ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.nCountCarrier := 1; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.tWaitForStop := t#3s;
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#{{ t.id }}.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.tRasterHigh := t#5s; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.tRasterLow := t#5s; // {{ TODO }}: pruefen
|
||||
|
||||
#{{ t.id }}.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["{{ v.stSenCarrInPos.value }}"]; // MT in Pos{{ m.todo_mehr(v.stSenCarrInPos) }}
|
||||
#{{ t.id }}.stInVario.stSenFinger := "DB_Inputs".Sensors["{{ v.stSenFinger.value }}"]; // Pusher Dog in Pos{{ m.todo_mehr(v.stSenFinger) }}
|
||||
#{{ t.id }}.stInVario.stSenLastPos := "DB_Inputs".Sensors["{{ v.stSenLastPos.value }}"]; // Conveyor full{{ m.todo_mehr(v.stSenLastPos) }}
|
||||
#{{ t.id }}.stInVario.stSenJam := "DB_Inputs".Sensors["{{ v.stSenJam.value }}"]; // Jam detector{{ m.todo_mehr(v.stSenJam) }}
|
||||
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
|
||||
#{{ t.id }}.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#{{ t.id }}.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
|
||||
#{{ t.id }}.stInSettings.xIsBorner := {{ t.is_border.value }}; // {{ m.note(t.is_border, "pruefen - Bedeutung mit SPS klaeren") }}
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInMotorProtection := " ~ t.motor_protection.value, m.note(t.motor_protection))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(('xInReset := "cTodoFalse"', "")) %}
|
||||
{% set _ = z.append(("xInErrorQuit := " ~ t.error_quit.value, m.note(t.error_quit, "Quittierung"))) %}
|
||||
{% set _ = z.append(("xInAutomatic := " ~ t.automatic.value, m.note(t.automatic, "Automatik des Schaltschranks"))) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
|
||||
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
|
||||
{% set _ = z.append(("arInOutJamFinger := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamFinger := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(('xOutStopper => "' ~ t.separators[0].stopper.value ~ '"', m.note(t.separators[0].stopper))) %}
|
||||
{% set _ = z.append(('xOutVarioMotor => "' ~ t.vario_motor.value ~ '"', m.note(t.vario_motor))) %}
|
||||
{{ m.call(t, z) }}
|
||||
END_REGION
|
||||
@@ -0,0 +1,85 @@
|
||||
{#-
|
||||
FB_ILS_MTRO_Vario_workStation - Aufrufstelle in FB_Main
|
||||
|
||||
Referenz: doc/TRO_Katalog/scl_templates/FB_ILS_MTRO_Vario_workStation_CallSite.scl
|
||||
(TRO 421, =A01+UH04-KF00 FB_Main). Der FB-Koerper liegt daneben
|
||||
(FB_ILS_MTRO_Vario_workStation.scl). Zeilen und Reihenfolge folgen
|
||||
der Aufrufstelle; geprueft per tests/test_scl_templates.py.
|
||||
|
||||
Entscheidungen zu den ortsgebundenen Zeilen (doc/SCL_Templates.md §6):
|
||||
Separatorzeiten tTrailingTime "cTimeSepTrailing", tHandlingTime 200 ms
|
||||
wie Referenz, "pruefen"
|
||||
xPluggedScanner FALSE wie Referenz - der Scanner des FB sitzt am
|
||||
Arbeitsplatz, nicht am Separator ("pruefen")
|
||||
tSenFree/tSenWait 1200/4300 ms wie Referenz, "pruefen"
|
||||
Vario-Zeiten wie Referenz (tWaitForJog 400 ms, tConvEmpty 30 s), "pruefen"
|
||||
fbConvVario.xBeladenError Sensor offen (E)
|
||||
stInStationSensor/xInReleaseStation Arbeitsplatz-Sensor und -Freigabe offen (E)
|
||||
arInOutJamEntr/JamFinger Staubereich zum (ersten) Vorgaenger / Nachfolger
|
||||
Alle noch mit den SPS-Kollegen zu bestaetigen.
|
||||
-#}
|
||||
{% import "_macros.scl.j2" as m %}
|
||||
{% set v = t.vario_sensors %}
|
||||
{% set ws = t.work_station %}
|
||||
REGION {{ t.region_title }}
|
||||
{{ m.kopf(t) }}
|
||||
|
||||
{% for s in t.separators %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ m.todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := "cTimeSepTrailing"; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#200ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := FALSE; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ m.todo(s.sensor) }}
|
||||
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenFree := T#1200ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tSenWait := T#4300ms; // {{ TODO }}: pruefen
|
||||
{% endfor %}
|
||||
|
||||
#{{ t.id }}.stInVario.stSenFinger := "DB_Inputs".Sensors["{{ v.stSenFinger.value }}"]; // Finger in Pos{{ m.todo_mehr(v.stSenFinger) }}
|
||||
#{{ t.id }}.stInVario.stSenLastPos := "DB_Inputs".Sensors["{{ v.stSenLastPos.value }}"]; // Conveyor full{{ m.todo_mehr(v.stSenLastPos) }}
|
||||
#{{ t.id }}.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["{{ v.stSenCarrInPos.value }}"]; // MT in Position{{ m.todo_mehr(v.stSenCarrInPos) }}
|
||||
#{{ t.id }}.stInVario.stSenJam := "DB_Inputs".Sensors["{{ v.stSenJam.value }}"]; // Jam Detector{{ m.todo_mehr(v.stSenJam) }}
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#{{ t.id }}.stInVario.Settings.tWaitForJog := T#400ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.tConvEmpty := T#30000ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.nCountCarrier := 1; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInVario.Settings.tWaitForStop := T#3s;
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#{{ t.id }}.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
|
||||
#{{ t.id }}.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#{{ t.id }}.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
|
||||
#{{ t.id }}.stInPriorityManager.xReleaseOk := TRUE; // braucht keine Vorfahrtsregelung
|
||||
|
||||
#{{ t.id }}.fbConvVario.xBeladenError := "DB_Inputs".Sensors["{{ ws.loaded_error_sensor.value }}"].xDirectSensor;{{ m.todo(ws.loaded_error_sensor) }}
|
||||
|
||||
{% set z = [] %}
|
||||
{% set _ = z.append(('nInMainTroNo := "' ~ t.main_tro.value ~ '"', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(("xInSftyOk := " ~ t.safety.value, m.note(t.safety))) %}
|
||||
{% set _ = z.append(("xInAllRdyToStart := TRUE", "")) %}
|
||||
{% set _ = z.append(("xInAutomatic := " ~ t.automatic.value, m.note(t.automatic, "Automatik des Schaltschranks"))) %}
|
||||
{% set _ = z.append(('xInReset := "cTodoFalse"', "")) %}
|
||||
{% set _ = z.append(("xInRelease := " ~ t.release, "")) %}
|
||||
{% set _ = z.append(("xInMotorProtection := " ~ t.motor_protection.value, m.note(t.motor_protection))) %}
|
||||
{% set _ = z.append(('stInStationSensor := "DB_Inputs".Sensors["' ~ ws.station_sensor.value ~ '"]', m.note(ws.station_sensor, "Arbeitsplatz"))) %}
|
||||
{% set _ = z.append(("xInReleaseStation := " ~ ws.station_release.value, m.note(ws.station_release, "Freigabe Arbeitsplatz"))) %}
|
||||
{% set _ = z.append(("arInOutJamEntr := " ~ t.jam_in.carriers.value, m.note(t.jam_in.carriers, t.jam_in.title ~ " von " ~ t.jam_in.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr := " ~ t.jam_in.data.value, m.note(t.jam_in.data))) %}
|
||||
{% set _ = z.append(("arInOutJamFinger := " ~ t.jam_out.carriers.value, m.note(t.jam_out.carriers, t.jam_out.title ~ " vor " ~ t.jam_out.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamFinger := " ~ t.jam_out.data.value, m.note(t.jam_out.data))) %}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
{% set _ = z.append(('nOutStateLast => "DB_GLOB_TroState".arTroState["' ~ t.main_tro.value ~ '"]', m.note(t.main_tro))) %}
|
||||
{% set _ = z.append(('xOutStopper => "' ~ t.separators[0].stopper.value ~ '"', m.note(t.separators[0].stopper))) %}
|
||||
{% set _ = z.append(('xOutVarioMotor => "' ~ t.vario_motor.value ~ '"', m.note(t.vario_motor))) %}
|
||||
{{ m.call(t, z) }}
|
||||
END_REGION
|
||||
@@ -0,0 +1,140 @@
|
||||
{#-
|
||||
_macros.scl.j2 - gemeinsame Bausteine der FB-Templates (doc/SCL_Templates.md §5.2)
|
||||
|
||||
Jede Zeile ist eine der Klassen aus §6: L (Layout, aus dem Kontext),
|
||||
N (Name, heute Platzhalter + TODO), R (Referenzwert aus der realen Aufrufstelle),
|
||||
E (E-Planung, Platzhalter + TODO), C (Bedingung).
|
||||
|
||||
Makros liefern ihre Zeilen OHNE abschliessenden Zeilenumbruch; der Aufrufer
|
||||
setzt ihn. Globale Variable: TODO (Marker "TODO(E-Planung)").
|
||||
-#}
|
||||
|
||||
{#- TODO-Kommentar hinter einer Zeile, wenn der Name noch offen ist -#}
|
||||
{% macro todo(name, abstand=" ") -%}
|
||||
{% if name.todo %}{{ abstand }}// {{ TODO }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- TODO an einen schon vorhandenen Kommentar anhaengen ("// MT in Pos - TODO(E-Planung)") -#}
|
||||
{% macro todo_mehr(name) -%}
|
||||
{% if name.todo %} - {{ TODO }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Kommentartext fuer eine Parameterzeile des Aufrufs -#}
|
||||
{% macro note(name, text="") -%}
|
||||
{% if name.todo %}{{ TODO }}{% if text %} - {{ text }}{% endif %}{% else %}{{ text }}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Kommentarkopf der REGION: Lage und Nachbarn im Layout (L) -#}
|
||||
{% macro kopf(t) %}
|
||||
{% if t.position %}
|
||||
// Anlagenkoordinate x/y: {{ "%.0f"|format(t.position[0]) }} / {{ "%.0f"|format(t.position[1]) }} mm
|
||||
{% endif %}
|
||||
// Vorgaenger: {{ t.predecessors|join(", ") or "-" }}
|
||||
// Nachfolger: {{ t.successors|join(", ") or "-" }}
|
||||
{% if t.confidence %}
|
||||
// Einstufung: {{ t.confidence }}
|
||||
{% endif %}
|
||||
// Aufbau nach Template {{ t.fb }}.scl.j2 (Referenz: doc/TRO_Katalog/scl_templates)
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSeparator<n>: Zeiten R aus der Referenz (TRO107), Nummer N, Sensor E -#}
|
||||
{% macro separator(t, s) %}
|
||||
// Separator {{ s.n }} = Bauteil {{ s.layout }} aus dem Layout
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.nSeparatorNo := "{{ s.no.value }}";{{ todo(s.no) }}
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tTrailingTime := t#100ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tHandlingTime := t#50ms; // {{ TODO }}: pruefen
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.tJam := "TimeForSepJam";
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedJam := TRUE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedPart := FALSE;
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.Settings.xPluggedScanner := {{ "TRUE" if t.has_scanner else "FALSE" }};
|
||||
#{{ t.id }}.stInSeparator{{ s.n }}.stSenInSep := "DB_Inputs".Sensors["{{ s.sensor.value }}"];{{ todo(s.sensor) }}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSenJam<n>: ein Stausensor je Ausgang (E) -#}
|
||||
{% macro jam_sensors(t) %}
|
||||
{% for j in t.jam_exits %}
|
||||
#{{ t.id }}.stInSenJam{{ j.n }} := "DB_Inputs".Sensors["{{ j.sensor.value }}"];{{ todo(j.sensor) }}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- stInSwitch<n>: Nummer N; nTo1Destinations L, aber Bedeutung noch zu klaeren -#}
|
||||
{% macro switch(t, w) %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nSwitchNo := "{{ w.no.value }}";{{ todo(w.no) }}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen3 := FALSE;
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.xPluggedExSen4 := FALSE;
|
||||
{% if w.to1_destinations is not none %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := {{ w.to1_destinations }}; // Abgaenge laut Topologie - {{ TODO }}: pruefen (Info fuer HMI; Referenz TRO107 hat 3 bei 2 Abgaengen)
|
||||
{% else %}
|
||||
#{{ t.id }}.stInSwitch{{ w.n }}.Settings.nTo1Destinations := _; // {{ TODO }}: {{ t.successors|length }} Abgaenge auf {{ t.switches|length }} Weichen - Aufteilung festlegen
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Zielliste je Weichenabgang (L, nur Kommentar) -#}
|
||||
{% macro ziele(t) %}
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
{% for e in t.switch_exits %}
|
||||
// ueber {{ e.via }}: {{ e.targets|join(", ") or "-" }}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#-
|
||||
Instanzaufruf aus einer Liste von (Code, Kommentar). Das Komma wird hier
|
||||
gesetzt und steht damit VOR dem Kommentar - sonst waere es Teil des
|
||||
Kommentars und die Parameterliste uebersetzt nicht.
|
||||
-#}
|
||||
{% macro call(t, zeilen) %}
|
||||
{% set einzug = " " * (6 + t.id|length) %}
|
||||
{% for code, kommentar in zeilen %}
|
||||
{{ (" #" ~ t.id ~ "(") if loop.first else einzug }}{{ code }}{{ ");" if loop.last else "," }}{% if kommentar %} // {{ kommentar }}{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Cognex-Kamera: Eingaenge der Schnittstelle (E). ziel = "#arCognexInterface[...]" oder eine eigene Struktur -#}
|
||||
{% macro cognex_in(ziel, sc, einzug=" ") %}
|
||||
{{ einzug }}{{ ziel }}.xInCamTriggerReady := "{{ sc.tag.value }}-TriggerReady";{{ todo(sc.tag) }}
|
||||
{{ einzug }}{{ ziel }}.xInCamTriggerAck := "{{ sc.tag.value }}-TriggerAck";{{ todo(sc.tag) }}
|
||||
{{ einzug }}{{ ziel }}.xInCamResultsAvailable := "{{ sc.tag.value }}-Results Available";{{ todo(sc.tag) }}
|
||||
{{ einzug }}{{ ziel }}.xInCamGeneralFault := "{{ sc.tag.value }}-General Fault";{{ todo(sc.tag) }}
|
||||
{{ einzug }}{{ ziel }}.hwInCamInspectionResults := "{{ sc.hw_results.value }}";{{ todo(sc.hw_results) }}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Cognex-Kamera: Ausgaenge zurueck auf die Hardware-Symbole (E) -#}
|
||||
{% macro cognex_out(ziel, sc, einzug=" ") %}
|
||||
{{ einzug }}"{{ sc.tag.value }}-TriggerEnable" := {{ ziel }}.xOutCamTriggerEnable;{{ todo(sc.tag) }}
|
||||
{{ einzug }}"{{ sc.tag.value }}-Trigger" := {{ ziel }}.xOutCamTrigger;{{ todo(sc.tag) }}
|
||||
{{ einzug }}"{{ sc.tag.value }}-ResultsAck" := {{ ziel }}.xOutCamResultsAck;{{ todo(sc.tag) }}
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Schluessel einer Kamera in #arCognexInterface -#}
|
||||
{% macro cognex(sc) -%}
|
||||
#arCognexInterface["{{ sc.interface.value }}"]
|
||||
{%- endmacro %}
|
||||
|
||||
{#- Traegerkorrektur nach dem Scan (FC_CARR_GLOB_CORRECTION); Traeger am Eingang -#}
|
||||
{% macro correction(t, titel, einzug=" ") %}
|
||||
{{ einzug }}REGION {{ titel }}
|
||||
{{ einzug }} "FC_CARR_GLOB_CORRECTION"(xInStart := #{{ t.id }}.xOutCorrection,
|
||||
{{ einzug }} nInBarcodeScan := #{{ t.id }}.nBarcodeResult,
|
||||
{{ einzug }} nInStation := {{ t.correction_station.value }},{{ todo(t.correction_station) }}
|
||||
{{ einzug }} arOutEventLogInfo => #arDummyEventCorrectionInfo,
|
||||
{{ einzug }} InOutCarrierExpect := {{ t.jam_in.carriers.value }}[1]);{{ todo(t.jam_in.carriers) }}
|
||||
{{ einzug }}END_REGION
|
||||
{%- endmacro %}
|
||||
|
||||
{#- REGION Direction: je Weiche ein FC_Direction-Aufruf -#}
|
||||
{% macro direction(t) %}
|
||||
REGION Direction
|
||||
{% for w in t.switches %}
|
||||
"FC_Direction"(nInSwitchNo := "{{ w.no.value }}",{{ todo(w.no) }}
|
||||
nOutDirection => {{ w.direction_target }},
|
||||
InOutCarrier := {{ w.jam_db.value }}.arCarrier[1]);{{ todo(w.jam_db) }}
|
||||
{% if not loop.last %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
END_REGION
|
||||
{%- endmacro %}
|
||||
+197
-106
@@ -12,6 +12,12 @@ TROs und schreibt eine **Kopie** der Zeichnung mit zusaetzlicher Beschriftung:
|
||||
* optional Pfeile fuer den gerichteten Materialfluss zwischen den TROs
|
||||
* optional eine Legende
|
||||
|
||||
Die Zeichnung ist eine **Ansicht** zur Sichtkontrolle, keine Datenquelle: die
|
||||
TRO-JSON fuer scl_skeleton.py schreibt tro_flow.py --json direkt aus dem CSV.
|
||||
tro_annotate leitet die TROs mit genau demselben Aufruf ab wie tro_flow, damit
|
||||
Zeichnung und JSON uebereinstimmen. (tro_extract.py, der Rueckweg Zeichnung ->
|
||||
JSON, ist geparkt.)
|
||||
|
||||
Die Originalzeichnung wird nie veraendert. Alle Ergaenzungen liegen auf eigenen
|
||||
Layern (Vorgabe: Praefix "TRO_") und tragen XDATA der Anwendungskennung
|
||||
SPS_SKEL_TRO. Ein erneuter Lauf entfernt genau diese Objekte und baut sie neu -
|
||||
@@ -101,8 +107,19 @@ from dxf_registration import (
|
||||
)
|
||||
from material_flow import build_graph, env_dir, read_elements, resolve_input
|
||||
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
|
||||
from tro_flow import Analysis, analyse_tros
|
||||
from tro_overrides import load_overrides
|
||||
from model import Analysis
|
||||
from parameter import get_float
|
||||
from tro_flow import (
|
||||
BOUNDARY_ENTRY,
|
||||
BOUNDARY_EXIT,
|
||||
STATION_FARBE,
|
||||
_station_positionen,
|
||||
analyse_tros,
|
||||
find_sibling_raw_json,
|
||||
load_sepliste_counts,
|
||||
resolve_ids_file,
|
||||
)
|
||||
from tro_ids import TroIds
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
@@ -115,11 +132,11 @@ BLOCK_PREFIX = "TRO_SYM_"
|
||||
# SSG_BLOCKEDIT in dxfmakros ihn ueber "CONNECTION_*" erkennt.
|
||||
CONNECTION_BLOCK = "CONNECTION_ARROW"
|
||||
|
||||
# Zeichnungsgroessen in mm, wenn nicht anders angegeben
|
||||
DEFAULT_MARKER = 300.0
|
||||
DEFAULT_TEXT = 220.0
|
||||
# Restfehler, ab dem der Lagebezug abgelehnt wird
|
||||
DEFAULT_MAX_RESIDUAL = 600.0
|
||||
# Zeichnungsgroessen und Restfehler-Grenze des Lagebezugs aus cfg/parameter.ini,
|
||||
# Abschnitt [tro_annotate] (lib/parameter.py)
|
||||
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
||||
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
||||
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
||||
|
||||
|
||||
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||
@@ -312,10 +329,13 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
|
||||
doc.layers.add(name, dxfattribs={
|
||||
"true_color": ezdxf.rgb2int(hex_to_rgb(style.stroke)),
|
||||
})
|
||||
# openpoint: im Review gemeldete Stellen, die noch KEIN TRO sind. Kraeftiges
|
||||
# Rot, damit sie sich von den abgeleiteten Markern klar abheben.
|
||||
# Kantenfarben wie im TRO-Fluss-SVG (tro_flow.render_dot): Uebergabe blau,
|
||||
# Weiche orange, Materialeingang gruen, Materialausgang rot.
|
||||
for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
|
||||
("legend", "#808080"), ("openpoint", "#e00000")):
|
||||
("weiche", "#cc8800"),
|
||||
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
||||
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
||||
("legend", "#808080")):
|
||||
name = f"{prefix}{key.upper()}"
|
||||
names[key] = name
|
||||
if name not in doc.layers:
|
||||
@@ -334,6 +354,40 @@ def _polygon(sides: int, radius: float, start_deg: float) -> list[tuple[float, f
|
||||
]
|
||||
|
||||
|
||||
def shape_outline(symbol: str, r: float) -> list[tuple[float, float]]:
|
||||
"""Aeussere Kontur eines Symbols als Polygon - Rand der Fuellflaeche
|
||||
(Kreise als 48-Eck angenaehert)."""
|
||||
if symbol in (TroSymbol.CIRCLE, TroSymbol.DOUBLE_CIRCLE):
|
||||
return _polygon(48, r, 0)
|
||||
if symbol == TroSymbol.TRIANGLE:
|
||||
return _polygon(3, r, 90)
|
||||
if symbol in (TroSymbol.TRIANGLE_DOWN, TroSymbol.DOUBLE_TRIANGLE_DOWN):
|
||||
return _polygon(3, r, 270)
|
||||
if symbol == TroSymbol.SQUARE:
|
||||
return _polygon(4, r, 45)
|
||||
if symbol == TroSymbol.DIAMOND:
|
||||
return _polygon(4, r, 90)
|
||||
if symbol == TroSymbol.PENTAGON:
|
||||
return _polygon(5, r, 90)
|
||||
if symbol == TroSymbol.HEXAGON:
|
||||
return _polygon(6, r, 0)
|
||||
if symbol == TroSymbol.RECT:
|
||||
w, h = r * 1.45, r * 0.72
|
||||
return [(-w, -h), (w, -h), (w, h), (-w, h)]
|
||||
if symbol == TroSymbol.ARROW:
|
||||
return [(-r * 0.55, -r), (r, 0), (-r * 0.55, r), (-r * 0.55, r * 0.35),
|
||||
(-r, r * 0.35), (-r, -r * 0.35), (-r * 0.55, -r * 0.35)]
|
||||
return _polygon(48, r, 0)
|
||||
|
||||
|
||||
def draw_fill(blk, symbol: str, r: float, fill_hex: str) -> None:
|
||||
"""Typ-eigene Fuellfarbe (wie im TRO-Fluss-SVG) als Solid-Hatch unter der
|
||||
Kontur. Feste Farbe statt BYLAYER: der Layer traegt die Randfarbe der
|
||||
Gruppe, die Fuellung unterscheidet den einzelnen Typ."""
|
||||
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(fill_hex))})
|
||||
hatch.paths.add_polyline_path(shape_outline(symbol, r), is_closed=True)
|
||||
|
||||
|
||||
def draw_shape(blk, symbol: str, r: float) -> None:
|
||||
"""
|
||||
Symbolgeometrie in eine Blockdefinition zeichnen.
|
||||
@@ -399,7 +453,10 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
|
||||
blk = doc.blocks.new(name=name)
|
||||
|
||||
definition = get_tro(type_name)
|
||||
draw_shape(blk, definition.get_symbol() if definition else "", marker)
|
||||
symbol = definition.get_symbol() if definition else ""
|
||||
if definition:
|
||||
draw_fill(blk, symbol, marker, definition.get_fill())
|
||||
draw_shape(blk, symbol, marker)
|
||||
|
||||
gap = marker * 1.5
|
||||
rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)]
|
||||
@@ -481,45 +538,25 @@ def place_markers(
|
||||
return placed, internal
|
||||
|
||||
|
||||
def draw_open_points(
|
||||
msp, analysis: Analysis, transform: Transform, layers: dict[str, str],
|
||||
marker: float, text: float,
|
||||
) -> int:
|
||||
"""Offene Punkte aus cfg/tro_overrides.ini als Fragezeichen-Marker setzen.
|
||||
|
||||
Das sind im Review gemeldete Stellen, die noch kein TRO sind. Sie werden
|
||||
bewusst NICHT als TRO-Symbol gezeichnet, sondern als durchgestrichener
|
||||
Kreis mit Klartext daneben - damit in der Zeichnung sofort erkennbar ist,
|
||||
dass hier eine Entscheidung aussteht und nichts abgeleitet wurde.
|
||||
"""
|
||||
drawn = 0
|
||||
radius = marker * 1.35
|
||||
for point in analysis.overrides.open_points:
|
||||
x, y = transform.to_dxf(point.x, point.y)
|
||||
ring = msp.add_circle((x, y), radius,
|
||||
dxfattribs={"layer": layers["openpoint"]})
|
||||
tag(ring, f"OFFEN:{point.name}")
|
||||
diag = radius * 0.7071
|
||||
for (x1, y1), (x2, y2) in (((-diag, -diag), (diag, diag)),
|
||||
((-diag, diag), (diag, -diag))):
|
||||
line = msp.add_line((x + x1, y + y1), (x + x2, y + y2),
|
||||
dxfattribs={"layer": layers["openpoint"]})
|
||||
tag(line, f"OFFEN:{point.name}")
|
||||
|
||||
lines = [f"OFFEN - noch kein TRO: {point.name}"]
|
||||
if point.expect:
|
||||
lines.append(f"erwartet: {point.expect}")
|
||||
if point.belongs_to:
|
||||
lines.append(f"heute Teil von: {point.belongs_to}")
|
||||
lines.append(f"Anlage x/y: {point.describe_position()}")
|
||||
label = msp.add_mtext("\\P".join(lines), dxfattribs={
|
||||
"layer": layers["openpoint"],
|
||||
"char_height": text,
|
||||
})
|
||||
label.set_location((x + radius * 1.3, y + radius))
|
||||
tag(label, f"OFFEN:{point.name}")
|
||||
drawn += 1
|
||||
return drawn
|
||||
def place_stations(
|
||||
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
||||
layers: dict[str, str], text: float,
|
||||
) -> dict[str, tuple[float, float]]:
|
||||
"""Be-/Entladestationen wie im TRO-Fluss-SVG: kein Symbol, nur der Name
|
||||
in Gruen (Eingang) bzw. Rot (Ausgang) am Ende des Zu-/Ablaufpfeils.
|
||||
Lage wie dort (tro_flow._station_positionen): feste Pfeillaenge ab dem
|
||||
angehaengten TRO, Richtung von dessen Kanten weg."""
|
||||
positionen = _station_positionen(analysis, placed)
|
||||
for station in analysis.boundaries:
|
||||
if station.tro_id not in positionen:
|
||||
continue
|
||||
x, y = positionen[station.tro_id]
|
||||
layer = layers["eingang" if station.type_name == BOUNDARY_ENTRY else "ausgang"]
|
||||
label = msp.add_text(station.label, height=text,
|
||||
dxfattribs={"layer": layer, "style": "Standard"})
|
||||
label.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
|
||||
tag(label, station.tro_id)
|
||||
return positionen
|
||||
|
||||
|
||||
def build_connection_symbol(doc, marker: float, text: float) -> str:
|
||||
@@ -537,9 +574,9 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
|
||||
UID unsichtbar - eindeutige interne Nummer, dauerhaft
|
||||
FROM_UID unsichtbar - interne UID des Quell-TRO
|
||||
TO_UID unsichtbar - interne UID des Ziel-TRO
|
||||
FROM_TRO sichtbar - sprechender Name der Quelle (TRO01)
|
||||
TO_TRO sichtbar - sprechender Name des Ziels (TRO09)
|
||||
KIND unsichtbar - flow | colocated
|
||||
FROM_TRO unsichtbar - sprechender Name der Quelle (TRO01)
|
||||
TO_TRO unsichtbar - sprechender Name des Ziels (TRO09)
|
||||
KIND unsichtbar - flow | weiche | colocated
|
||||
"""
|
||||
name = CONNECTION_BLOCK
|
||||
if name in doc.blocks:
|
||||
@@ -552,13 +589,17 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
|
||||
dxfattribs={"color": 256})
|
||||
blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256})
|
||||
|
||||
# FROM_TRO/TO_TRO unsichtbar: an jeder Kante sichtbar machten sie die
|
||||
# Zeichnung unruhig, im TRO-Fluss-SVG stehen sie auch nicht (Review
|
||||
# 2026-10-02). Die Daten bleiben am Block, CONNECTION_EDIT zeigt sie.
|
||||
offset = size * 1.4
|
||||
for tag_name in ("FROM_TRO", "TO_TRO"):
|
||||
blk.add_attdef(
|
||||
tag_name,
|
||||
insert=(size * 1.3, offset),
|
||||
height=text * 0.8,
|
||||
dxfattribs={"style": "Standard", "color": 256, "text": ""},
|
||||
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
||||
"invisible": 1},
|
||||
)
|
||||
offset -= text * 1.2
|
||||
|
||||
@@ -589,30 +630,57 @@ def draw_connections(
|
||||
"""
|
||||
head = marker * 0.9
|
||||
block = build_connection_symbol(doc, marker, text)
|
||||
stationen = {b.tro_id for b in analysis.boundaries}
|
||||
drawn = 0
|
||||
for src, dst in analysis.edges:
|
||||
if src not in placed or dst not in placed:
|
||||
continue
|
||||
# Farbe/Layer wie im TRO-Fluss-SVG
|
||||
if src in stationen:
|
||||
layer, kind = layers["eingang"], "eingang"
|
||||
elif dst in stationen:
|
||||
layer, kind = layers["ausgang"], "ausgang"
|
||||
elif analysis.edge_kinds.get((src, dst)) == "weiche":
|
||||
layer, kind = layers["weiche"], "weiche"
|
||||
else:
|
||||
layer, kind = layers["flow"], "flow"
|
||||
x1, y1 = placed[src]
|
||||
x2, y2 = placed[dst]
|
||||
length = math.dist((x1, y1), (x2, y2))
|
||||
if length <= 2 * marker * 1.1:
|
||||
if length < 1e-6:
|
||||
continue
|
||||
ux, uy = (x2 - x1) / length, (y2 - y1) / length
|
||||
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht deckt
|
||||
sx, sy = x1 + ux * marker * 1.1, y1 + uy * marker * 1.1
|
||||
ex, ey = x2 - ux * marker * 1.1, y2 - uy * marker * 1.1
|
||||
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
|
||||
# deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
|
||||
kurz_a = marker * (0.3 if src in stationen else 1.1)
|
||||
kurz_b = marker * (0.3 if dst in stationen else 1.1)
|
||||
# Zu kurz fuer einen gekuerzten Pfeil (Marker liegen dicht beieinander,
|
||||
# z.B. HundM05 TRO03->TRO04 mit 500 mm): NICHT weglassen - sonst fehlt
|
||||
# die Verbindung in tro_extract.py und damit in der SCL. Stattdessen
|
||||
# Mitte-zu-Mitte ohne Kuerzung zeichnen.
|
||||
kurz = length <= kurz_a + kurz_b + head
|
||||
if kurz:
|
||||
kurz_a = kurz_b = 0.0
|
||||
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
|
||||
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
|
||||
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
|
||||
dxfattribs={"layer": layers["flow"]})
|
||||
dxfattribs={"layer": layer})
|
||||
tag(shaft, f"{src}->{dst}")
|
||||
# Pfeilspitze als gefuellte Flaeche
|
||||
bx, by = ex - ux * head, ey - uy * head
|
||||
# Pfeilspitze als gefuellte Flaeche (bei kurzer Verbindung in der Mitte,
|
||||
# damit sie nicht im Zielmarker verschwindet)
|
||||
hx, hy = ((x1 + x2) / 2 + ux * head / 2, (y1 + y2) / 2 + uy * head / 2) \
|
||||
if kurz else (ex, ey)
|
||||
bx, by = hx - ux * head, hy - uy * head
|
||||
px, py = -uy * head * 0.38, ux * head * 0.38
|
||||
point = msp.add_solid(
|
||||
[(ex, ey), (bx + px, by + py), (bx - px, by - py)],
|
||||
dxfattribs={"layer": layers["flow"]},
|
||||
[(hx, hy), (bx + px, by + py), (bx - px, by - py)],
|
||||
dxfattribs={"layer": layer},
|
||||
)
|
||||
tag(point, f"{src}->{dst}")
|
||||
if kind in ("eingang", "ausgang"):
|
||||
# Stationspfeile ohne Verbindungsblock: keine TRO-TRO-Beziehung
|
||||
drawn += 1
|
||||
continue
|
||||
|
||||
# Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils
|
||||
ref = msp.add_blockref(
|
||||
@@ -626,7 +694,7 @@ def draw_connections(
|
||||
"TO_UID": internal.get(dst, ""),
|
||||
"FROM_TRO": src,
|
||||
"TO_TRO": dst,
|
||||
"KIND": "flow",
|
||||
"KIND": kind,
|
||||
})
|
||||
tag(ref, f"{src}->{dst}")
|
||||
next_id += 1
|
||||
@@ -653,28 +721,48 @@ def draw_legend(
|
||||
title.set_placement((x0, y0 + row))
|
||||
tag(title)
|
||||
|
||||
# Wie die Legende im TRO-Fluss-SVG: je vorkommendem Typ Fuellfarbe, Rand
|
||||
# der Gruppe und Form, danach die Kantenfarben.
|
||||
used = {t.type_name for t in analysis.tros}
|
||||
groups = []
|
||||
for style in STYLES:
|
||||
types = sorted(n for n in used
|
||||
if (get_tro(n).get_group() if get_tro(n) else "ext") == style.group)
|
||||
if types:
|
||||
groups.append((style, types))
|
||||
zeilen: list[tuple[str, str | None, str | None]] = []
|
||||
for definition in TRO_CATALOG:
|
||||
if definition.get_name() in used:
|
||||
zeilen.append((f"{definition.get_name()} - {definition.describe_symbol()}",
|
||||
definition.get_fill(),
|
||||
layers.get(definition.get_group(), layers["ext"])))
|
||||
pfeile = [("eingang", "Materialeingang (Beladung / Eingang)"),
|
||||
("ausgang", "Materialausgang (Entladung / Ausgang)"),
|
||||
("flow", "Uebergabe / Ein-Ausschleusung"),
|
||||
("weiche", "Weiche (Kreisel-Uebergang)")]
|
||||
|
||||
for index, (style, types) in enumerate(groups):
|
||||
y = y0 - index * row
|
||||
box = msp.add_solid(
|
||||
[(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)],
|
||||
dxfattribs={"layer": layers[style.group]},
|
||||
)
|
||||
y = y0
|
||||
for beschriftung, fill, rand_layer in zeilen:
|
||||
quadrat = [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)]
|
||||
box = msp.add_hatch(dxfattribs={"layer": layers["legend"],
|
||||
"true_color": ezdxf.rgb2int(hex_to_rgb(fill))})
|
||||
box.paths.add_polyline_path(quadrat, is_closed=True)
|
||||
tag(box)
|
||||
label = msp.add_text(
|
||||
f"{style.label}: {', '.join(types)}",
|
||||
height=text,
|
||||
dxfattribs={"layer": layers["legend"], "style": "Standard"},
|
||||
)
|
||||
rand = msp.add_lwpolyline(quadrat, close=True, dxfattribs={"layer": rand_layer})
|
||||
tag(rand)
|
||||
label = msp.add_text(beschriftung, height=text,
|
||||
dxfattribs={"layer": layers["legend"], "style": "Standard"})
|
||||
label.set_placement((x0 + text * 1.8, y - text))
|
||||
tag(label)
|
||||
y -= row
|
||||
for key, beschriftung in pfeile:
|
||||
mitte = y - text / 2
|
||||
linie = msp.add_lwpolyline([(x0 - text * 0.5, mitte), (x0 + text * 0.9, mitte)],
|
||||
dxfattribs={"layer": layers[key]})
|
||||
tag(linie)
|
||||
spitze = msp.add_solid([(x0 + text * 1.4, mitte), (x0 + text * 0.9, mitte + text * 0.3),
|
||||
(x0 + text * 0.9, mitte - text * 0.3)],
|
||||
dxfattribs={"layer": layers[key]})
|
||||
tag(spitze)
|
||||
label = msp.add_text(beschriftung, height=text,
|
||||
dxfattribs={"layer": layers[key], "style": "Standard"})
|
||||
label.set_placement((x0 + text * 1.8, y - text))
|
||||
tag(label)
|
||||
y -= row
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -705,13 +793,6 @@ def report(
|
||||
if counts.get("first_id"):
|
||||
print(f"Interne IDs ab = {format_internal_id(counts['first_id'])} "
|
||||
f"(ID-Raum von SSG_LIB, siehe IDSCHECK)")
|
||||
print(f"Offene Punkte = {counts['openpoints']} (Layer TRO_OPENPOINT, "
|
||||
f"noch kein TRO)")
|
||||
if analysis.overrides.applied:
|
||||
print(f"Typkorrekturen = {len(analysis.overrides.applied)} aus "
|
||||
f"{analysis.overrides.source}")
|
||||
for entry in analysis.overrides.applied:
|
||||
print(f" * {entry}")
|
||||
print(f"Entfernte Objekte= {counts['removed']} (aus frueherem Lauf)")
|
||||
print(f"Layer = {counts['layers']}, Bloecke: {counts['blocks']}")
|
||||
print(f"DXF = {out_file if out_file else '- (--check)'}")
|
||||
@@ -766,17 +847,14 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
metavar="MM", help="Markerradius in mm. Standard: %(default)s")
|
||||
parser.add_argument("--text-height", type=float, default=DEFAULT_TEXT,
|
||||
metavar="MM", help="Texthoehe in mm. Standard: %(default)s")
|
||||
parser.add_argument("--layer-prefix", default=LAYER_PREFIX, metavar="P",
|
||||
help="Praefix der erzeugten Layer. Standard: %(default)s")
|
||||
parser.add_argument("--ids", metavar="DATEI",
|
||||
help="ID-Datei der TROs wie bei tro_flow (nur gelesen). "
|
||||
"Standard: <csv>_tro_ids.ini in %%SKEL_CFG%%.")
|
||||
parser.add_argument("--offset", metavar="DX,DY",
|
||||
help="Lagebezug von Hand setzen statt ermitteln.")
|
||||
parser.add_argument("--re-register", action="store_true",
|
||||
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
||||
"einer abgelegt ist.")
|
||||
parser.add_argument("--max-residual", type=float, default=DEFAULT_MAX_RESIDUAL,
|
||||
metavar="MM",
|
||||
help="Groesster zugelassener Restfehler des Lagebezugs. "
|
||||
"Standard: %(default)s")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="Nur pruefen und berichten, keine Datei schreiben.")
|
||||
parser.add_argument("--emit-lisp", action="store_true",
|
||||
@@ -834,8 +912,22 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
graph = build_graph(elements, csv_file.name, read_warnings)
|
||||
overrides = load_overrides(csv_file.name, cfg_dir)
|
||||
analysis = analyse_tros(graph, elements, overrides)
|
||||
# Derselbe Aufruf wie in tro_flow.main (ohne cfg/tro_overrides.ini, mit
|
||||
# der Separator-Gegenprobe aus <name>_raw.json und der ID-Datei), damit die Zeichnung genau
|
||||
# die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine Datenquelle.
|
||||
raw_json_file = find_sibling_raw_json(csv_file)
|
||||
sepliste_counts = (load_sepliste_counts(raw_json_file, read_warnings)
|
||||
if raw_json_file is not None else {})
|
||||
# Dieselbe ID-Datei wie tro_flow, aber nur lesen: neue TROs bekommen
|
||||
# dieselbe vorlaeufige ID, die tro_flow anhaengen wuerde.
|
||||
ids_file = resolve_ids_file(args.ids, csv_file)
|
||||
try:
|
||||
tro_ids = TroIds.load(ids_file) if ids_file.is_file() else None
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||
tro_ids=tro_ids)
|
||||
if not analysis.tros:
|
||||
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
||||
return 1
|
||||
@@ -855,16 +947,15 @@ def main(argv: list[str] | None = None) -> int:
|
||||
except RegistrationError as exc:
|
||||
print(f"FEHLER: Lagebezug nicht bestimmbar: {exc}", file=sys.stderr)
|
||||
return 4
|
||||
if transform.residual_max > args.max_residual:
|
||||
if transform.residual_max > DEFAULT_MAX_RESIDUAL:
|
||||
print(f"FEHLER: Restfehler des Lagebezugs {transform.residual_max:.1f} mm "
|
||||
f"ueber der Grenze von {args.max_residual:.1f} mm.", file=sys.stderr)
|
||||
f"ueber der Grenze von {DEFAULT_MAX_RESIDUAL:.1f} mm.", file=sys.stderr)
|
||||
print(f" {transform.describe()}", file=sys.stderr)
|
||||
print(" Mit --offset von Hand setzen oder --max-residual anheben.",
|
||||
file=sys.stderr)
|
||||
print(" Mit --offset von Hand setzen.", file=sys.stderr)
|
||||
return 4
|
||||
|
||||
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
|
||||
"openpoints": 0, "first_id": 0}
|
||||
"first_id": 0}
|
||||
|
||||
if args.check:
|
||||
counts["marker"] = sum(1 for t in analysis.tros if t.position)
|
||||
@@ -873,7 +964,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
ensure_appid(doc)
|
||||
counts["removed"] = remove_previous(msp)
|
||||
layers = build_layers(doc, args.layer_prefix)
|
||||
layers = build_layers(doc, LAYER_PREFIX)
|
||||
counts["layers"] = len({v for v in layers.values()})
|
||||
|
||||
# Interne IDs erst NACH remove_previous suchen: die Marker des letzten Laufs
|
||||
@@ -887,13 +978,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
|
||||
counts["first_id"] = next_id
|
||||
|
||||
counts["openpoints"] = draw_open_points(msp, analysis, transform, layers,
|
||||
args.marker_size, args.text_height)
|
||||
|
||||
if args.flow:
|
||||
# Stationen nur mit --flow: sie bestehen aus ihrem Zu-/Ablaufpfeil
|
||||
stationen = place_stations(msp, analysis, placed, layers, args.text_height)
|
||||
counts["flow"] = draw_connections(
|
||||
msp, doc, analysis, placed, internal, layers, args.marker_size,
|
||||
args.text_height, next_id=next_id + len(placed),
|
||||
msp, doc, analysis, {**placed, **stationen}, internal, layers,
|
||||
args.marker_size, args.text_height, next_id=next_id + len(placed),
|
||||
)
|
||||
if args.legend:
|
||||
draw_legend(msp, analysis, placed, layers, args.marker_size,
|
||||
|
||||
+65
-16
@@ -231,6 +231,11 @@ class TroStyle(BaseModel):
|
||||
|
||||
Wichtig: gruen bedeutet **Speicher**, nicht Foerderer. Der Vario ist
|
||||
violett.
|
||||
|
||||
Innerhalb einer Gruppe hat jeder Typ zusaetzlich eine eigene Fuellfarbe
|
||||
(TroDefinition.fill, ein Ton im Farbton der Gruppe); der Rand bleibt die
|
||||
Gruppenfarbe (stroke). So ist jeder Typ an Farbe UND Form eindeutig, und
|
||||
die Gruppe bleibt am Farbton/Rand lesbar.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(validate_assignment=True, extra="forbid",
|
||||
@@ -337,6 +342,12 @@ class TroDefinition(BaseModel):
|
||||
min_length=1,
|
||||
description="Zeichensymbol des Markers im CAD, z. B. 'triangle'",
|
||||
)
|
||||
fill: str = Field(
|
||||
default="",
|
||||
description="Typ-eigene Fuellfarbe als Hex; leer = Fuellfarbe der Farbgruppe. "
|
||||
"Der Rand bleibt immer in der Gruppenfarbe, damit die Gruppe "
|
||||
"trotz eigener Typfarbe erkennbar bleibt.",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -345,6 +356,7 @@ class TroDefinition(BaseModel):
|
||||
items: dict[str, int] | None = None,
|
||||
style: TroStyle | None = None,
|
||||
symbol: str | None = None,
|
||||
fill: str | None = None,
|
||||
**data,
|
||||
) -> None:
|
||||
# Positionsargumente auf Feldnamen abbilden, damit die bisherige
|
||||
@@ -359,6 +371,8 @@ class TroDefinition(BaseModel):
|
||||
data["style"] = style
|
||||
if symbol is not None:
|
||||
data["symbol"] = symbol
|
||||
if fill is not None:
|
||||
data["fill"] = fill
|
||||
super().__init__(**data)
|
||||
|
||||
@field_validator("items", mode="after")
|
||||
@@ -420,14 +434,32 @@ class TroDefinition(BaseModel):
|
||||
"""Kurzname der Farbgruppe, z. B. 'vario'."""
|
||||
return self.style.group
|
||||
|
||||
def get_fill(self) -> str:
|
||||
"""Fuellfarbe dieses Typs: die typ-eigene, sonst die der Farbgruppe."""
|
||||
return self.fill or self.style.fill
|
||||
|
||||
def get_dot_attrs(self) -> dict[str, str]:
|
||||
"""Graphviz-Knotenattribute dieses Typs: Fuell-/Randfarbe der Farbgruppe
|
||||
plus die typ-eigene Form (siehe TroSymbol/dot_shape_attrs) - Farbe
|
||||
gruppiert verwandte Typen, Form unterscheidet den einzelnen Typ."""
|
||||
"""Graphviz-Knotenattribute dieses Typs: typ-eigene Fuellfarbe, Rand in
|
||||
der Farbe der Gruppe, plus die typ-eigene Form (siehe TroSymbol/
|
||||
dot_shape_attrs) - Rand und Farbton gruppieren verwandte Typen,
|
||||
Fuellung und Form unterscheiden den einzelnen Typ."""
|
||||
attrs = self.style.dot_attrs()
|
||||
attrs["fillcolor"] = self.get_fill()
|
||||
attrs.update(dot_shape_attrs(self.symbol))
|
||||
return attrs
|
||||
|
||||
def mermaid_class(self) -> str:
|
||||
"""Mermaid-Klassenname dieses Typs (Typnamen beginnen teils mit einer
|
||||
Ziffer, die Mermaid als Klassenname nicht erlaubt)."""
|
||||
return f"t_{self.name}"
|
||||
|
||||
def mermaid_classdef(self) -> str:
|
||||
"""Mermaid-classDef-Zeile fuer diesen Typ (eigene Fuellung, Gruppenrand)."""
|
||||
return (
|
||||
f"classDef {self.mermaid_class()} fill:{self.get_fill()},"
|
||||
f"stroke:{self.style.stroke},color:{self.style.font}"
|
||||
)
|
||||
|
||||
# -- set ---------------------------------------------------------------
|
||||
#
|
||||
# Jede Zuweisung laeuft durch die Pydantic-Validierung, weil
|
||||
@@ -453,6 +485,10 @@ class TroDefinition(BaseModel):
|
||||
"""Zeichensymbol des Markers setzen."""
|
||||
self.symbol = symbol
|
||||
|
||||
def set_fill(self, fill: str) -> None:
|
||||
"""Typ-eigene Fuellfarbe setzen ('' = Farbe der Gruppe)."""
|
||||
self.fill = fill
|
||||
|
||||
def set_item(self, item: str, count: int) -> None:
|
||||
"""
|
||||
Anzahl einer Bauteilart setzen.
|
||||
@@ -503,29 +539,29 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
# (PriorityManager, JamArea Entry/Exit, HMI, Safety, MachineState).
|
||||
TroDefinition("1Sep", "FB_ILS_MTRO_1Sep", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
}, STYLE_SEP, TroSymbol.CIRCLE),
|
||||
}, STYLE_SEP, TroSymbol.CIRCLE, "#d4e8ff"),
|
||||
# laut TRO_Typen.md: Basis + 1x Switch-Modul
|
||||
TroDefinition("1Sep1Swi", "FB_ILS_MTRO_1Sep1Swi", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SWITCH: 1,
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE),
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE, "#ffe4a0"),
|
||||
# laut TRO_Typen.md: Basis + 2x Switch-Modul + Scanner-Modul
|
||||
TroDefinition("1Sep2Swi", "FB_ILS_MTRO_1Sep2Swi", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 1,
|
||||
}, STYLE_SWI, TroSymbol.PENTAGON),
|
||||
}, STYLE_SWI, TroSymbol.PENTAGON, "#ffc766"),
|
||||
# laut TRO_Typen.md: Basis + Scanner-Modul + SSCC-Modul (Messsensor + WCS)
|
||||
TroDefinition("1Sep_SSCC", "FB_ILS_MTRO_1Sep_SSCC", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.SCANNER: 1,
|
||||
TroItem.SSCC: 1,
|
||||
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE),
|
||||
}, STYLE_SSCC, TroSymbol.DOUBLE_CIRCLE, "#ffd4d4"),
|
||||
# laut TRO_Typen.md: Basis + Vario-Antriebsmodul
|
||||
TroDefinition("Vario", "FB_ILS_MTRO_Vario", {
|
||||
TroItem.SEPARATOR: 1,
|
||||
TroItem.VARIO_DRIVE: 1,
|
||||
}, STYLE_VARIO, TroSymbol.RECT),
|
||||
}, STYLE_VARIO, TroSymbol.RECT, "#e8d4f0"),
|
||||
# laut TRO_Typen.md: Basis (Pin-Separator) + 19x Storage-Line + Scanner
|
||||
# + Encoder + StorageRemoval-Schnittstelle
|
||||
TroDefinition("PinStore_Auto", "FB_ILS_MTRO_PinStore_Auto", {
|
||||
@@ -533,7 +569,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.STORAGE_LINE: 19,
|
||||
TroItem.SCANNER: 1,
|
||||
TroItem.ENCODER: 1,
|
||||
}, STYLE_STORE, TroSymbol.SQUARE),
|
||||
}, STYLE_STORE, TroSymbol.SQUARE, "#d4f0d4"),
|
||||
# aus SCL belegt (FB_ILS_MTRO_Vario_workStation.scl, VAR ab Zeile 77):
|
||||
# fbSeparator1, fbConvVario, fbSepWait, fbBarcodeReader.
|
||||
# Der Vario-Antrieb ist hier die WorkStation-Variante des STRO-Bausteins.
|
||||
@@ -543,7 +579,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.WORK_STATION: 1,
|
||||
TroItem.ACCUMULATE: 1,
|
||||
TroItem.SCANNER: 1,
|
||||
}, STYLE_VARIO, TroSymbol.HEXAGON),
|
||||
}, STYLE_VARIO, TroSymbol.HEXAGON, "#caa6e0"),
|
||||
# aus SCL belegt (FB_EmptyCarrBuffer.scl): arfbSeparatorStorage als
|
||||
# Array[1..5] of FB_ILS_STRO_Sep + 5x fbCarrierWaitStore.
|
||||
# Kein eigener Eingangs-Separator - der "inTro" ist ein separater 1Sep-TRO
|
||||
@@ -551,7 +587,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroDefinition("EmptyCarrBuffer", "FB_EmptyCarrBuffer", {
|
||||
TroItem.STORAGE_LINE: 5,
|
||||
TroItem.ACCUMULATE: 5,
|
||||
}, STYLE_STORE, TroSymbol.DIAMOND),
|
||||
}, STYLE_STORE, TroSymbol.DIAMOND, "#a6dca6"),
|
||||
# aus SCL belegt (FB_LoadingBoom_INBOUND.scl): fbTiltSensor,
|
||||
# fbDistanceFoot, fbWorkSation. Bewusst KEIN Separator und KEINE Weiche -
|
||||
# der einzige nicht additive Typ.
|
||||
@@ -559,7 +595,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.BOOM: 1,
|
||||
TroItem.FOOT: 1,
|
||||
TroItem.TILT_SENSOR: 1,
|
||||
}, STYLE_EXT, TroSymbol.ARROW),
|
||||
}, STYLE_EXT, TroSymbol.ARROW, "#d9d9d9"),
|
||||
# aus SCL belegt (FB_ILS_MTRO_2Sep1Swi.scl, VAR ab Zeile 115):
|
||||
# fbSeparator1, fbSeparator2, fbSwitch1, fbBarcodeReader1, fbBarcodeReader2.
|
||||
# Die Mapping-Tabelle in TRO_Typen.md nennt die beiden Scanner nicht -
|
||||
@@ -568,7 +604,7 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.SEPARATOR: 2,
|
||||
TroItem.SWITCH: 1,
|
||||
TroItem.SCANNER: 2,
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN),
|
||||
}, STYLE_SWI, TroSymbol.TRIANGLE_DOWN, "#ffd6ad"),
|
||||
# ACHTUNG - NICHT aus SCL belegt. Alle Typen oberhalb stammen aus
|
||||
# vorhandenem Code bzw. der Mapping-Tabelle in TRO_Typen.md; dieser hier
|
||||
# wurde am 2026-08-12 auf Anforderung aus dem Projekt 500573 (Mubea)
|
||||
@@ -582,7 +618,18 @@ TRO_CATALOG: tuple[TroDefinition, ...] = (
|
||||
TroItem.SEPARATOR: 2,
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 2,
|
||||
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN),
|
||||
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#f2a65a"),
|
||||
# ACHTUNG - NICHT aus SCL belegt, wie 2Sep2Swi. Aufgenommen am 2026-10-14
|
||||
# fuer die Testanlage data/export.csv: Weichenknoten, an dem die Umlenkung
|
||||
# eines Kreisels zwei andere beruehrt (Kreisel 5 an 4 und 6) - drei
|
||||
# Separatoren laufen in Laufrichtung auf die zwei Weichen zu (siehe
|
||||
# lib/kreisel_junctions.py). Scanner je Separator analog zu 2Sep2Swi.
|
||||
# FB_ILS_MTRO_3Sep2Swi existiert noch nicht in der ILSLib.
|
||||
TroDefinition("3Sep2Swi", "FB_ILS_MTRO_3Sep2Swi", {
|
||||
TroItem.SEPARATOR: 3,
|
||||
TroItem.SWITCH: 2,
|
||||
TroItem.SCANNER: 3,
|
||||
}, STYLE_SWI, TroSymbol.DOUBLE_TRIANGLE_DOWN, "#d9822b"),
|
||||
)
|
||||
|
||||
_BY_NAME: dict[str, TroDefinition] = {t.get_name(): t for t in TRO_CATALOG}
|
||||
@@ -610,5 +657,7 @@ def catalog() -> dict[str, TroDefinition]:
|
||||
|
||||
|
||||
def mermaid_classdefs() -> list[str]:
|
||||
"""classDef-Zeilen fuer ein Mermaid-Diagramm, eine je Farbgruppe."""
|
||||
return [style.mermaid_classdef() for style in STYLES]
|
||||
"""classDef-Zeilen fuer ein Mermaid-Diagramm: eine je Farbgruppe (u. a.
|
||||
'ext' fuer die Systemgrenzen) plus eine je Typ (t_<Name>)."""
|
||||
return ([style.mermaid_classdef() for style in STYLES]
|
||||
+ [definition.mermaid_classdef() for definition in TRO_CATALOG])
|
||||
+2178
-492
File diff suppressed because it is too large.
Load diff
@@ -1,338 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
||||
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
||||
"""
|
||||
tro_geometry.py - Separator-Host und Weichen-Fanout direkt aus der Geometrie.
|
||||
|
||||
Ergaenzt tro_flow.py::derive_tros() um einen zweiten Weg, zwei seiner Eingaben
|
||||
zu bestimmen:
|
||||
|
||||
Zuordnung (Host des Separators) -> resolve_host_geometric
|
||||
Nachbarn/graph.edges (Fanout) -> count_geometric_exits
|
||||
|
||||
Beide Werte kommen normalerweise aus den CSV-Spalten "Zuordnung"/"Nachbarn"
|
||||
(material_flow.py::build_graph). Dieses Modul sucht sie stattdessen direkt in
|
||||
den Weltkoordinaten der Bauteile: ein shapely-STRtree ueber die AS/ES-Enden
|
||||
(K1/K2) jeder Strecke/Gefaellestrecke sowie die beiden Kreisel-Bahnenden
|
||||
liefert per .nearest() den naechstgelegenen Host zu einem Separator und per
|
||||
.query(..., predicate="dwithin") alle anderen Ketten, die sich an einem
|
||||
gegebenen Punkt tatsaechlich treffen - unabhaengig davon, ob die betroffene
|
||||
Zeile im CSV eine vollstaendige "Nachbarn"-Liste hat.
|
||||
|
||||
Hintergrund: hundmM05_1.csv zeigt fuer mehrere "ILS 2.0 Strecke"-Zeilen den
|
||||
Fehler "nur ein Partner" (Spalte Fehler) - die von dxfmakros berechnete
|
||||
Nachbarschaft ist dort unvollstaendig. Eine fruehere, gleichartige Datei
|
||||
(hundm_export.csv) zeigte den Ausfall dieser Abhaengigkeit in voller Wucht:
|
||||
tro_flow.py leitete 5 TROs ab, alle vom Typ Vario, 0 Verbindungen, alle
|
||||
isoliert (siehe results/hundm_export_tro_doc.md) - Zuordnung/Nachbarn liessen
|
||||
sich fuer diesen Export nicht ausreichend aufloesen. Dieses Modul macht die
|
||||
TRO-Ableitung fuer genau diesen Fall unabhaengig von der Qualitaet dieser
|
||||
beiden Spalten.
|
||||
|
||||
Nutzung: `tro_flow.py --geometry` (siehe dort, `make_geometric_resolvers`).
|
||||
Dieses Modul kennt tro_flow.py nicht - nur material_flow.py::Element-Listen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from shapely import STRtree
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from material_flow import (
|
||||
LANE_LEFT,
|
||||
LANE_RIGHT,
|
||||
UNASSIGNED,
|
||||
Element,
|
||||
_as_float,
|
||||
_merkmal,
|
||||
)
|
||||
|
||||
# Kreisel-Durchmesser (mm), falls kein Merkmal mit dem Durchmesser vorliegt -
|
||||
# Default wie dxfmakros' cfg/export.cfg [Kreisel] durchmesser_mm.
|
||||
DEFAULT_KREISEL_DURCHMESSER_MM = 800.0
|
||||
|
||||
# Merkmal-Namen, unter denen der Achsabstand der beiden Kreiselstationen
|
||||
# (AN8/SP8) im CSV-Export auftaucht - in Metern, siehe kreisel_capsule().
|
||||
ABSTAND_MERKMALE = (
|
||||
"Abstand (Kreiselachse A - Kreiselachse) in Meter",
|
||||
"Abstand_m",
|
||||
"ABSTAND",
|
||||
)
|
||||
|
||||
# Default-Toleranz (mm), innerhalb derer zwei Anschlusspunkte als "derselbe
|
||||
# Uebergabepunkt" gelten. Bewusst zwischen dxfmakros' omniflo_ks_toleranz_mm
|
||||
# (10mm, enge KOS-Deckung von Omniflo-Steckverbindern) und vf_linienzug.lsp's
|
||||
# *vfl-kette-tol-weit* (100mm, "sieht verbunden aus, ist es aber vielleicht
|
||||
# nicht") - zwei Kettenenden an einem echten mechanischen Uebergang treffen
|
||||
# sich nicht auf den zehntel Millimeter, muessen aber auch nicht 10cm
|
||||
# auseinanderliegen duerfen, um noch als verbunden zu gelten.
|
||||
DEFAULT_TOLERANCE_MM = 50.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class _EndpointEntry:
|
||||
"""Ein indizierter Anschlusspunkt: welches Bauteil, welches Ende."""
|
||||
|
||||
teile_id: str
|
||||
end: str # "K1"/"K2" (AS/ES) oder "a"/"b" (Kreisel-Bahnende)
|
||||
|
||||
|
||||
def chain_endpoints(
|
||||
element: Element,
|
||||
) -> tuple[tuple[float, float] | None, tuple[float, float] | None]:
|
||||
"""AS-(K1) und ES-(K2) Weltpunkt einer Strecke/Gefaellestrecke.
|
||||
|
||||
Fehlen K1/K2 (aeltere Exporte ohne AS/ES-Punkte), Ersatz durch die Mitten
|
||||
der kurzen Stirnseiten der Bounding-Box entlang ihrer laengeren Ausdehnung
|
||||
- gleiche Idee wie dxfmakros' export_neighbors.py::_strecke_ends, nur ohne
|
||||
dessen "Anfang liegt naeher am Einfuegepunkt"-Zuordnung: fuer die reine
|
||||
Fanout-Zaehlung an einem Punkt (count_geometric_exits) ist unerheblich,
|
||||
welches Ende AS und welches ES ist.
|
||||
"""
|
||||
by_label = dict(element.kpoints)
|
||||
k1, k2 = by_label.get("K1"), by_label.get("K2")
|
||||
if k1 is not None and k2 is not None:
|
||||
return k1, k2
|
||||
if element.x is None or element.y is None or element.bbox is None:
|
||||
return k1, k2
|
||||
cx, cy = element.x, element.y
|
||||
dx, dy = element.bbox_x or 0.0, element.bbox_y or 0.0
|
||||
if dx >= dy:
|
||||
bbox_ends = ((cx - dx / 2.0, cy), (cx + dx / 2.0, cy))
|
||||
else:
|
||||
bbox_ends = ((cx, cy - dy / 2.0), (cx, cy + dy / 2.0))
|
||||
return k1 or bbox_ends[0], k2 or bbox_ends[1]
|
||||
|
||||
|
||||
def kreisel_durchmesser_mm(element: Element) -> float:
|
||||
"""Kreisel-Durchmesser (mm) aus einem Merkmal, sonst DEFAULT_*."""
|
||||
for key in ("Durchmesser_mm", "Durchmesser (mm)", "Durchmesser"):
|
||||
value = _as_float(_merkmal(element.merkmale, key))
|
||||
if value:
|
||||
return value
|
||||
return DEFAULT_KREISEL_DURCHMESSER_MM
|
||||
|
||||
|
||||
def kreisel_capsule(element: Element, durchmesser_mm: float | None = None) -> dict | None:
|
||||
"""Achsen-Segment (Kapsel) eines Kreisels in Weltkoordinaten.
|
||||
|
||||
dxfmakros' eigenes export_neighbors.py::_kreisel_capsule setzt die
|
||||
Antriebsstation (AN8) lokal bei (radius, 0) und die Spannstation (SP8) bei
|
||||
(radius+abstand, 0) - relativ zum BLOCK-Einfuegepunkt (lokaler Ursprung
|
||||
des KREISEL_n-Wrappers, siehe KreiselInsert.lsp::draw-module). Die CSV-
|
||||
Spalte "Position" fuehrt bei einem Kreisel jedoch nachweislich den
|
||||
BBOX-MITTELPUNKT, nicht diesen Einfuegepunkt: die exportierte Boundingbox
|
||||
ist exakt symmetrisch um Position (halbe Breite = (abstand+durchmesser)/2,
|
||||
siehe hundmM05_1.csv Zeile 6/7 - AN8-lokal-(radius,0) waere das nicht).
|
||||
AN8/SP8 werden daher symmetrisch um Position gelegt: AN8 bei
|
||||
(-abstand/2, 0), SP8 bei (+abstand/2, 0) - beide auf der lokalen X-Achse,
|
||||
gedreht um das Merkmal "Drehung" (Grad, CCW positiv).
|
||||
|
||||
Rueckgabe {"a": (x,y), "b": (x,y), "r": radius} (a = AN8-, b = SP8-Mitte),
|
||||
oder None ohne Position.
|
||||
"""
|
||||
if element.x is None or element.y is None:
|
||||
return None
|
||||
abstand_m = None
|
||||
for key in ABSTAND_MERKMALE:
|
||||
abstand_m = _as_float(_merkmal(element.merkmale, key))
|
||||
if abstand_m is not None:
|
||||
break
|
||||
abstand_mm = (abstand_m or 0.0) * 1000.0
|
||||
durchmesser = durchmesser_mm or kreisel_durchmesser_mm(element)
|
||||
r = durchmesser / 2.0
|
||||
drehung = _as_float(_merkmal(element.merkmale, "Drehung")) or 0.0
|
||||
rad = math.radians(drehung)
|
||||
cos_r, sin_r = math.cos(rad), math.sin(rad)
|
||||
|
||||
def world(lx: float, ly: float) -> tuple[float, float]:
|
||||
return (element.x + lx * cos_r - ly * sin_r,
|
||||
element.y + lx * sin_r + ly * cos_r)
|
||||
|
||||
half = abstand_mm / 2.0
|
||||
return {"a": world(-half, 0.0), "b": world(half, 0.0), "r": r}
|
||||
|
||||
|
||||
def kreisel_lane_for_point(capsule: dict, point: tuple[float, float]) -> str:
|
||||
"""Welche Bahn (L/R) liegt ein Punkt bezogen auf die Kreiselachse a->b.
|
||||
|
||||
Kreuzprodukt Achse x (Punkt - a): positiv = links der Achsrichtung
|
||||
(mathematisch positive/CCW-Drehung von der Achse aus), konsistent mit
|
||||
dxfmakros' "links" = lokal Y in [0, +radius] (siehe kreisel_half_bboxes).
|
||||
"""
|
||||
ax, ay = capsule["a"]
|
||||
bx, by = capsule["b"]
|
||||
px, py = point
|
||||
cross = (bx - ax) * (py - ay) - (by - ay) * (px - ax)
|
||||
return LANE_LEFT if cross >= 0 else LANE_RIGHT
|
||||
|
||||
|
||||
class GeometryIndex:
|
||||
"""STRtree-Index der Bauteil-Geometrie fuer die geometrische TRO-Ableitung.
|
||||
|
||||
Zwei getrennte Baeume:
|
||||
_host_tree eine LineString je Gefaellestrecke/Strecke (K1->K2-Achse)
|
||||
bzw. Kreisel (AN8->SP8-Achse), abgefragt mit .nearest() -
|
||||
"welcher Bauteil-ACHSE liegt dieser Separator am
|
||||
naechsten" (resolve_host_geometric). Bewusst die Achse,
|
||||
nicht die lose Bounding-Box: ein Kreisel deckt mit
|
||||
seiner Box leicht 20+ Meter ab (Antriebs- bis
|
||||
Spannstation), ein Separator auf einer nahen VF-Kette
|
||||
waere nach reiner Box-Distanz faelschlich "im Kreisel",
|
||||
obwohl er auf der Kette sitzt (siehe Modul-Historie:
|
||||
genau dieser Fehler zeigte sich beim ersten Testlauf
|
||||
gegen hundmM05_1.csv).
|
||||
_endpoint_tree ein Punkt je AS/ES-Ende einer Strecke/Gefaellestrecke
|
||||
plus je Kreisel-Bahnende, abgefragt mit .query(...,
|
||||
predicate="dwithin") - "wie viele andere Ketten treffen
|
||||
sich an diesem Punkt" (count_geometric_exits).
|
||||
"""
|
||||
|
||||
def __init__(self, elements: list[Element], durchmesser_mm: float | None = None):
|
||||
self.durchmesser_mm = durchmesser_mm
|
||||
self._host_ids: list[str] = []
|
||||
host_axes = []
|
||||
# Kreisel-Kapseln nach TeileId, um am Treffer die Bahn (-L/-R) zu
|
||||
# bestimmen - graph.nodes kennt Kreisel nur unter "<id>-L"/"<id>-R",
|
||||
# nie unter der blanken TeileId (siehe material_flow.py::_build_nodes).
|
||||
self._kreisel_capsules: dict[str, dict] = {}
|
||||
self._endpoints: list[_EndpointEntry] = []
|
||||
endpoint_points = []
|
||||
|
||||
for element in elements:
|
||||
if element.kind in ("Gefaellestrecke", "Strecke"):
|
||||
k1, k2 = chain_endpoints(element)
|
||||
if k1 is None or k2 is None:
|
||||
continue
|
||||
self._host_ids.append(element.teile_id)
|
||||
host_axes.append(LineString([k1, k2]))
|
||||
for label, point in (("K1", k1), ("K2", k2)):
|
||||
self._endpoints.append(_EndpointEntry(element.teile_id, label))
|
||||
endpoint_points.append(Point(point))
|
||||
elif element.kind == "Kreisel":
|
||||
if element.x is None or element.y is None:
|
||||
continue
|
||||
capsule = kreisel_capsule(element, durchmesser_mm)
|
||||
if capsule is None:
|
||||
continue
|
||||
self._host_ids.append(element.teile_id)
|
||||
host_axes.append(LineString([capsule["a"], capsule["b"]]))
|
||||
self._kreisel_capsules[element.teile_id] = capsule
|
||||
for end in ("a", "b"):
|
||||
self._endpoints.append(_EndpointEntry(element.teile_id, end))
|
||||
endpoint_points.append(Point(capsule[end]))
|
||||
|
||||
self._host_tree = STRtree(host_axes) if host_axes else None
|
||||
self._endpoint_tree = STRtree(endpoint_points) if endpoint_points else None
|
||||
|
||||
def nearest_host(self, x: float, y: float) -> str | None:
|
||||
"""Naechster Host-Knoten nach Achsabstand (Gefaellestrecke/Strecke-
|
||||
TeileId, oder "<Kreisel-TeileId>-L"/"-R" - dieselbe Schreibweise wie
|
||||
graph.nodes)."""
|
||||
if self._host_tree is None:
|
||||
return None
|
||||
index = self._host_tree.nearest(Point(x, y))
|
||||
if index is None:
|
||||
return None
|
||||
teile_id = self._host_ids[int(index)]
|
||||
capsule = self._kreisel_capsules.get(teile_id)
|
||||
if capsule is None:
|
||||
return teile_id
|
||||
lane = kreisel_lane_for_point(capsule, (x, y))
|
||||
return f"{teile_id}-{lane}"
|
||||
|
||||
def exits_near(self, x: float, y: float, tol_mm: float, exclude_ids: set[str]) -> set[str]:
|
||||
"""Andere Bauteil-Ids, deren Anschlusspunkt innerhalb tol_mm von (x,y) liegt."""
|
||||
if self._endpoint_tree is None:
|
||||
return set()
|
||||
hits = self._endpoint_tree.query(Point(x, y), predicate="dwithin", distance=tol_mm)
|
||||
return {self._endpoints[int(i)].teile_id for i in hits} - exclude_ids
|
||||
|
||||
|
||||
def resolve_host_geometric(separator: Element, index: GeometryIndex) -> str | None:
|
||||
"""Naechster Host zum Separator rein nach Weltkoordinate."""
|
||||
if separator.x is None or separator.y is None:
|
||||
return None
|
||||
return index.nearest_host(separator.x, separator.y)
|
||||
|
||||
|
||||
def _strip_lane(host_id: str) -> str:
|
||||
""""0006-L" -> "0006" (Kreisel-Bahnsuffix entfernen; alles andere unveraendert)."""
|
||||
if host_id.endswith(("-L", "-R")):
|
||||
return host_id[:-2]
|
||||
return host_id
|
||||
|
||||
|
||||
def count_geometric_exits(
|
||||
hosts: list[str],
|
||||
elements_by_id: dict[str, Element],
|
||||
index: GeometryIndex,
|
||||
tol_mm: float = DEFAULT_TOLERANCE_MM,
|
||||
) -> list[str]:
|
||||
"""Andere Ketten, die geometrisch an einem Ende von `hosts` zusammentreffen.
|
||||
|
||||
Entspricht tro_flow.py::_switch_exits, aber unabhaengig von graph.edges:
|
||||
beide Enden JEDES Hosts (K1 und K2 bzw. beide Kreisel-Bahnenden) werden
|
||||
abgefragt, die Treffer aller Enden vereinigt - wie eine aus beiden Enden
|
||||
zusammengefuehrte "Nachbarn"-Spalte, nur direkt aus der Geometrie statt aus
|
||||
der (fuer diese Bauteile teils unvollstaendigen) CSV-Spalte.
|
||||
|
||||
`hosts` kann Kreisel-Bahnen als "<TeileId>-L"/"-R" enthalten (siehe
|
||||
GeometryIndex.nearest_host) - die Endpunkt-Eintraege im Index fuehren dazu
|
||||
nur die blanke TeileId, daher wird der Suffix hier wie dort entfernt.
|
||||
"""
|
||||
exclude = {_strip_lane(h) for h in hosts}
|
||||
found: set[str] = set()
|
||||
for host_id in hosts:
|
||||
element = elements_by_id.get(_strip_lane(host_id))
|
||||
if element is None:
|
||||
continue
|
||||
if element.kind == "Kreisel":
|
||||
capsule = kreisel_capsule(element, index.durchmesser_mm)
|
||||
points = (capsule["a"], capsule["b"]) if capsule else ()
|
||||
else:
|
||||
points = tuple(p for p in chain_endpoints(element) if p is not None)
|
||||
for x, y in points:
|
||||
found |= index.exits_near(x, y, tol_mm, exclude)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def make_geometric_resolvers(
|
||||
elements: list[Element],
|
||||
tol_mm: float = DEFAULT_TOLERANCE_MM,
|
||||
durchmesser_mm: float | None = None,
|
||||
):
|
||||
"""resolve_host/count_exits-Paar fuer tro_flow.py::derive_tros(...).
|
||||
|
||||
Ersetzt Zuordnung (Spalte) und Nachbarn/graph.edges durch eine direkte
|
||||
STRtree-Nachbarsuche ueber die Weltkoordinaten der Bauteile. Weicht der
|
||||
geometrisch gefundene Host von der CSV-Spalte "Zuordnung" ab, wird das als
|
||||
Hinweis protokolliert statt stillschweigend ignoriert - das deckt zugleich
|
||||
auf, wo die CSV-Spalte fehlerhaft ist.
|
||||
"""
|
||||
index = GeometryIndex(elements, durchmesser_mm)
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
|
||||
def resolve_host(sep: Element, findings: list[str]) -> str | None:
|
||||
host = resolve_host_geometric(sep, index)
|
||||
if host is None:
|
||||
findings.append(
|
||||
f"Separator {sep.teile_id} '{sep.name}': geometrisch kein Host "
|
||||
f"gefunden (CSV-Zuordnung '{sep.assignment or '-'}') - keinem "
|
||||
f"TRO zugeordnet"
|
||||
)
|
||||
return None
|
||||
if sep.assignment and sep.assignment != UNASSIGNED and sep.assignment != host:
|
||||
findings.append(
|
||||
f"Separator {sep.teile_id} '{sep.name}': geometrischer Host "
|
||||
f"'{host}' weicht von CSV-Zuordnung '{sep.assignment}' ab"
|
||||
)
|
||||
return host
|
||||
|
||||
def count_exits(hosts: list[str]) -> list[str]:
|
||||
return count_geometric_exits(hosts, by_id, index, tol_mm)
|
||||
|
||||
return resolve_host, count_exits
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
tro_ids.py - stabile TRO-IDs ueber eine ID-Datei je Anlage.
|
||||
|
||||
Die TRO-IDs vergeben die SPS-Programmierer; eine ID muss nur eindeutig sein.
|
||||
Sobald SCL-Code steht, darf sich eine ID nicht mehr aendern, nur weil das
|
||||
Layout neu exportiert wurde. Die Layout-TeileId taugt dafuer nicht (sie
|
||||
wechselt zwischen zwei Exporten derselben Anlage), stabil ist nur die Lage.
|
||||
Siehe doc/Konzept_Layout_Hardware_SCL.md, "Festlegungen" -> "TRO-IDs".
|
||||
|
||||
Die ID-Datei (`%SKEL_CFG%/<csv>_tro_ids.ini`, versioniert, von Hand
|
||||
umbenennbar) haelt je TRO die ID und die Lage seines Anker-Separators:
|
||||
|
||||
[tro]
|
||||
TRO01 = 1234.5, -567.8 ; 1Sep1Swi | Separator 0003 auf Kreisel2 Bahn L
|
||||
K0102_1 = 2210.0, 410.0 ; von Hand umbenannt
|
||||
|
||||
Ablauf (assign_ids):
|
||||
|
||||
- ohne ID-Datei bzw. mit renumber: alle TROs in der uebergebenen Reihenfolge
|
||||
(entlang des Materialflusses) neu durchnummerieren, TRO01, TRO02, ...
|
||||
- mit ID-Datei: jeder Eintrag findet den TRO, dessen Separator seiner Lage am
|
||||
naechsten liegt (innerhalb der Toleranz, je TRO hoechstens ein Eintrag).
|
||||
Nicht gefundene TROs bekommen die naechste freie Nummer, nicht gefundene
|
||||
Eintraege bleiben in der Datei stehen und ihre ID wird nicht wiederverwendet.
|
||||
|
||||
Das Modul liest und schreibt nur die Datei; Lage und Reihenfolge der TROs
|
||||
liefert tro_flow.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from parameter import get_float
|
||||
|
||||
__all__ = [
|
||||
"SECTION",
|
||||
"DEFAULT_TOLERANCE",
|
||||
"IdEntry",
|
||||
"IdAssignment",
|
||||
"TroIds",
|
||||
"ids_path",
|
||||
"load_ids",
|
||||
"assign_ids",
|
||||
"render_ids",
|
||||
"render_new_entries",
|
||||
"insert_entries",
|
||||
"other_sections",
|
||||
]
|
||||
|
||||
SECTION = "tro"
|
||||
ID_FILE_SUFFIX = "_tro_ids.ini"
|
||||
PREFIX = "TRO"
|
||||
|
||||
# Abstand (mm) zwischen Eintrag und Separator, bis zu dem ein TRO als
|
||||
# wiedergefunden gilt - aus cfg/parameter.ini [tro_ids] (lib/parameter.py).
|
||||
DEFAULT_TOLERANCE = get_float("tro_ids", "default_tolerance")
|
||||
|
||||
# Eine ID wird zum SCL-Bezeichner (Instanz #<ID>, "DB_TRO_TEST".<ID>).
|
||||
_ID_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
# SYSn sind die Systemgrenzen (tro_flow.derive_boundaries), keine TROs.
|
||||
_RESERVED = re.compile(r"^SYS\d+$")
|
||||
_NUMBERED = re.compile(rf"^{PREFIX}(\d+)$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdEntry:
|
||||
"""Ein Eintrag der ID-Datei: ID und Lage des Anker-Separators (mm)."""
|
||||
|
||||
tro_id: str
|
||||
x: float
|
||||
y: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdAssignment:
|
||||
"""Ergebnis von assign_ids, fuer den Bericht."""
|
||||
|
||||
renumbered: bool = False
|
||||
# (ID, Abstand in mm) der wiedergefundenen TROs
|
||||
kept: list[tuple[str, float]] = field(default_factory=list)
|
||||
# IDs, die in diesem Lauf neu vergeben wurden (ohne renumbered)
|
||||
new: list[str] = field(default_factory=list)
|
||||
# Eintraege der Datei ohne passenden TRO
|
||||
missing: list[IdEntry] = field(default_factory=list)
|
||||
# weggefallene ID -> TRO, dessen Separator dort liegt, der aber schon eine
|
||||
# naehere ID traegt (z. B. zwei TROs zu einem 2Sep2Swi zusammengelegt)
|
||||
merged_into: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TroIds:
|
||||
"""ID-Datei einer Anlage plus Einstellungen fuer einen Lauf.
|
||||
|
||||
entries ist leer, wenn die Datei (noch) nicht existiert. result fuellt
|
||||
tro_flow.derive_tros ueber assign_ids.
|
||||
"""
|
||||
|
||||
path: Path
|
||||
entries: list[IdEntry] = field(default_factory=list)
|
||||
exists: bool = False
|
||||
tolerance: float = DEFAULT_TOLERANCE
|
||||
renumber: bool = False
|
||||
result: IdAssignment | None = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path, *, tolerance: float = DEFAULT_TOLERANCE,
|
||||
renumber: bool = False) -> "TroIds":
|
||||
exists = path.is_file()
|
||||
entries = load_ids(path) if exists else []
|
||||
return cls(path=path, entries=entries, exists=exists,
|
||||
tolerance=tolerance, renumber=renumber)
|
||||
|
||||
|
||||
def ids_path(csv_stem: str, cfg_dir: Path) -> Path:
|
||||
"""Standardpfad der ID-Datei zu einer CSV: <cfg>/<stem>_tro_ids.ini."""
|
||||
return cfg_dir / f"{csv_stem}{ID_FILE_SUFFIX}"
|
||||
|
||||
|
||||
def _parser() -> configparser.ConfigParser:
|
||||
parser = configparser.ConfigParser(
|
||||
inline_comment_prefixes=(";", "#"),
|
||||
interpolation=None,
|
||||
strict=True,
|
||||
)
|
||||
parser.optionxform = str # IDs nicht in Kleinbuchstaben wandeln
|
||||
return parser
|
||||
|
||||
|
||||
def load_ids(path: Path) -> list[IdEntry]:
|
||||
"""ID-Datei lesen. ValueError bei doppelter ID, falscher Lage oder ID."""
|
||||
parser = _parser()
|
||||
try:
|
||||
parser.read(path, encoding="utf-8")
|
||||
except configparser.Error as exc:
|
||||
raise ValueError(f"{path}: {exc}") from exc
|
||||
if not parser.has_section(SECTION):
|
||||
raise ValueError(f"{path}: Abschnitt [{SECTION}] fehlt")
|
||||
|
||||
entries: list[IdEntry] = []
|
||||
for tro_id, value in parser.items(SECTION):
|
||||
if not _ID_PATTERN.match(tro_id):
|
||||
raise ValueError(f"{path}: '{tro_id}' ist kein gueltiger SCL-Bezeichner")
|
||||
if _RESERVED.match(tro_id):
|
||||
raise ValueError(f"{path}: '{tro_id}' ist fuer Systemgrenzen reserviert")
|
||||
parts = [p.strip() for p in value.split(",")]
|
||||
try:
|
||||
x, y = (float(p) for p in parts)
|
||||
except ValueError:
|
||||
raise ValueError(f"{path}: {tro_id} = '{value}' - erwartet 'x, y' in mm") \
|
||||
from None
|
||||
entries.append(IdEntry(tro_id, x, y))
|
||||
return entries
|
||||
|
||||
|
||||
def _distance(tro, entry: IdEntry, anchors: dict[str, tuple[float, float]]) -> float:
|
||||
"""Kleinster Abstand eines Separators des TRO zum Eintrag (inf ohne Lage)."""
|
||||
best = math.inf
|
||||
for sep in tro.separators:
|
||||
pos = anchors.get(sep)
|
||||
if pos is not None:
|
||||
best = min(best, math.hypot(pos[0] - entry.x, pos[1] - entry.y))
|
||||
return best
|
||||
|
||||
|
||||
def _width(count: int) -> int:
|
||||
return max(2, len(str(count)))
|
||||
|
||||
|
||||
def assign_ids(
|
||||
tros: list,
|
||||
anchors: dict[str, tuple[float, float]],
|
||||
entries: list[IdEntry],
|
||||
*,
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
renumber: bool = False,
|
||||
) -> IdAssignment:
|
||||
"""tro.tro_id fuer alle TROs setzen (Reihenfolge = Reihenfolge der Liste).
|
||||
|
||||
anchors: Separator-TeileId -> (x, y) in mm.
|
||||
"""
|
||||
result = IdAssignment(renumbered=renumber or not entries)
|
||||
if result.renumbered:
|
||||
width = _width(len(tros))
|
||||
for index, tro in enumerate(tros, start=1):
|
||||
tro.tro_id = f"{PREFIX}{index:0{width}d}"
|
||||
return result
|
||||
|
||||
# Paare nach Abstand; jeder TRO und jeder Eintrag hoechstens einmal
|
||||
pairs = sorted(
|
||||
(d, i, j)
|
||||
for i, tro in enumerate(tros)
|
||||
for j, entry in enumerate(entries)
|
||||
if (d := _distance(tro, entry, anchors)) <= tolerance
|
||||
)
|
||||
tro_taken: dict[int, tuple[int, float]] = {}
|
||||
entry_taken: set[int] = set()
|
||||
for d, i, j in pairs:
|
||||
if i in tro_taken or j in entry_taken:
|
||||
continue
|
||||
tro_taken[i] = (j, d)
|
||||
entry_taken.add(j)
|
||||
|
||||
used = {e.tro_id for e in entries}
|
||||
numbers = [int(m.group(1)) for e in entries if (m := _NUMBERED.match(e.tro_id))]
|
||||
widths = [len(m.group(1)) for e in entries if (m := _NUMBERED.match(e.tro_id))]
|
||||
width = max(widths + [_width(len(tros))])
|
||||
next_number = max(numbers, default=0) + 1
|
||||
|
||||
for i, tro in enumerate(tros):
|
||||
if i in tro_taken:
|
||||
j, d = tro_taken[i]
|
||||
tro.tro_id = entries[j].tro_id
|
||||
result.kept.append((tro.tro_id, d))
|
||||
continue
|
||||
while f"{PREFIX}{next_number:0{width}d}" in used:
|
||||
next_number += 1
|
||||
tro.tro_id = f"{PREFIX}{next_number:0{width}d}"
|
||||
used.add(tro.tro_id)
|
||||
next_number += 1
|
||||
result.new.append(tro.tro_id)
|
||||
|
||||
result.missing = [e for j, e in enumerate(entries) if j not in entry_taken]
|
||||
for d, i, j in pairs:
|
||||
if j not in entry_taken:
|
||||
result.merged_into.setdefault(entries[j].tro_id, tros[i].tro_id)
|
||||
return result
|
||||
|
||||
|
||||
def _anchor(tro, anchors: dict[str, tuple[float, float]]) -> tuple[float, float] | None:
|
||||
for sep in tro.separators:
|
||||
if sep in anchors:
|
||||
return anchors[sep]
|
||||
return None
|
||||
|
||||
|
||||
def _entry_lines(tros: list, anchors: dict[str, tuple[float, float]]) -> list[str]:
|
||||
lines = []
|
||||
for tro in tros:
|
||||
pos = _anchor(tro, anchors)
|
||||
if pos is None:
|
||||
continue
|
||||
seps = ", ".join(tro.separators)
|
||||
lines.append(f"{tro.tro_id} = {pos[0]:.1f}, {pos[1]:.1f}"
|
||||
f" ; {tro.type_name} | {tro.label} | Sep {seps}")
|
||||
return lines
|
||||
|
||||
|
||||
def render_ids(tros: list, anchors: dict[str, tuple[float, float]], source: str) -> str:
|
||||
"""Komplette ID-Datei (erster Lauf oder renumber)."""
|
||||
head = [
|
||||
"; ============================================================",
|
||||
f"; TRO-IDs zu {source} - erzeugt von lib/tro_flow.py "
|
||||
f"am {datetime.now():%Y-%m-%d}",
|
||||
";",
|
||||
"; Je TRO: ID = x, y (mm) seines Anker-Separators im CSV-Koordinatensystem.",
|
||||
"; Die IDs duerfen umbenannt werden (eindeutig, gueltiger SCL-Bezeichner);",
|
||||
"; tro_flow findet jeden TRO ueber die Lage wieder. Neue TROs werden",
|
||||
"; unten angehaengt, weggefallene bleiben stehen und werden gemeldet.",
|
||||
"; Alles neu nummerieren: tro_flow --renumber (ueberschreibt diese Datei).",
|
||||
"; Siehe doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs.",
|
||||
"; ============================================================",
|
||||
"",
|
||||
f"[{SECTION}]",
|
||||
]
|
||||
return "\n".join(head + _entry_lines(tros, anchors)) + "\n"
|
||||
|
||||
|
||||
def render_new_entries(tros: list, anchors: dict[str, tuple[float, float]],
|
||||
new_ids: list[str], source: str) -> str:
|
||||
"""Anhang fuer neu gefundene TROs an eine bestehende ID-Datei."""
|
||||
neu = [t for t in tros if t.tro_id in set(new_ids)]
|
||||
head = [f"; neu aus {source} am {datetime.now():%Y-%m-%d}"]
|
||||
return "\n".join(head + _entry_lines(neu, anchors)) + "\n"
|
||||
|
||||
|
||||
def other_sections(path: Path) -> str:
|
||||
"""Alle Abschnitte ausser [tro] als Text (bleiben bei renumber erhalten)."""
|
||||
if not path.is_file():
|
||||
return ""
|
||||
header = re.compile(r"^\s*\[(.+)\]\s*$")
|
||||
keep: list[str] = []
|
||||
current = None
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if m := header.match(line):
|
||||
current = m.group(1).strip()
|
||||
if current is not None and current != SECTION:
|
||||
keep.append(line)
|
||||
return "\n".join(keep).strip("\n")
|
||||
|
||||
|
||||
def insert_entries(path: Path, block: str) -> None:
|
||||
"""block am Ende des Abschnitts [tro] einfuegen, Rest der Datei unveraendert."""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
header = re.compile(r"^\s*\[(.+)\]\s*$")
|
||||
start = next(i for i, line in enumerate(lines)
|
||||
if (m := header.match(line)) and m.group(1).strip() == SECTION)
|
||||
end = next((i for i in range(start + 1, len(lines)) if header.match(lines[i])),
|
||||
len(lines))
|
||||
# vor Leerzeilen am Abschnittsende einfuegen
|
||||
while end > start + 1 and not lines[end - 1].strip():
|
||||
end -= 1
|
||||
lines[end:end] = block.rstrip("\n").splitlines()
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
+49
-237
@@ -10,10 +10,13 @@ eigener TRO erkannt wurde. Solche Erkenntnisse kommen aus der Begehung bzw.
|
||||
aus dem Review der erzeugten Diagramme.
|
||||
|
||||
Damit sie den naechsten Lauf ueberleben, stehen sie in einer INI-Datei
|
||||
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien. `tro_flow.py`
|
||||
und `tro_annotate.py` lesen sie und wenden sie nach der Ableitung an; die Doku
|
||||
weist jede angewandte Korrektur getrennt aus, damit abgeleitet und behauptet
|
||||
unterscheidbar bleiben.
|
||||
(`%SKEL_CFG%/tro_overrides.ini`) statt in den erzeugten Dateien.
|
||||
|
||||
Seit 2026-10-05 nur noch von `material_flow.py --connect` gelesen (Abschnitt
|
||||
`[<Anlage>.manual]` der connect.ini). `tro_flow.py` und `tro_annotate.py`
|
||||
wenden keine Korrekturen mehr an: die Abschnitte hingen an laufenden
|
||||
TRO-Nummern und veralteten mit jedem Export - Fehler der Ableitung werden
|
||||
seitdem im Code behoben (lib/tro_flow.py), nicht in der INI.
|
||||
|
||||
Aufbau der INI (Sektionsnamen mit dem CSV-Dateinamen als Praefix, damit
|
||||
mehrere Anlagen in einer Datei stehen koennen):
|
||||
@@ -23,17 +26,19 @@ mehrere Anlagen in einer Datei stehen koennen):
|
||||
reason = ... Begruendung (kommt in die Doku)
|
||||
confidence = bestaetigt Vertrauensangabe (Default: bestaetigt)
|
||||
|
||||
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO
|
||||
[<csv>.openpoint.<Name>] offener Punkt - noch KEIN TRO (model.TroCandidate)
|
||||
x = 529.95 Anlagenkoordinate in mm
|
||||
y = -3895.27
|
||||
z = 1468
|
||||
expect = 1Sep1Swi hier erwarteter TRO-Typ
|
||||
expect = 1Sep1Swi hier erwarteter TRO-Typ (-> type_name)
|
||||
belongs_to = TRO10 TRO, der das Bauteil heute enthaelt
|
||||
note = ... Erlaeuterung
|
||||
|
||||
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**. Sie werden nur
|
||||
in der Doku und in der annotierten Zeichnung markiert, damit sie in BricsCAD
|
||||
geprueft werden koennen, bevor daraus ein TRO wird.
|
||||
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
||||
|
||||
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
||||
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
||||
`model.Connection` bzw. `model.TroCandidate`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -43,6 +48,7 @@ import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from model import Connection, TroCandidate
|
||||
from tro_catalog import get_tro
|
||||
|
||||
OVERRIDE_FILE = "tro_overrides.ini"
|
||||
@@ -62,16 +68,17 @@ class TroOverride:
|
||||
wegfallen; die Separator-Nummer aus dem CSV-Export tut das nicht.
|
||||
"""
|
||||
|
||||
key: str
|
||||
by_separator: bool = False
|
||||
# genau eins von beiden ist gesetzt
|
||||
tro_id: str = ""
|
||||
separator: str = ""
|
||||
type_name: str = ""
|
||||
reason: str = ""
|
||||
confidence: str = ""
|
||||
|
||||
def matches(self, tro) -> bool:
|
||||
if self.by_separator:
|
||||
return self.key in tro.separators
|
||||
return self.key == tro.tro_id
|
||||
if self.separator:
|
||||
return self.separator in tro.separators
|
||||
return self.tro_id == tro.tro_id
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -89,21 +96,12 @@ class SplitSpec:
|
||||
type_name: str
|
||||
separators: list[str] = field(default_factory=list)
|
||||
scanners: list[str] = field(default_factory=list)
|
||||
host: str = ""
|
||||
hosts: list[str] = field(default_factory=list)
|
||||
label: str = ""
|
||||
reason: str = ""
|
||||
confidence: str = DEFAULT_CONFIDENCE
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionSpec:
|
||||
"""Eine vor Ort bestaetigte Verbindung zwischen zwei TROs."""
|
||||
|
||||
src: str
|
||||
dst: str
|
||||
kind: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergeSpec:
|
||||
"""Mehrere abgeleitete TROs zu einem Steuerobjekt zusammenfassen.
|
||||
@@ -118,28 +116,12 @@ class MergeSpec:
|
||||
name: str
|
||||
type_name: str
|
||||
separators: list[str] = field(default_factory=list)
|
||||
host: str = ""
|
||||
hosts: list[str] = field(default_factory=list)
|
||||
label: str = ""
|
||||
reason: str = ""
|
||||
confidence: str = DEFAULT_CONFIDENCE
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenPoint:
|
||||
"""Eine im Review gemeldete Stelle, die noch kein eigener TRO ist."""
|
||||
|
||||
name: str
|
||||
x: float
|
||||
y: float
|
||||
z: float | None = None
|
||||
expect: str = ""
|
||||
belongs_to: str = ""
|
||||
note: str = ""
|
||||
|
||||
def describe_position(self) -> str:
|
||||
return f"{self.x:.0f}, {self.y:.0f}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Overrides:
|
||||
"""Alle Korrekturen zu einer CSV-Datei."""
|
||||
@@ -149,15 +131,15 @@ class Overrides:
|
||||
splits: list[SplitSpec] = field(default_factory=list)
|
||||
merges: list[MergeSpec] = field(default_factory=list)
|
||||
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
||||
connections: list[ConnectionSpec] = field(default_factory=list)
|
||||
open_points: list[OpenPoint] = field(default_factory=list)
|
||||
connections: list[Connection] = field(default_factory=list)
|
||||
candidates: list[TroCandidate] = field(default_factory=list)
|
||||
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
||||
applied: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.tros or self.splits or self.merges or self.connections
|
||||
or self.open_points)
|
||||
or self.candidates)
|
||||
|
||||
|
||||
def override_path(cfg_dir: Path | None = None) -> Path:
|
||||
@@ -178,6 +160,10 @@ def _as_float(raw: str, where: str, warnings: list[str]) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _as_list(raw: str) -> list[str]:
|
||||
return [s.strip() for s in raw.split(",") if s.strip()]
|
||||
|
||||
|
||||
def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
"""Korrekturen zu `csv_name` laden (leeres Ergebnis, wenn es keine gibt)."""
|
||||
path = override_path(cfg_dir)
|
||||
@@ -204,7 +190,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
)
|
||||
continue
|
||||
result.connections.append(
|
||||
ConnectionSpec(src=src, dst=dst, kind=rest.strip())
|
||||
Connection(src=src, dst=dst, kind=rest.strip())
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -229,8 +215,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
key = name.upper()
|
||||
by_sep = key.startswith("SEP")
|
||||
result.tros[key] = TroOverride(
|
||||
key=key[3:] if by_sep else key,
|
||||
by_separator=by_sep,
|
||||
tro_id="" if by_sep else key,
|
||||
separator=key[3:] if by_sep else "",
|
||||
type_name=type_name,
|
||||
reason=values.get("reason", "").strip(),
|
||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
||||
@@ -243,7 +229,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
f"- neuer TRO nicht angelegt"
|
||||
)
|
||||
continue
|
||||
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
|
||||
seps = _as_list(values.get("separators", ""))
|
||||
if not seps:
|
||||
result.warnings.append(
|
||||
f"[{section}]: 'separators' fehlt - ohne Separator kein TRO"
|
||||
@@ -254,9 +240,8 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
name=name,
|
||||
type_name=type_name,
|
||||
separators=seps,
|
||||
scanners=[s.strip() for s in values.get("scanners", "").split(",")
|
||||
if s.strip()],
|
||||
host=values.get("host", "").strip(),
|
||||
scanners=_as_list(values.get("scanners", "")),
|
||||
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
||||
label=values.get("label", "").strip(),
|
||||
reason=values.get("reason", "").strip(),
|
||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
||||
@@ -270,7 +255,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
f"- Zusammenfassung nicht durchgefuehrt"
|
||||
)
|
||||
continue
|
||||
seps = [s.strip() for s in values.get("separators", "").split(",") if s.strip()]
|
||||
seps = _as_list(values.get("separators", ""))
|
||||
if len(seps) < 2:
|
||||
result.warnings.append(
|
||||
f"[{section}]: 'separators' braucht mindestens zwei Nummern"
|
||||
@@ -281,7 +266,7 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
name=name,
|
||||
type_name=type_name,
|
||||
separators=seps,
|
||||
host=values.get("host", "").strip(),
|
||||
hosts=_as_list(values.get("hosts", values.get("host", ""))),
|
||||
label=values.get("label", "").strip(),
|
||||
reason=values.get("reason", "").strip(),
|
||||
confidence=values.get("confidence", "").strip() or DEFAULT_CONFIDENCE,
|
||||
@@ -293,200 +278,27 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
if x is None or y is None:
|
||||
continue
|
||||
z_raw = values.get("z", "").strip()
|
||||
expect = values.get("expect", "").strip()
|
||||
if expect and get_tro(expect) is None:
|
||||
z = _as_float(z_raw, f"[{section}] z", result.warnings) if z_raw else None
|
||||
type_name = values.get("expect", "").strip()
|
||||
if type_name and get_tro(type_name) is None:
|
||||
result.warnings.append(
|
||||
f"[{section}]: erwarteter Typ '{expect}' steht nicht im "
|
||||
f"[{section}]: erwarteter Typ '{type_name}' steht nicht im "
|
||||
f"TRO-Katalog"
|
||||
)
|
||||
result.open_points.append(
|
||||
OpenPoint(
|
||||
result.candidates.append(
|
||||
TroCandidate(
|
||||
name=name,
|
||||
x=x,
|
||||
y=y,
|
||||
z=_as_float(z_raw, f"[{section}] z", result.warnings)
|
||||
if z_raw
|
||||
else None,
|
||||
expect=expect,
|
||||
type_name=type_name,
|
||||
position=(x, y, z if z is not None else 0.0),
|
||||
belongs_to=values.get("belongs_to", "").strip(),
|
||||
note=values.get("note", "").strip(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
result.warnings.append(
|
||||
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro' oder "
|
||||
f"'openpoint'"
|
||||
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
||||
f"'merge' oder 'openpoint'"
|
||||
)
|
||||
|
||||
result.open_points.sort(key=lambda p: p.name)
|
||||
result.candidates.sort(key=lambda p: p.name)
|
||||
return result
|
||||
|
||||
|
||||
def apply_splits(tros, overrides: Overrides, make_tro) -> list:
|
||||
"""Neue TROs aus bestehenden herausloesen (vor der Nummernvergabe).
|
||||
|
||||
`make_tro(spec, hosts, label)` erzeugt das projektspezifische Tro-Objekt -
|
||||
so bleibt dieses Modul frei von einem Import aus tro_flow (das umgekehrt
|
||||
schon von hier importiert).
|
||||
"""
|
||||
for spec in overrides.splits:
|
||||
quellen = [t for t in tros if set(spec.separators) & set(t.separators)]
|
||||
if not quellen:
|
||||
overrides.warnings.append(
|
||||
f"Split '{spec.name}': keiner der Separatoren "
|
||||
f"{', '.join(spec.separators)} kommt in diesem Lauf vor - "
|
||||
f"nicht angelegt"
|
||||
)
|
||||
continue
|
||||
|
||||
hosts: list[str] = []
|
||||
for quelle in quellen:
|
||||
quelle.separators = [s for s in quelle.separators
|
||||
if s not in spec.separators]
|
||||
quelle.scanners = [s for s in quelle.scanners if s not in spec.scanners]
|
||||
hosts.extend(quelle.hosts)
|
||||
if not quelle.separators and quelle.type_name not in ("Vario",):
|
||||
overrides.warnings.append(
|
||||
f"Split '{spec.name}': {quelle.label} hat danach keinen "
|
||||
f"Separator mehr - bitte pruefen"
|
||||
)
|
||||
|
||||
ziel_hosts = [spec.host] if spec.host else sorted(set(hosts))[:1]
|
||||
label = spec.label or (
|
||||
f"Separator {', '.join(spec.separators)} "
|
||||
f"(manuell aus {quellen[0].label} herausgeloest)"
|
||||
)
|
||||
tros.append(make_tro(spec, ziel_hosts, label))
|
||||
overrides.applied.append(
|
||||
f"neu {spec.name}: {spec.type_name} auf {', '.join(ziel_hosts)} "
|
||||
f"mit Separator {', '.join(spec.separators)}, herausgeloest aus "
|
||||
f"{', '.join(sorted({q.label for q in quellen}))} "
|
||||
f"({spec.reason or 'manuelle Ergaenzung'})"
|
||||
)
|
||||
return tros
|
||||
|
||||
|
||||
def apply_merges(tros, overrides: Overrides) -> list:
|
||||
"""Mehrere TROs zu einem zusammenfassen (nach den Splits, vor der Nummer).
|
||||
|
||||
Der erste betroffene TRO bleibt bestehen und uebernimmt Bauteile, Hosts und
|
||||
Typ; die uebrigen werden aus der Liste entfernt.
|
||||
"""
|
||||
for spec in overrides.merges:
|
||||
betroffen = [t for t in tros if set(spec.separators) & set(t.separators)]
|
||||
if len(betroffen) < 2:
|
||||
gefunden = ", ".join(t.tro_id or t.label for t in betroffen) or "keiner"
|
||||
overrides.warnings.append(
|
||||
f"Merge '{spec.name}': nur {len(betroffen)} der Separatoren "
|
||||
f"{', '.join(spec.separators)} gefunden ({gefunden}) - "
|
||||
f"nicht zusammengefasst"
|
||||
)
|
||||
continue
|
||||
|
||||
ziel = betroffen[0]
|
||||
quellen = betroffen[1:]
|
||||
for quelle in quellen:
|
||||
ziel.separators = sorted(set(ziel.separators) | set(quelle.separators))
|
||||
ziel.scanners = sorted(set(ziel.scanners) | set(quelle.scanners))
|
||||
ziel.hosts = sorted(set(ziel.hosts) | set(quelle.hosts))
|
||||
tros.remove(quelle)
|
||||
|
||||
if spec.host:
|
||||
# Ein Host bestimmt, wo der zusammengefasste TRO im Fluss haengt;
|
||||
# die uebrigen Bahnen bleiben als weitere Hosts erhalten, damit die
|
||||
# Verknuepfung beide Seiten des Uebergangs sieht.
|
||||
ziel.hosts = [spec.host] + [h for h in ziel.hosts if h != spec.host]
|
||||
ziel.type_name = spec.type_name
|
||||
ziel.confidence = spec.confidence
|
||||
ziel.label = spec.label or ziel.label
|
||||
grund = spec.reason or "manuell zusammengefasst"
|
||||
ziel.reason = f"{ziel.reason}; manuell zusammengefasst ({grund})"
|
||||
overrides.applied.append(
|
||||
f"merge {spec.name}: {len(betroffen)} TROs -> ein {spec.type_name} "
|
||||
f"mit Separatoren {', '.join(ziel.separators)} ({grund})"
|
||||
)
|
||||
return tros
|
||||
|
||||
|
||||
def apply_connections(tros, overrides: Overrides):
|
||||
"""Gesetzte Topologie an die Stelle der abgeleiteten Kanten setzen.
|
||||
|
||||
Ersetzt vollstaendig, nicht ergaenzend: wenn die Verbindungen vor Ort
|
||||
aufgenommen wurden, sind die abgeleiteten damit erledigt - sonst blieben
|
||||
genau die Kanten stehen, die korrigiert werden sollten. Was dabei wegfaellt
|
||||
bzw. hinzukommt, steht anschliessend in overrides.applied.
|
||||
|
||||
Rueckgabe: (edges, edge_kinds) wie von link_tros.
|
||||
"""
|
||||
bekannt = {t.tro_id for t in tros}
|
||||
edges: list[tuple[str, str]] = []
|
||||
kinds: dict[tuple[str, str], str] = {}
|
||||
for spec in overrides.connections:
|
||||
for seite, wert in (("von", spec.src), ("nach", spec.dst)):
|
||||
if wert not in bekannt:
|
||||
overrides.warnings.append(
|
||||
f"Verbindung {spec.src} -> {spec.dst}: {seite} '{wert}' ist "
|
||||
f"kein TRO dieses Laufs - Verbindung ignoriert"
|
||||
)
|
||||
break
|
||||
else:
|
||||
paar = (spec.src, spec.dst)
|
||||
if paar in kinds:
|
||||
continue
|
||||
edges.append(paar)
|
||||
kinds[paar] = spec.kind or "gesetzt"
|
||||
|
||||
for tro in tros:
|
||||
tro.successors = sorted(d for s, d in edges if s == tro.tro_id)
|
||||
tro.predecessors = sorted(s for s, d in edges if d == tro.tro_id)
|
||||
tro.colocated = []
|
||||
|
||||
ohne = [t.tro_id for t in tros if not t.successors and not t.predecessors]
|
||||
if ohne:
|
||||
overrides.warnings.append(
|
||||
"ohne Verbindung in der gesetzten Topologie: " + ", ".join(sorted(ohne))
|
||||
)
|
||||
overrides.applied.append(
|
||||
f"Topologie gesetzt: {len(edges)} Verbindungen aus der Override-Datei "
|
||||
f"ersetzen die abgeleiteten"
|
||||
)
|
||||
return sorted(edges), kinds
|
||||
|
||||
|
||||
def apply_overrides(tros, overrides: Overrides) -> None:
|
||||
"""Typkorrekturen auf die abgeleitete TRO-Liste anwenden.
|
||||
|
||||
Wird aufgerufen, nachdem die TRO-IDs vergeben sind (die Korrekturen sind
|
||||
ueber die ID adressiert) und bevor FB-Baustein und Bauteilliste aus dem
|
||||
Katalog gezogen werden - der Typwechsel zieht beides mit.
|
||||
"""
|
||||
if not overrides.tros:
|
||||
return
|
||||
for section_key, override in sorted(overrides.tros.items()):
|
||||
treffer = [t for t in tros if override.matches(t)]
|
||||
if not treffer:
|
||||
wie = "Separator" if override.by_separator else "TRO-ID"
|
||||
overrides.warnings.append(
|
||||
f"[{section_key}]: kein TRO mit {wie} '{override.key}' in diesem "
|
||||
f"Lauf - Korrektur nicht angewandt"
|
||||
+ ("" if override.by_separator else
|
||||
" (Nummerierung geaendert? Besser ueber SEP<Nummer> adressieren)")
|
||||
)
|
||||
continue
|
||||
if len(treffer) > 1:
|
||||
overrides.warnings.append(
|
||||
f"[{section_key}]: trifft {len(treffer)} TROs "
|
||||
f"({', '.join(t.tro_id for t in treffer)}) - auf alle angewandt"
|
||||
)
|
||||
for tro in treffer:
|
||||
if not override.type_name or override.type_name == tro.type_name:
|
||||
continue
|
||||
vorher = tro.type_name
|
||||
tro.type_name = override.type_name
|
||||
tro.confidence = override.confidence
|
||||
grund = override.reason or "manuelle Korrektur"
|
||||
tro.reason = (f"{tro.reason}; manuell auf {override.type_name} "
|
||||
f"gesetzt ({grund})")
|
||||
overrides.applied.append(
|
||||
f"{tro.tro_id}: {vorher} -> {override.type_name} ({grund})"
|
||||
)
|
||||
+5
-6
@@ -3,15 +3,14 @@
|
||||
|
||||
pydantic >= 2.0.0
|
||||
|
||||
# SCL-Templates (scl_skeleton --templates, lib/scl_templates/*.scl.j2)
|
||||
jinja2 >= 3.1
|
||||
|
||||
# DXF lesen und schreiben (tro_annotate). DWG wird nicht unterstuetzt.
|
||||
ezdxf >= 1.4.0
|
||||
|
||||
# STRtree fuer die geometrische TRO-Ableitung (tro_flow.py --geometry, siehe
|
||||
# lib/tro_geometry.py) - nur zur Laufzeit von --geometry benoetigt.
|
||||
shapely >= 2.1
|
||||
|
||||
# Nur fuer Vorschaubilder eines DXF (ezdxf.addons.drawing) - optional:
|
||||
# matplotlib >= 3.8
|
||||
|
||||
# Noch nicht benoetigt:
|
||||
# pytest >= 9.0.0
|
||||
# Tests (tests/, Aufruf: python -m pytest tests/)
|
||||
pytest >= 9.0.0
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""lib/ importierbar machen, auch ohne vorher bin/setenv aufzurufen."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
LIB = Path(__file__).resolve().parent.parent / "lib"
|
||||
if str(LIB) not in sys.path:
|
||||
sys.path.insert(0, str(LIB))
|
||||
+967
@@ -0,0 +1,967 @@
|
||||
// ============================================================
|
||||
// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py
|
||||
// Quelle : HundM05_tro.json
|
||||
// Erzeugt: <maskiert>
|
||||
//
|
||||
// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum
|
||||
// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem
|
||||
// Anlagenlayout und der vor Ort aufgenommenen Topologie.
|
||||
//
|
||||
// Jede Stelle, die aus der E-Planung kommen muss, ist mit
|
||||
// 'TODO(E-Planung)' markiert - danach suchen, dann ist nichts vergessen.
|
||||
// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),
|
||||
// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche
|
||||
// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.
|
||||
//
|
||||
// Reihenfolge der REGIONs = Materialfluss.
|
||||
// Anfang bei TRO20: Materialeingang ueber SYS1, SYS2.
|
||||
// ============================================================
|
||||
|
||||
FUNCTION_BLOCK "FB_Main"
|
||||
{ S7_Optimized_Access := 'TRUE' }
|
||||
VERSION : 0.1
|
||||
VAR
|
||||
TRO20 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0007
|
||||
TRO01 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0008
|
||||
TRO02 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0025
|
||||
TRO06 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0010
|
||||
TRO03 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0026
|
||||
TRO07 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0031
|
||||
TRO09 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0012
|
||||
TRO04 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0027
|
||||
TRO08 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0032
|
||||
TRO10 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0035
|
||||
TRO12 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0014
|
||||
TRO05 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0028
|
||||
TRO11 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0036
|
||||
TRO13 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0039
|
||||
TRO15 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0016
|
||||
TRO14 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0040
|
||||
TRO16 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0018
|
||||
TRO17 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0043
|
||||
TRO19 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0020
|
||||
TRO18 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0044
|
||||
END_VAR
|
||||
|
||||
BEGIN
|
||||
|
||||
REGION TRO20 (1Sep) Sep 0007
|
||||
// Anlagenkoordinate x/y: 2415 / 365 mm
|
||||
// Vorgaenger: SYS1, SYS2, TRO19
|
||||
// Nachfolger: TRO01
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0007 aus dem Layout
|
||||
#TRO20.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO20.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO20.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO20.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO20.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO20.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO20.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO20.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO20.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO20.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO20.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO20.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO20(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO20,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0007 von SYS1 (Station, kein TRO), SYS2 (Station, kein TRO), TRO19
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 vor TRO01
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO20,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO01 (1Sep1Swi) Sep 0008
|
||||
// Anlagenkoordinate x/y: 4076 / 985 mm
|
||||
// Vorgaenger: TRO20
|
||||
// Nachfolger: TRO02, TRO06
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0008 aus dem Layout
|
||||
#TRO01.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO01.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO01.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO01.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO01.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO01.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO01.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO01.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO01.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO01.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO01.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO02: SYS3, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
// ueber TRO06: SYS3, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
|
||||
#TRO01.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO01(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO01,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0008 von TRO20
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 vor TRO02
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO01,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO01.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO02 (Vario) Sep 0025
|
||||
// Anlagenkoordinate x/y: 5070 / 2530 mm
|
||||
// Vorgaenger: TRO01
|
||||
// Nachfolger: TRO03
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0025 aus dem Layout
|
||||
#TRO02.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO02.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO02.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO02.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO02.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO02.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO02.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO02.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO02.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO02.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO02.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO02.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO02.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO02.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO02.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO02.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO02.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO02.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO02(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO02,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0025 von TRO01
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 vor TRO03
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO02,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO06 (1Sep1Swi) Sep 0010
|
||||
// Anlagenkoordinate x/y: 8577 / 989 mm
|
||||
// Vorgaenger: TRO01, TRO05
|
||||
// Nachfolger: TRO07, TRO09
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0010 aus dem Layout
|
||||
#TRO06.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO06.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO06.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO06.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO06.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO06.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO06.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO06.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO06.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO06.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO06.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO06.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO07: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
// ueber TRO09: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
|
||||
#TRO06.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO06(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO06,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 von TRO01, TRO05
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 vor TRO07
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO06,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO06.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO03 (1Sep) Sep 0026
|
||||
// Anlagenkoordinate x/y: 7223 / 6180 mm
|
||||
// Vorgaenger: TRO02
|
||||
// Nachfolger: TRO04
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0026 aus dem Layout
|
||||
#TRO03.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO03.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO03.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO03.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO03.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO03.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO03.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO03.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO03.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO03(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO03,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0026 von TRO02
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 vor TRO04
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO03,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO07 (Vario) Sep 0031
|
||||
// Anlagenkoordinate x/y: 9571 / 5933 mm
|
||||
// Vorgaenger: TRO06
|
||||
// Nachfolger: TRO08
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0031 aus dem Layout
|
||||
#TRO07.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO07.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO07.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO07.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO07.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO07.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO07.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO07.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO07.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO07.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO07.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO07.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO07.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO07.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO07.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO07.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO07.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO07.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO07.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO07(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO07,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0031 von TRO06
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 vor TRO08
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO07,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO09 (1Sep1Swi) Sep 0012
|
||||
// Anlagenkoordinate x/y: 12650 / 987 mm
|
||||
// Vorgaenger: TRO06, TRO08
|
||||
// Nachfolger: TRO10, TRO12
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0012 aus dem Layout
|
||||
#TRO09.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO09.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO09.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO09.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO09.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO09.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO09.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO09.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO09.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO09.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO09.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO09.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO10: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
// ueber TRO12: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
|
||||
#TRO09.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO09(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO09,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO06, TRO08
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 vor TRO10
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO09,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO09.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO04 (Vario) Sep 0027
|
||||
// Anlagenkoordinate x/y: 7223 / 5680 mm
|
||||
// Vorgaenger: TRO03
|
||||
// Nachfolger: TRO05
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0027 aus dem Layout
|
||||
#TRO04.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO04.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO04.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO04.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO04.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO04.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO04.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO04.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO04.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO04.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO04.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO04.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO04.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO04.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO04.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO04.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO04.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO04.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO04.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO04(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO04,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0027 von TRO03
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 vor TRO05
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO04,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO08 (1Sep) Sep 0032
|
||||
// Anlagenkoordinate x/y: 10841 / 1724 mm
|
||||
// Vorgaenger: TRO07
|
||||
// Nachfolger: TRO09
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0032 aus dem Layout
|
||||
#TRO08.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO08.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO08.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO08.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO08.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO08.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO08.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO08.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO08.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO08(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO08,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0032 von TRO07
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO09
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO08,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO10 (Vario) Sep 0035
|
||||
// Anlagenkoordinate x/y: 13648 / 5933 mm
|
||||
// Vorgaenger: TRO09
|
||||
// Nachfolger: TRO11
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0035 aus dem Layout
|
||||
#TRO10.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO10.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO10.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO10.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO10.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO10.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO10.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO10.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO10.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO10.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO10.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO10.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO10.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO10.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO10(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO10,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0035 von TRO09
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 vor TRO11
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO10,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO12 (1Sep1Swi) Sep 0014
|
||||
// Anlagenkoordinate x/y: 16820 / 991 mm
|
||||
// Vorgaenger: TRO09, TRO11
|
||||
// Nachfolger: TRO13, TRO15
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0014 aus dem Layout
|
||||
#TRO12.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO12.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO12.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO12.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO12.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO12.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO12.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO12.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO12.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO12.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO12.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO12.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO13: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
// ueber TRO15: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO15, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
|
||||
#TRO12.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO12(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO12,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 von TRO09, TRO11
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 vor TRO13
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO12,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO12.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO05 (1Sep) Sep 0028
|
||||
// Anlagenkoordinate x/y: 6274 / 1756 mm
|
||||
// Vorgaenger: TRO04
|
||||
// Nachfolger: TRO06
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0028 aus dem Layout
|
||||
#TRO05.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO05.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO05.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO05.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO05.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO05.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO05.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO05.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO05.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO05(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO05,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0028 von TRO04
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0010 vor TRO06
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO05,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO11 (1Sep) Sep 0036
|
||||
// Anlagenkoordinate x/y: 14918 / 1725 mm
|
||||
// Vorgaenger: TRO10
|
||||
// Nachfolger: TRO12
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0036 aus dem Layout
|
||||
#TRO11.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO11.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO11.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO11.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO11.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO11.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO11.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO11.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO11.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO11(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO11,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von TRO10
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0014 vor TRO12
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO11,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO13 (Vario) Sep 0039
|
||||
// Anlagenkoordinate x/y: 17816 / 5934 mm
|
||||
// Vorgaenger: TRO12
|
||||
// Nachfolger: TRO14
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0039 aus dem Layout
|
||||
#TRO13.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO13.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO13.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO13.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO13.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO13.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO13.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO13.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO13.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO13.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO13.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO13.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO13.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO13.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO13.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO13.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO13.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO13.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO13.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO13(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO13,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0039 von TRO12
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 vor TRO14
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO13,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO15 (1Sep1Swi) Sep 0016
|
||||
// Anlagenkoordinate x/y: 20692 / 985 mm
|
||||
// Vorgaenger: TRO12, TRO14
|
||||
// Nachfolger: TRO16, TRO17
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0016 aus dem Layout
|
||||
#TRO15.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO15.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO15.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO15.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO15.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO15.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO15.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO15.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO15.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO15.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO15.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO15.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO16: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO16, TRO19, TRO20
|
||||
// ueber TRO17: SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO16, TRO17, TRO18, TRO19, TRO20
|
||||
|
||||
#TRO15.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO15(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO15,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 von TRO12, TRO14
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO17
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO15,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO15.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO14 (1Sep) Sep 0040
|
||||
// Anlagenkoordinate x/y: 19086 / 1725 mm
|
||||
// Vorgaenger: TRO13
|
||||
// Nachfolger: TRO15
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0040 aus dem Layout
|
||||
#TRO14.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO14.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO14.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO14.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO14.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO14.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO14.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO14.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO14.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO14(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO14,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO13
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0016 vor TRO15
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO14,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO16 (1Sep) Sep 0018
|
||||
// Anlagenkoordinate x/y: 23729 / 990 mm
|
||||
// Vorgaenger: TRO15, TRO18
|
||||
// Nachfolger: TRO19
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0018 aus dem Layout
|
||||
#TRO16.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO16.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO16.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO16.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO16.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO16.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO16.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO16.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO16.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO16(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO16,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 von TRO15, TRO18
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 vor TRO19
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO16,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO17 (Vario) Sep 0043
|
||||
// Anlagenkoordinate x/y: 21590 / 5933 mm
|
||||
// Vorgaenger: TRO15
|
||||
// Nachfolger: TRO18
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0043 aus dem Layout
|
||||
#TRO17.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO17.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO17.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO17.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO17.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO17.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO17.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO17.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO17.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO17.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO17.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO17.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO17.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO17.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO17.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO17.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO17.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO17.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO17.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO17(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO17,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO15
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO18
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO17,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO19 (1Sep) Sep 0020
|
||||
// Anlagenkoordinate x/y: 24570 / 1167 mm
|
||||
// Vorgaenger: TRO16
|
||||
// Nachfolger: SYS3, TRO20
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0020 aus dem Layout
|
||||
#TRO19.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO19.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO19.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO19.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO19.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO19.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO19.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO19.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO19.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO19.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO19.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO19.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO19(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO19,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0020 von TRO16
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO)
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0007 vor TRO20
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO19,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO18 (1Sep) Sep 0044
|
||||
// Anlagenkoordinate x/y: 22860 / 1724 mm
|
||||
// Vorgaenger: TRO17
|
||||
// Nachfolger: TRO16
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0044 aus dem Layout
|
||||
#TRO18.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO18.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO18.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO18.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO18.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO18.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO18.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO18.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO18(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO18,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO17
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0018 vor TRO16
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO18,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
END_FUNCTION_BLOCK
|
||||
+1108
File diff suppressed because it is too large.
Load diff
Vendored
+932
@@ -0,0 +1,932 @@
|
||||
{
|
||||
"schema": "sps_skel-tro-v3",
|
||||
"source": "HundM05.csv",
|
||||
"created": "2026-10-09 11:30",
|
||||
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
||||
"tros": [
|
||||
{
|
||||
"id": "TRO01",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0008"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO20"
|
||||
],
|
||||
"successors": [
|
||||
"TRO02",
|
||||
"TRO06"
|
||||
],
|
||||
"position": {
|
||||
"x": 4075.94,
|
||||
"y": 985.0,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "Separator 0008 an Ausschleuselement 0024 (Strecke 0001, Anfang)",
|
||||
"hosts": [
|
||||
"0001"
|
||||
],
|
||||
"scanners": [
|
||||
"0009"
|
||||
],
|
||||
"reason": "1014mm von Ausschleuselement 0024 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO02",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0025"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO01"
|
||||
],
|
||||
"successors": [
|
||||
"TRO03"
|
||||
],
|
||||
"position": {
|
||||
"x": 5070.1,
|
||||
"y": 2530.06,
|
||||
"z": 2098.34
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0001"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,horizontal,Auf,horizontal)",
|
||||
"colocated": [
|
||||
"TRO04",
|
||||
"TRO05"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO03",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0026"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO02"
|
||||
],
|
||||
"successors": [
|
||||
"TRO04"
|
||||
],
|
||||
"position": {
|
||||
"x": 7222.54,
|
||||
"y": 6180.03,
|
||||
"z": 2019.94
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0001"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (3. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [
|
||||
"TRO05"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO04",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0027"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO03"
|
||||
],
|
||||
"successors": [
|
||||
"TRO05"
|
||||
],
|
||||
"position": {
|
||||
"x": 7222.54,
|
||||
"y": 5679.62,
|
||||
"z": 1993.71
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0001"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,horizontal,Auf,horizontal)",
|
||||
"colocated": [
|
||||
"TRO02"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO05",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0028"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO04"
|
||||
],
|
||||
"successors": [
|
||||
"TRO06"
|
||||
],
|
||||
"position": {
|
||||
"x": 6273.83,
|
||||
"y": 1756.17,
|
||||
"z": 2305.73
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0001"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [
|
||||
"TRO02",
|
||||
"TRO03"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO06",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0010"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO01",
|
||||
"TRO05"
|
||||
],
|
||||
"successors": [
|
||||
"TRO07",
|
||||
"TRO09"
|
||||
],
|
||||
"position": {
|
||||
"x": 8577.09,
|
||||
"y": 988.6,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "Separator 0010 an Ausschleuselement 0030 (Strecke 0002, Anfang)",
|
||||
"hosts": [
|
||||
"0002"
|
||||
],
|
||||
"scanners": [
|
||||
"0011"
|
||||
],
|
||||
"reason": "1015mm von Ausschleuselement 0030 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO07",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0031"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO06"
|
||||
],
|
||||
"successors": [
|
||||
"TRO08"
|
||||
],
|
||||
"position": {
|
||||
"x": 9570.9,
|
||||
"y": 5932.96,
|
||||
"z": 1918.34
|
||||
},
|
||||
"label": "VF_2",
|
||||
"hosts": [
|
||||
"0002"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO08",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0032"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO07"
|
||||
],
|
||||
"successors": [
|
||||
"TRO09"
|
||||
],
|
||||
"position": {
|
||||
"x": 10840.84,
|
||||
"y": 1724.4,
|
||||
"z": 2305.73
|
||||
},
|
||||
"label": "VF_2",
|
||||
"hosts": [
|
||||
"0002"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_2 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO09",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0012"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO06",
|
||||
"TRO08"
|
||||
],
|
||||
"successors": [
|
||||
"TRO10",
|
||||
"TRO12"
|
||||
],
|
||||
"position": {
|
||||
"x": 12650.4,
|
||||
"y": 987.3,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "Separator 0012 an Ausschleuselement 0034 (Strecke 0003, Anfang)",
|
||||
"hosts": [
|
||||
"0003"
|
||||
],
|
||||
"scanners": [
|
||||
"0013"
|
||||
],
|
||||
"reason": "1016mm von Ausschleuselement 0034 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO10",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0035"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO09"
|
||||
],
|
||||
"successors": [
|
||||
"TRO11"
|
||||
],
|
||||
"position": {
|
||||
"x": 13648.0,
|
||||
"y": 5933.46,
|
||||
"z": 1918.34
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0003"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO11",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0036"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO10"
|
||||
],
|
||||
"successors": [
|
||||
"TRO12"
|
||||
],
|
||||
"position": {
|
||||
"x": 14917.94,
|
||||
"y": 1724.9,
|
||||
"z": 2305.73
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0003"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_3 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO12",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0014"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO09",
|
||||
"TRO11"
|
||||
],
|
||||
"successors": [
|
||||
"TRO13",
|
||||
"TRO15"
|
||||
],
|
||||
"position": {
|
||||
"x": 16819.62,
|
||||
"y": 990.75,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "Separator 0014 an Ausschleuselement 0038 (Strecke 0004, Anfang)",
|
||||
"hosts": [
|
||||
"0004"
|
||||
],
|
||||
"scanners": [
|
||||
"0015"
|
||||
],
|
||||
"reason": "1016mm von Ausschleuselement 0038 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO13",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0039"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO12"
|
||||
],
|
||||
"successors": [
|
||||
"TRO14"
|
||||
],
|
||||
"position": {
|
||||
"x": 17816.0,
|
||||
"y": 5933.96,
|
||||
"z": 1918.34
|
||||
},
|
||||
"label": "VF_4",
|
||||
"hosts": [
|
||||
"0004"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO14",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0040"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO13"
|
||||
],
|
||||
"successors": [
|
||||
"TRO15"
|
||||
],
|
||||
"position": {
|
||||
"x": 19085.94,
|
||||
"y": 1725.4,
|
||||
"z": 2305.73
|
||||
},
|
||||
"label": "VF_4",
|
||||
"hosts": [
|
||||
"0004"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_4 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO15",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0016"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO12",
|
||||
"TRO14"
|
||||
],
|
||||
"successors": [
|
||||
"TRO16",
|
||||
"TRO17"
|
||||
],
|
||||
"position": {
|
||||
"x": 20692.43,
|
||||
"y": 985.25,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "Separator 0016 an Ausschleuselement 0042 (Strecke 0005, Anfang)",
|
||||
"hosts": [
|
||||
"0005"
|
||||
],
|
||||
"scanners": [
|
||||
"0017"
|
||||
],
|
||||
"reason": "1013mm von Ausschleuselement 0042 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO16",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0018"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO15",
|
||||
"TRO18"
|
||||
],
|
||||
"successors": [
|
||||
"TRO19"
|
||||
],
|
||||
"position": {
|
||||
"x": 23729.42,
|
||||
"y": 989.7,
|
||||
"z": 2154.75
|
||||
},
|
||||
"label": "VF_5",
|
||||
"hosts": [
|
||||
"0005"
|
||||
],
|
||||
"scanners": [
|
||||
"0019"
|
||||
],
|
||||
"reason": "Separator 0018: Zuordnung zeigt auf VF_5, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
|
||||
"colocated": [
|
||||
"TRO17"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO17",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0043"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO15"
|
||||
],
|
||||
"successors": [
|
||||
"TRO18"
|
||||
],
|
||||
"position": {
|
||||
"x": 21590.2,
|
||||
"y": 5932.86,
|
||||
"z": 1918.34
|
||||
},
|
||||
"label": "VF_5",
|
||||
"hosts": [
|
||||
"0005"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: horizontal,horizontal,Auf)",
|
||||
"colocated": [
|
||||
"TRO16"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO18",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0044"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO17"
|
||||
],
|
||||
"successors": [
|
||||
"TRO16"
|
||||
],
|
||||
"position": {
|
||||
"x": 22860.14,
|
||||
"y": 1724.3,
|
||||
"z": 2305.73
|
||||
},
|
||||
"label": "VF_5",
|
||||
"hosts": [
|
||||
"0005"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_5 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO19",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0020"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO16"
|
||||
],
|
||||
"successors": [
|
||||
"SYS3",
|
||||
"TRO20"
|
||||
],
|
||||
"position": {
|
||||
"x": 24570.15,
|
||||
"y": 1167.2,
|
||||
"z": 2155.5
|
||||
},
|
||||
"label": "Separator 0020 auf Kreisel1 Bahn L",
|
||||
"hosts": [
|
||||
"0006-L"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO20",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0007"
|
||||
],
|
||||
"predecessors": [
|
||||
"SYS1",
|
||||
"SYS2",
|
||||
"TRO19"
|
||||
],
|
||||
"successors": [
|
||||
"TRO01"
|
||||
],
|
||||
"position": {
|
||||
"x": 2414.95,
|
||||
"y": 365.0,
|
||||
"z": 2155.5
|
||||
},
|
||||
"label": "Separator 0007 auf Kreisel1 Bahn R",
|
||||
"hosts": [
|
||||
"0006-R"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "SYS1",
|
||||
"type": "Materialeingang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [],
|
||||
"successors": [
|
||||
"TRO20"
|
||||
],
|
||||
"position": {
|
||||
"x": 5532.67,
|
||||
"y": 265.14,
|
||||
"z": 2109.31
|
||||
},
|
||||
"label": "BTMT Beladung :2",
|
||||
"hosts": [
|
||||
"0021"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"id": "SYS2",
|
||||
"type": "Materialeingang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [],
|
||||
"successors": [
|
||||
"TRO20"
|
||||
],
|
||||
"position": {
|
||||
"x": 4503.25,
|
||||
"y": 269.77,
|
||||
"z": 2109.31
|
||||
},
|
||||
"label": "BTMT Beladung :1",
|
||||
"hosts": [
|
||||
"0022"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"id": "SYS3",
|
||||
"type": "Materialausgang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [
|
||||
"TRO19"
|
||||
],
|
||||
"successors": [],
|
||||
"position": {
|
||||
"x": 25014.37,
|
||||
"y": 1545.51,
|
||||
"z": 2001.09
|
||||
},
|
||||
"label": "BTMT Entladung :1",
|
||||
"hosts": [
|
||||
"0023"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from": "SYS1",
|
||||
"to": "TRO20",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "SYS2",
|
||||
"to": "TRO20",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO01",
|
||||
"to": "TRO02",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO01",
|
||||
"to": "TRO06",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO02",
|
||||
"to": "TRO03",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO03",
|
||||
"to": "TRO04",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO04",
|
||||
"to": "TRO05",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO05",
|
||||
"to": "TRO06",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO06",
|
||||
"to": "TRO07",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO06",
|
||||
"to": "TRO09",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO07",
|
||||
"to": "TRO08",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO08",
|
||||
"to": "TRO09",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO09",
|
||||
"to": "TRO10",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO09",
|
||||
"to": "TRO12",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO10",
|
||||
"to": "TRO11",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO11",
|
||||
"to": "TRO12",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO12",
|
||||
"to": "TRO13",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO12",
|
||||
"to": "TRO15",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO13",
|
||||
"to": "TRO14",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO14",
|
||||
"to": "TRO15",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO15",
|
||||
"to": "TRO16",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO15",
|
||||
"to": "TRO17",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO16",
|
||||
"to": "TRO19",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO17",
|
||||
"to": "TRO18",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO18",
|
||||
"to": "TRO16",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO19",
|
||||
"to": "SYS3",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO19",
|
||||
"to": "TRO20",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO20",
|
||||
"to": "TRO01",
|
||||
"kind": "bahn"
|
||||
}
|
||||
],
|
||||
"candidates": [],
|
||||
"findings": [
|
||||
"Scanner 0019 'Scanner :1': montiert an 0006-L, liest aber an Separator 0018 auf 0005",
|
||||
"TRO16 (VF_5): Scanner 0019 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
|
||||
"VF_1 Separator 0026: alternierendes Muster (3. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_1 Separator 0028: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_2 Separator 0032: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_3 Separator 0036: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_4 Separator 0040: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_5 Separator 0018: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
|
||||
"VF_5 Separator 0044: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft"
|
||||
]
|
||||
}
|
||||
Vendored
+851
@@ -0,0 +1,851 @@
|
||||
// ============================================================
|
||||
// FB_Main - GERUEST, erzeugt von lib/scl_skeleton.py
|
||||
// Quelle : mubea_tro.json
|
||||
// Erzeugt: <maskiert>
|
||||
//
|
||||
// Das ist KEIN lauffaehiges Programm, sondern ein Geruest zum
|
||||
// Ausfuellen. Struktur, Reihenfolge und Bauteilzahl stammen aus dem
|
||||
// Anlagenlayout und der vor Ort aufgenommenen Topologie.
|
||||
//
|
||||
// Jede Stelle, die aus der E-Planung kommen muss, ist mit
|
||||
// 'TODO(E-Planung)' markiert - danach suchen, dann ist nichts vergessen.
|
||||
// Offen sind durchgaengig: Sensor-/Aktoradressen (cInBG/MB/MA),
|
||||
// Separator- und Weichennummern, MainTro-Nummern, die Staubereiche
|
||||
// (DB_JamArea...) und die Zeiten, soweit als 'pruefen' vermerkt.
|
||||
//
|
||||
// Reihenfolge der REGIONs = Materialfluss.
|
||||
// Anfang bei TRO03: Materialeingang ueber SYS1.
|
||||
// ============================================================
|
||||
|
||||
FUNCTION_BLOCK "FB_Main"
|
||||
{ S7_Optimized_Access := 'TRUE' }
|
||||
VERSION : 0.1
|
||||
VAR
|
||||
TRO03 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0042
|
||||
TRO05 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0044
|
||||
TRO18 : "FB_ILS_MTRO_PinStore_Auto"; // PinStore_Auto, Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
|
||||
TRO07 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0045
|
||||
TRO10 : "FB_ILS_MTRO_2Sep2Swi"; // 2Sep2Swi, Sep 0005, 0036
|
||||
TRO04 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0043
|
||||
TRO01 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0040
|
||||
TRO02 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0012
|
||||
TRO11 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0058
|
||||
TRO13 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0011
|
||||
TRO12 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0059
|
||||
TRO16 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0062
|
||||
TRO17 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0063
|
||||
TRO15 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0038
|
||||
TRO06 : "FB_ILS_MTRO_1Sep1Swi"; // 1Sep1Swi, Sep 0003
|
||||
TRO14 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0037
|
||||
TRO08 : "FB_ILS_MTRO_Vario"; // Vario, Sep 0054
|
||||
TRO09 : "FB_ILS_MTRO_1Sep"; // 1Sep, Sep 0055
|
||||
END_VAR
|
||||
|
||||
BEGIN
|
||||
|
||||
REGION TRO03 (1Sep) Sep 0042
|
||||
// Anlagenkoordinate x/y: 10490 / -525 mm
|
||||
// Vorgaenger: SYS1, TRO04
|
||||
// Nachfolger: TRO05
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0042 aus dem Layout
|
||||
#TRO03.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO03.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO03.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO03.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO03.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO03.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO03.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO03.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO03.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO03.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO03(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO03,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 von SYS1 (Station, kein TRO), TRO04
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 vor TRO05
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO03,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO05 (1Sep) Sep 0044
|
||||
// Anlagenkoordinate x/y: 10310 / -5013 mm
|
||||
// Vorgaenger: TRO03
|
||||
// Nachfolger: TRO18
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0044 aus dem Layout
|
||||
#TRO05.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO05.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO05.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO05.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO05.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO05.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO05.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO05.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO05.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO05.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO05(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO05,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0044 von TRO03
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO05,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO18 (PinStore_Auto) Sep 0066, 0069, 0072, 0075, 0078, 0081, 0084, 0087, 0090, 0093, 0096, 0099, 0102, 0105, 0108, 0111, 0114, 0117, 0120, 0123
|
||||
// Anlagenkoordinate x/y: 958 / 4700 mm
|
||||
// Vorgaenger: TRO05, TRO12
|
||||
// Nachfolger: TRO07, TRO10
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0066 aus dem Layout
|
||||
#TRO18.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO18.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO18.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO18.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO18.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO18.stInSeparator1.Settings.xPluggedScanner := TRUE;
|
||||
#TRO18.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO18.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO18.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO18.stInBarcodeReader.Settings.xActivateCheckASCI := TRUE;
|
||||
#TRO18.stInBarcodeReader.Settings.tTimeout := T#3000ms;
|
||||
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
|
||||
|
||||
// TODO(E-Planung): Linienspeicher mit 19 Linien - Parametrierung je Linie
|
||||
|
||||
#TRO18.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO18(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO18,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 von TRO05, TRO12 (Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator)
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO18,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO07 (1Sep) Sep 0045
|
||||
// Anlagenkoordinate x/y: 9510 / 14217 mm
|
||||
// Vorgaenger: TRO09, TRO18
|
||||
// Nachfolger: TRO04
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0045 aus dem Layout
|
||||
#TRO07.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO07.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO07.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO07.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO07.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO07.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO07.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO07.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO07.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO07.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO07(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO07,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 von TRO09, TRO18
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 vor TRO04
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO07,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO10 (2Sep2Swi) Sep 0005, 0036
|
||||
// Anlagenkoordinate x/y: 1012 / -4650 mm
|
||||
// Vorgaenger: TRO01, TRO06, TRO18
|
||||
// Nachfolger: TRO01, TRO02, TRO11
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0005 aus dem Layout
|
||||
#TRO10.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO10.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO10.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO10.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO10.stInSeparator1.Settings.xPluggedScanner := TRUE;
|
||||
#TRO10.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
// Separator 2 = Bauteil 0036 aus dem Layout
|
||||
#TRO10.stInSeparator2.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator2.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO10.stInSeparator2.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator2.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator2.Settings.tJam := "TimeForSepJam";
|
||||
#TRO10.stInSeparator2.Settings.xPluggedJam := TRUE;
|
||||
#TRO10.stInSeparator2.Settings.xPluggedPart := FALSE;
|
||||
#TRO10.stInSeparator2.Settings.xPluggedScanner := TRUE;
|
||||
#TRO10.stInSeparator2.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO10.stInSeparator2.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO10.stInSeparator2.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO10.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO10.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO10.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO10.stInSwitch1.Settings.nTo1Destinations := _; // TODO(E-Planung): dieser TRO hat 3 Abgaenge auf 2 Weichen - Aufteilung festlegen
|
||||
|
||||
#TRO10.stInSwitch2.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO10.stInSwitch2.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO10.stInSwitch2.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO10.stInSwitch2.Settings.nTo1Destinations := _; // TODO(E-Planung): dieser TRO hat 3 Abgaenge auf 2 Weichen - Aufteilung festlegen
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO01: TRO01
|
||||
// ueber TRO02: SYS2, SYS3, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
|
||||
// ueber TRO11: TRO03, TRO04, TRO05, TRO07, TRO11, TRO12, TRO18
|
||||
|
||||
#TRO10.stInBarcodeReader1.Settings.xActivateCheckASCI := TRUE;
|
||||
#TRO10.stInBarcodeReader1.Settings.tTimeout := T#3000ms;
|
||||
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
|
||||
|
||||
#TRO10.stInBarcodeReader2.Settings.xActivateCheckASCI := TRUE;
|
||||
#TRO10.stInBarcodeReader2.Settings.tTimeout := T#3000ms;
|
||||
// TODO(E-Planung): Cognex-Interface verdrahten (#arCognexInterface["cScanner____"])
|
||||
|
||||
#TRO10.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO10(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO10,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 von TRO01, TRO06, TRO18 (Aufteilung der Vorgaenger auf die Eingaenge offen)
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamEntr2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0036 von - (Aufteilung der Vorgaenger auf die Eingaenge offen)
|
||||
stInOutJamEntr2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 vor TRO01
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 vor TRO02
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0058 vor TRO11
|
||||
stInOutJamExit4 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutStopper2 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
xOutSw2ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO10,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO10.nInDirection1,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO10.nInDirection2,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO04 (1Sep) Sep 0043
|
||||
// Anlagenkoordinate x/y: 10310 / 160 mm
|
||||
// Vorgaenger: TRO07
|
||||
// Nachfolger: TRO03
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0043 aus dem Layout
|
||||
#TRO04.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO04.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO04.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO04.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO04.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO04.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO04.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO04.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO04.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO04.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO04(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO04,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0043 von TRO07
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0042 vor TRO03
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO04,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO01 (1Sep) Sep 0040
|
||||
// Anlagenkoordinate x/y: 5355 / -5878 mm
|
||||
// Vorgaenger: TRO10
|
||||
// Nachfolger: TRO10
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0040 aus dem Layout
|
||||
#TRO01.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO01.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO01.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO01.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO01.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO01.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO01.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO01.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO01.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO01(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO01,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0040 von TRO10
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO01,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO02 (1Sep) Sep 0012
|
||||
// Anlagenkoordinate x/y: -220 / -3472 mm
|
||||
// Vorgaenger: TRO10
|
||||
// Nachfolger: SYS2, SYS3, TRO13
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0012 aus dem Layout
|
||||
#TRO02.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO02.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO02.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO02.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO02.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO02.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO02.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO02.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO02.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO02(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO02,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0012 von TRO10
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS2 (Station, kein TRO)
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich vor SYS3 (Station, kein TRO)
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit4 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 vor TRO13
|
||||
stInOutJamExit4 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO02,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO11 (Vario) Sep 0058
|
||||
// Anlagenkoordinate x/y: 3300 / -4743 mm
|
||||
// Vorgaenger: TRO10
|
||||
// Nachfolger: TRO12
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0058 aus dem Layout
|
||||
#TRO11.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO11.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO11.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO11.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO11.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO11.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO11.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO11.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO11.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO11.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO11.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO11.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO11.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO11.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO11.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO11.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO11.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO11.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO11.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO11.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO11(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO11,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0058 von TRO10
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 vor TRO12
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO11,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO13 (1Sep1Swi) Sep 0011
|
||||
// Anlagenkoordinate x/y: -220 / -481 mm
|
||||
// Vorgaenger: TRO02
|
||||
// Nachfolger: TRO16, TRO17
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0011 aus dem Layout
|
||||
#TRO13.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO13.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO13.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO13.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO13.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO13.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO13.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO13.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO13.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO13.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO13.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO13.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO13.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO16: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO14, TRO15, TRO16, TRO17, TRO18
|
||||
// ueber TRO17: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO06, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO17, TRO18
|
||||
|
||||
#TRO13.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO13(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO13,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0011 von TRO02
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 vor TRO16
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO13,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO13.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO12 (1Sep) Sep 0059
|
||||
// Anlagenkoordinate x/y: 9412 / -4150 mm
|
||||
// Vorgaenger: TRO11
|
||||
// Nachfolger: TRO18
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0059 aus dem Layout
|
||||
#TRO12.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO12.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO12.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO12.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO12.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO12.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO12.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO12.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO12.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO12.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO12(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO12,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0059 von TRO11
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0066 vor TRO18
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO12,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO16 (Vario) Sep 0062
|
||||
// Anlagenkoordinate x/y: -899 / 1135 mm
|
||||
// Vorgaenger: TRO13
|
||||
// Nachfolger: TRO15
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0062 aus dem Layout
|
||||
#TRO16.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO16.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO16.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO16.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO16.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO16.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO16.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO16.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO16.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO16.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO16.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO16.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO16.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO16.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO16.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO16.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO16.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO16.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO16.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO16.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO16(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO16,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0062 von TRO13
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 vor TRO15
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO16,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO17 (1Sep) Sep 0063
|
||||
// Anlagenkoordinate x/y: -703 / 12296 mm
|
||||
// Vorgaenger: TRO13, TRO14
|
||||
// Nachfolger: TRO06
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0063 aus dem Layout
|
||||
#TRO17.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO17.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO17.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO17.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO17.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO17.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO17.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO17.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO17.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO17.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO17(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO17,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 von TRO13, TRO14
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 vor TRO06
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO17,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO15 (1Sep) Sep 0038
|
||||
// Anlagenkoordinate x/y: -1206 / 5330 mm
|
||||
// Vorgaenger: TRO16
|
||||
// Nachfolger: TRO14
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0038 aus dem Layout
|
||||
#TRO15.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO15.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO15.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO15.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO15.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO15.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO15.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO15.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO15.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO15.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO15(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO15,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0038 von TRO16
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 vor TRO14
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO15,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO06 (1Sep1Swi) Sep 0003
|
||||
// Anlagenkoordinate x/y: -220 / 13960 mm
|
||||
// Vorgaenger: TRO17
|
||||
// Nachfolger: TRO08, TRO10
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0003 aus dem Layout
|
||||
#TRO06.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO06.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO06.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO06.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO06.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO06.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO06.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO06.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO06.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO06.stInSwitch1.Settings.nSwitchNo := "cSwi_____"; // TODO(E-Planung)
|
||||
#TRO06.stInSwitch1.Settings.xPluggedExSen3 := FALSE;
|
||||
#TRO06.stInSwitch1.Settings.xPluggedExSen4 := FALSE;
|
||||
#TRO06.stInSwitch1.Settings.nTo1Destinations := 2; // Abgaenge laut Topologie
|
||||
|
||||
// Zielliste je Abgang (aus dem TRO-Graphen berechnet) -
|
||||
// Grundlage fuer die Zuordnung Ziel -> Richtung in FC_Direction:
|
||||
// ueber TRO08: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO08, TRO09, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
|
||||
// ueber TRO10: SYS2, SYS3, TRO01, TRO02, TRO03, TRO04, TRO05, TRO07, TRO10, TRO11, TRO12, TRO13, TRO14, TRO15, TRO16, TRO17, TRO18
|
||||
|
||||
#TRO06.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO06(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO06,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0003 von TRO17
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 vor TRO08
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit3 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0005 vor TRO10
|
||||
stInOutJamExit3 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutSw1ExTo3 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO06,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
|
||||
REGION Direction
|
||||
"FC_Direction"(nInSwitchNo := "cSwi_____", // TODO(E-Planung)
|
||||
nOutDirection => #TRO06.nInDirection,
|
||||
InOutCarrier := "DB_JamArea____".stJam____.arCarrier[1]);
|
||||
END_REGION
|
||||
END_REGION
|
||||
|
||||
REGION TRO14 (1Sep) Sep 0037
|
||||
// Anlagenkoordinate x/y: -1026 / 10723 mm
|
||||
// Vorgaenger: TRO15
|
||||
// Nachfolger: TRO17
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0037 aus dem Layout
|
||||
#TRO14.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO14.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO14.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO14.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO14.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO14.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO14.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO14.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO14.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO14.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO14(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO14,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0037 von TRO15
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0063 vor TRO17
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO14,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO08 (Vario) Sep 0054
|
||||
// Anlagenkoordinate x/y: 3300 / 13510 mm
|
||||
// Vorgaenger: TRO06
|
||||
// Nachfolger: TRO09
|
||||
// Einstufung: hoch
|
||||
|
||||
// Separator 1 = Bauteil 0054 aus dem Layout
|
||||
#TRO08.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO08.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO08.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO08.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO08.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO08.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO08.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO08.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO08.stInVario.Settings.xContinousEmptying := FALSE;
|
||||
#TRO08.stInVario.Settings.tWaitForJog := T#300ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInVario.Settings.tConvEmpty := T#45000ms; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInVario.Settings.nCountCarrier := 1; // TODO(E-Planung): pruefen
|
||||
#TRO08.stInVario.Settings.tWaitForStop := t#3s;
|
||||
#TRO08.stInVario.Settings.tCarrierInPosDelay := t#1200ms;
|
||||
#TRO08.stInVario.Settings.tCarrierInPosEdgeDelay := t#100ms;
|
||||
#TRO08.stInVario.Settings.xJamSenDelayActive := TRUE;
|
||||
#TRO08.stInVario.Settings.tJamSenDelay := T#2s;
|
||||
#TRO08.stInVario.stSenCarrInPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO08.stInVario.stSenFinger := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO08.stInVario.stSenLastPos := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO08.stInVario.stSenJam := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
|
||||
#TRO08.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO08(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO08,
|
||||
xInMotorProtection := "FC____", // TODO(E-Planung)
|
||||
xInAutomatic := #t_automatik,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0054 von TRO06
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 vor TRO09
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
xOutVarioMotor => "MA____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO08,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
REGION TRO09 (1Sep) Sep 0055
|
||||
// Anlagenkoordinate x/y: 8953 / 13510 mm
|
||||
// Vorgaenger: TRO08
|
||||
// Nachfolger: TRO07
|
||||
// Einstufung: mittel
|
||||
|
||||
// Separator 1 = Bauteil 0055 aus dem Layout
|
||||
#TRO09.stInSeparator1.Settings.nSeparatorNo := "cSep_____"; // TODO(E-Planung)
|
||||
#TRO09.stInSeparator1.Settings.tDelayToNextItem := "TimeForSepWaiting_Short";
|
||||
#TRO09.stInSeparator1.Settings.tTrailingTime := t#600ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tHandlingTime := t#200ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tJam := "TimeForSepJam";
|
||||
#TRO09.stInSeparator1.Settings.xPluggedJam := TRUE;
|
||||
#TRO09.stInSeparator1.Settings.xPluggedPart := FALSE;
|
||||
#TRO09.stInSeparator1.Settings.xPluggedScanner := FALSE;
|
||||
#TRO09.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG____"]; // TODO(E-Planung)
|
||||
#TRO09.stInSeparator1.Settings.tSenFree := T#1500ms; // TODO(E-Planung): pruefen
|
||||
#TRO09.stInSeparator1.Settings.tSenWait := T#4000ms; // TODO(E-Planung): pruefen
|
||||
|
||||
#TRO09.stInPriorityManager.xReleaseOk := TRUE;
|
||||
|
||||
#TRO09(nInMainTroNo := "cMainTro____", // TODO(E-Planung)
|
||||
xInSftyOk := "DB_InterfaceSafety".stFromSafety.EStopMain.Q, // TODO(E-Planung)
|
||||
xInAllRdyToStart := TRUE,
|
||||
xInRelease := "DB_TRO_TEST".TRO09,
|
||||
stInOutMachineState := #stInOutControlUnitCabinet,
|
||||
stInOutCognexInterface := #stCognexInterfaceDummy,
|
||||
arInOutJamEntr1 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0055 von TRO08
|
||||
stInOutJamEntr1 := "DB_JamArea____".stJam____.stData,
|
||||
arInOutJamExit2 := "DB_JamArea____".stJam____.arCarrier, // TODO(E-Planung) - Staubereich J0045 vor TRO07
|
||||
stInOutJamExit2 := "DB_JamArea____".stJam____.stData,
|
||||
xOutStopper1 => "MB____", // TODO(E-Planung)
|
||||
stInOutHMI := "DB_Interface_HMI".stTRO.TRO09,
|
||||
nOutStateLast => "DB_GLOB_TroState".arTroState["cMainTro____"]); // TODO(E-Planung)
|
||||
END_REGION
|
||||
|
||||
END_FUNCTION_BLOCK
|
||||
+1251
File diff suppressed because it is too large.
Load diff
Vendored
+907
@@ -0,0 +1,907 @@
|
||||
{
|
||||
"schema": "sps_skel-tro-v3",
|
||||
"source": "mubea.csv",
|
||||
"created": "2026-10-09 11:38",
|
||||
"note": "Aus dem CSV-Export abgeleitet (lib/tro_flow.py) - die Zeichnung ist nur Ansicht, keine Datenquelle.",
|
||||
"tros": [
|
||||
{
|
||||
"id": "TRO01",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0040"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO10"
|
||||
],
|
||||
"successors": [
|
||||
"TRO10"
|
||||
],
|
||||
"position": {
|
||||
"x": 5355.05,
|
||||
"y": -5878.0,
|
||||
"z": 1555.5
|
||||
},
|
||||
"label": "Separator 0040 auf Kreisel1 Bahn R",
|
||||
"hosts": [
|
||||
"0001-R"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO02",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0012"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO10"
|
||||
],
|
||||
"successors": [
|
||||
"SYS2",
|
||||
"SYS3",
|
||||
"TRO13"
|
||||
],
|
||||
"position": {
|
||||
"x": -220.0,
|
||||
"y": -3472.47,
|
||||
"z": 1554.75
|
||||
},
|
||||
"label": "Separator 0012 auf Kreisel2 Bahn L",
|
||||
"hosts": [
|
||||
"0002-L"
|
||||
],
|
||||
"scanners": [
|
||||
"0007"
|
||||
],
|
||||
"reason": "Separator mit Scanner auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO03",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0042"
|
||||
],
|
||||
"predecessors": [
|
||||
"SYS1",
|
||||
"TRO04"
|
||||
],
|
||||
"successors": [
|
||||
"TRO05"
|
||||
],
|
||||
"position": {
|
||||
"x": 10490.0,
|
||||
"y": -525.02,
|
||||
"z": 2028.75
|
||||
},
|
||||
"label": "Separator 0042 auf Kreisel3 Bahn R",
|
||||
"hosts": [
|
||||
"0010-R"
|
||||
],
|
||||
"scanners": [
|
||||
"0008"
|
||||
],
|
||||
"reason": "Separator mit Scanner auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO04",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0043"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO07"
|
||||
],
|
||||
"successors": [
|
||||
"TRO03"
|
||||
],
|
||||
"position": {
|
||||
"x": 10310.0,
|
||||
"y": 159.95,
|
||||
"z": 2029.5
|
||||
},
|
||||
"label": "Separator 0043 auf Kreisel3 Bahn R",
|
||||
"hosts": [
|
||||
"0010-R"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [
|
||||
"TRO05"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO05",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0044"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO03"
|
||||
],
|
||||
"successors": [
|
||||
"TRO18"
|
||||
],
|
||||
"position": {
|
||||
"x": 10310.0,
|
||||
"y": -5013.05,
|
||||
"z": 2029.5
|
||||
},
|
||||
"label": "Separator 0044 auf Kreisel3 Bahn R",
|
||||
"hosts": [
|
||||
"0010-R"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "einzelner Separator auf angetriebener Kreisel-Bahn",
|
||||
"colocated": [
|
||||
"TRO04"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO06",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0003"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO17"
|
||||
],
|
||||
"successors": [
|
||||
"TRO08",
|
||||
"TRO10"
|
||||
],
|
||||
"position": {
|
||||
"x": -220.0,
|
||||
"y": 13960.02,
|
||||
"z": 1554.75
|
||||
},
|
||||
"label": "Separator 0003 an Ausschleuselement 0053 (Strecke 0013, Anfang)",
|
||||
"hosts": [
|
||||
"0013"
|
||||
],
|
||||
"scanners": [
|
||||
"0004"
|
||||
],
|
||||
"reason": "899mm von Ausschleuselement 0053 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO07",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0045"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO09",
|
||||
"TRO18"
|
||||
],
|
||||
"successors": [
|
||||
"TRO04"
|
||||
],
|
||||
"position": {
|
||||
"x": 9510.0,
|
||||
"y": 14217.05,
|
||||
"z": 2029.5
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0013"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Separator 0045: Zuordnung zeigt auf VF_1, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
|
||||
"colocated": [
|
||||
"TRO08"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO08",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0054"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO06"
|
||||
],
|
||||
"successors": [
|
||||
"TRO09"
|
||||
],
|
||||
"position": {
|
||||
"x": 3299.55,
|
||||
"y": 13509.62,
|
||||
"z": 1416.12
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0013"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
|
||||
"colocated": [
|
||||
"TRO07"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO09",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0055"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO08"
|
||||
],
|
||||
"successors": [
|
||||
"TRO07"
|
||||
],
|
||||
"position": {
|
||||
"x": 8953.1,
|
||||
"y": 13509.62,
|
||||
"z": 2179.73
|
||||
},
|
||||
"label": "VF_1",
|
||||
"hosts": [
|
||||
"0013"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_1 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO10",
|
||||
"type": "2Sep2Swi",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0005",
|
||||
"0036"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO01",
|
||||
"TRO06",
|
||||
"TRO18"
|
||||
],
|
||||
"successors": [
|
||||
"TRO01",
|
||||
"TRO02",
|
||||
"TRO11"
|
||||
],
|
||||
"position": {
|
||||
"x": 1012.49,
|
||||
"y": -4649.76,
|
||||
"z": 1554.75
|
||||
},
|
||||
"label": "Uebergabestelle Kreisel2: Ausschleuselement 0057 + ILS Weiche 0125",
|
||||
"hosts": [
|
||||
"0014"
|
||||
],
|
||||
"scanners": [
|
||||
"0006",
|
||||
"0041"
|
||||
],
|
||||
"reason": "Ausschleuselement 0057 (Separator 0005) und ILS Weiche 0125 (Separator 0036) liegen in der TrackIds-Kette von Kreisel2 direkt hintereinander - eine Uebergabestelle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 4,
|
||||
"fbType": "FB_ILS_MTRO_2Sep2Swi",
|
||||
"items": {
|
||||
"Separator": 2,
|
||||
"Switch": 2,
|
||||
"Scanner": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO11",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0058"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO10"
|
||||
],
|
||||
"successors": [
|
||||
"TRO12"
|
||||
],
|
||||
"position": {
|
||||
"x": 3299.55,
|
||||
"y": -4743.38,
|
||||
"z": 1416.12
|
||||
},
|
||||
"label": "VF_2",
|
||||
"hosts": [
|
||||
"0014"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO12",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0059"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO11"
|
||||
],
|
||||
"successors": [
|
||||
"TRO18"
|
||||
],
|
||||
"position": {
|
||||
"x": 9411.55,
|
||||
"y": -4150.19,
|
||||
"z": 2103.86
|
||||
},
|
||||
"label": "VF_2",
|
||||
"hosts": [
|
||||
"0014"
|
||||
],
|
||||
"scanners": [
|
||||
"0046"
|
||||
],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_2 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO13",
|
||||
"type": "1Sep1Swi",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0011"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO02"
|
||||
],
|
||||
"successors": [
|
||||
"TRO16",
|
||||
"TRO17"
|
||||
],
|
||||
"position": {
|
||||
"x": -220.0,
|
||||
"y": -480.98,
|
||||
"z": 1554.75
|
||||
},
|
||||
"label": "Separator 0011 an Ausschleuselement 0061 (Strecke 0015, Anfang)",
|
||||
"hosts": [
|
||||
"0015"
|
||||
],
|
||||
"scanners": [
|
||||
"0009"
|
||||
],
|
||||
"reason": "832mm von Ausschleuselement 0061 entfernt - kein eigenes Weichen-Bauteil im Layout, das Ausschleuselement uebernimmt diese Rolle",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep1Swi",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"Switch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO14",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0037"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO15"
|
||||
],
|
||||
"successors": [
|
||||
"TRO17"
|
||||
],
|
||||
"position": {
|
||||
"x": -1026.0,
|
||||
"y": 10723.05,
|
||||
"z": 1634.5
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0015"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Separator 0037: Zuordnung zeigt auf VF_3, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
|
||||
"colocated": [
|
||||
"TRO16"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO15",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0038"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO16"
|
||||
],
|
||||
"successors": [
|
||||
"TRO14"
|
||||
],
|
||||
"position": {
|
||||
"x": -1206.0,
|
||||
"y": 5330.02,
|
||||
"z": 1477.75
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0015"
|
||||
],
|
||||
"scanners": [
|
||||
"0039"
|
||||
],
|
||||
"reason": "Separator 0038: Zuordnung zeigt auf VF_3, ist aber nicht Teil von dessen TrackIds-Kette - kein eigenes VF treibt ihn an",
|
||||
"colocated": [
|
||||
"TRO17"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 2,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO16",
|
||||
"type": "Vario",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0062"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO13"
|
||||
],
|
||||
"successors": [
|
||||
"TRO15"
|
||||
],
|
||||
"position": {
|
||||
"x": -898.87,
|
||||
"y": 1134.6,
|
||||
"z": 1534.33
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0015"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "angetriebenes Streckensegment (Antriebfahrtrichtung: Auf)",
|
||||
"colocated": [
|
||||
"TRO14",
|
||||
"TRO17"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_Vario",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"VarioDrive": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO17",
|
||||
"type": "1Sep",
|
||||
"confidence": "mittel",
|
||||
"separators": [
|
||||
"0063"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO13",
|
||||
"TRO14"
|
||||
],
|
||||
"successors": [
|
||||
"TRO06"
|
||||
],
|
||||
"position": {
|
||||
"x": -702.98,
|
||||
"y": 12296.45,
|
||||
"z": 1751.35
|
||||
},
|
||||
"label": "VF_3",
|
||||
"hosts": [
|
||||
"0015"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "alternierendes VF/GF-Muster vom Wiedereinschleusen von VF_3 rueckwaerts (1. Separator von hinten) - unbetriebener Abschnitt ohne eigenes VarioDrive (Review 2026-09-23/25)",
|
||||
"colocated": [
|
||||
"TRO15",
|
||||
"TRO16"
|
||||
],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 1,
|
||||
"fbType": "FB_ILS_MTRO_1Sep",
|
||||
"items": {
|
||||
"Separator": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "TRO18",
|
||||
"type": "PinStore_Auto",
|
||||
"confidence": "hoch",
|
||||
"separators": [
|
||||
"0066",
|
||||
"0069",
|
||||
"0072",
|
||||
"0075",
|
||||
"0078",
|
||||
"0081",
|
||||
"0084",
|
||||
"0087",
|
||||
"0090",
|
||||
"0093",
|
||||
"0096",
|
||||
"0099",
|
||||
"0102",
|
||||
"0105",
|
||||
"0108",
|
||||
"0111",
|
||||
"0114",
|
||||
"0117",
|
||||
"0120",
|
||||
"0123"
|
||||
],
|
||||
"predecessors": [
|
||||
"TRO05",
|
||||
"TRO12"
|
||||
],
|
||||
"successors": [
|
||||
"TRO07",
|
||||
"TRO10"
|
||||
],
|
||||
"position": {
|
||||
"x": 958.0,
|
||||
"y": 4700.0,
|
||||
"z": 1697.6
|
||||
},
|
||||
"label": "Speicher (20 Schwerkraftlinien)",
|
||||
"hosts": [
|
||||
"0016",
|
||||
"0017",
|
||||
"0018",
|
||||
"0019",
|
||||
"0020",
|
||||
"0021",
|
||||
"0022",
|
||||
"0023",
|
||||
"0024",
|
||||
"0025",
|
||||
"0026",
|
||||
"0027",
|
||||
"0028",
|
||||
"0029",
|
||||
"0030",
|
||||
"0031",
|
||||
"0032",
|
||||
"0033",
|
||||
"0034",
|
||||
"0035"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "20 Gefaellestrecken mit je eigenem Separator, gemeinsamer Zu-/Ablauf - Muster eines Linienspeichers",
|
||||
"colocated": [],
|
||||
"positionFrom": "Bauteile",
|
||||
"positionPoints": 20,
|
||||
"fbType": "FB_ILS_MTRO_PinStore_Auto",
|
||||
"items": {
|
||||
"Separator": 1,
|
||||
"StorageLine": 19,
|
||||
"Scanner": 1,
|
||||
"Encoder": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "SYS1",
|
||||
"type": "Materialeingang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [],
|
||||
"successors": [
|
||||
"TRO03"
|
||||
],
|
||||
"position": {
|
||||
"x": 10410.95,
|
||||
"y": 3.52,
|
||||
"z": 1983.31
|
||||
},
|
||||
"label": "BTMT Beladung :1",
|
||||
"hosts": [
|
||||
"0047"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"id": "SYS2",
|
||||
"type": "Materialausgang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [
|
||||
"TRO02"
|
||||
],
|
||||
"successors": [],
|
||||
"position": {
|
||||
"x": -778.79,
|
||||
"y": -1226.1,
|
||||
"z": 1401.09
|
||||
},
|
||||
"label": "BTMT Entladung :2",
|
||||
"hosts": [
|
||||
"0048"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"id": "SYS3",
|
||||
"type": "Materialausgang",
|
||||
"confidence": "hoch",
|
||||
"separators": [],
|
||||
"predecessors": [
|
||||
"TRO02"
|
||||
],
|
||||
"successors": [],
|
||||
"position": {
|
||||
"x": -775.22,
|
||||
"y": -2466.46,
|
||||
"z": 1401.09
|
||||
},
|
||||
"label": "BTMT Entladung :1",
|
||||
"hosts": [
|
||||
"0049"
|
||||
],
|
||||
"scanners": [],
|
||||
"reason": "Systemgrenze (Be-/Entladestation) - kein TRO, nur Materialein-/ausgang",
|
||||
"colocated": [],
|
||||
"positionFrom": "Host-Objekte",
|
||||
"positionPoints": 1,
|
||||
"fbType": "",
|
||||
"items": {}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from": "SYS1",
|
||||
"to": "TRO03",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO01",
|
||||
"to": "TRO10",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO02",
|
||||
"to": "SYS2",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO02",
|
||||
"to": "SYS3",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO02",
|
||||
"to": "TRO13",
|
||||
"kind": "transfer"
|
||||
},
|
||||
{
|
||||
"from": "TRO03",
|
||||
"to": "TRO05",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO04",
|
||||
"to": "TRO03",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO05",
|
||||
"to": "TRO18",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO06",
|
||||
"to": "TRO08",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO06",
|
||||
"to": "TRO10",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO07",
|
||||
"to": "TRO04",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO08",
|
||||
"to": "TRO09",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO09",
|
||||
"to": "TRO07",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO10",
|
||||
"to": "TRO01",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO10",
|
||||
"to": "TRO02",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO10",
|
||||
"to": "TRO11",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO11",
|
||||
"to": "TRO12",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO12",
|
||||
"to": "TRO18",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO13",
|
||||
"to": "TRO16",
|
||||
"kind": "weiche"
|
||||
},
|
||||
{
|
||||
"from": "TRO13",
|
||||
"to": "TRO17",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO14",
|
||||
"to": "TRO17",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO15",
|
||||
"to": "TRO14",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO16",
|
||||
"to": "TRO15",
|
||||
"kind": "bahn"
|
||||
},
|
||||
{
|
||||
"from": "TRO17",
|
||||
"to": "TRO06",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO18",
|
||||
"to": "TRO07",
|
||||
"kind": "normal"
|
||||
},
|
||||
{
|
||||
"from": "TRO18",
|
||||
"to": "TRO10",
|
||||
"kind": "normal"
|
||||
}
|
||||
],
|
||||
"candidates": [],
|
||||
"findings": [
|
||||
"Kreisel2: Ausschleuselement 0057 und ILS Weiche 0125 benachbart - als 2Sep2Swi zusammengefasst (FB_ILS_MTRO_2Sep2Swi fehlt noch in der ILSLib)",
|
||||
"Scanner 0008 'Scanner :5': montiert an 0010, liest aber an Separator 0042 auf 0010-R",
|
||||
"Scanner 0046 'Scanner :1': montiert an 0035, liest aber an Separator 0059 auf 0014",
|
||||
"TRO02 (Separator 0012 auf Kreisel2 Bahn L): Scanner 0007 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
|
||||
"TRO03 (Separator 0042 auf Kreisel3 Bahn R): Scanner 0008 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
|
||||
"TRO12 (VF_2): Scanner 0046 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
|
||||
"TRO15 (VF_3): Scanner 0039 am Separator - als 1Sep eingestuft. Pruefen, ob 1Sep_SSCC (SSCC-Scanner, Endmessung, WCS-Telegramm) erforderlich ist; das ist aus dem mechanischen Layout nicht erkennbar.",
|
||||
"VF_1 Separator 0045: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
|
||||
"VF_1 Separator 0055: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_2 Separator 0059: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft",
|
||||
"VF_3 Separator 0037: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
|
||||
"VF_3 Separator 0038: Zuordnung ohne TrackIds-Mitgliedschaft - als 1Sep statt Vario eingestuft",
|
||||
"VF_3 Separator 0063: alternierendes Muster (1. von hinten) - als 1Sep statt Vario eingestuft"
|
||||
]
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
|
||||
1;"ILS 2.0 Kreisel";"0001";"Kreisel1";"A/7-B/8";1;"-2011.74, 994.84, 2000.00";"3100.00, 800.00, 1.00";"yab0Dy4YHoSAAAAAAAAAAAAA";"";"";"";"";"0009-L";"0023, 0021";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel1", "Geruest fuer Einzelmodul": true}
|
||||
2;"ILS 2.0 Kreisel";"0002";"Kreisel2";"E/3-E/6";1;"4938.26, -4953.25, 2000.00";"800.00, 5300.00, 1.00";"S1oUi/vOHoSAAAAAAAAAWoJ4";"";"";"";"";"0007-L, 0007-R, 0003-L";"0015, 0029, 0014, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.5", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel2", "Geruest fuer Einzelmodul": true}
|
||||
3;"ILS 2.0 Kreisel";"0003";"Kreisel3";"E/1-E/4";1;"5738.26, -9850.80, 2000.00";"800.00, 6095.71, 1.00";"V48UgAABHoSAAAAAAAAAWoJ4";"";"";"";"";"0004-L, 0002-R";"0029, 0028";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "5.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel3", "Geruest fuer Einzelmodul": true}
|
||||
4;"ILS 2.0 Kreisel";"0004";"Kreisel4";"E/3-F/7";1;"6538.26, -3258.73, 2000.00";"800.00, 8689.42, 1.00";"Y8QUi/sQHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-R, 0003-R";"0016, 0028, 0017, 0027";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "7.89", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel4", "Geruest fuer Einzelmodul": true}
|
||||
5;"ILS 2.0 Kreisel";"0005";"Kreisel5";"E/7-K/8";1;"11430.05, 1485.99, 2000.00";"10583.58, 800.00, 1.00";"XamUFqymHoSAAAAAAAAAAAAA";"";"";"";"";"0006-L, 0006-R, 0004-L, 0004-R";"0018, 0026, 0027, 0020";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "9.78", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 0, "Name": "Kreisel5", "Geruest fuer Einzelmodul": true}
|
||||
6;"ILS 2.0 Kreisel";"0006";"Kreisel6";"E/8-F/10";1;"6538.26, 4145.31, 2000.00";"800.00, 4518.63, 1.00";"Y8QUHMcmHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-L";"0019, 0026";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.72", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel6", "Geruest fuer Einzelmodul": true}
|
||||
7;"ILS 2.0 Kreisel";"0007";"Kreisel7";"E/6-E/8";1;"4938.26, -453.25, 2000.00";"800.00, 3700.00, 1.00";"S1oU3NruHoSAAAAAAAAAWoJ4";"";"";"";"";"0008-L, 0008-R, 0002-L, 0002-R";"0024, 0013, 0025";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.9", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel7", "Geruest fuer Einzelmodul": true}
|
||||
8;"ILS 2.0 Kreisel";"0008";"Kreisel8";"B/7-E/7";1;"2438.26, 505.79, 2000.00";"4200.00, 800.00, 1.00";"BSlUB7e+HoSAAAAAAAAAAAAA";"";"";"";"";"0009-R, 0007-L";"0024, 0011, 0022, 0012";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.4", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel8", "Geruest fuer Einzelmodul": true}
|
||||
9;"ILS 2.0 Kreisel";"0009";"Kreisel9";"B/6-B/9";1;"-61.74, 1425.92, 2000.00";"800.00, 4800.00, 1.00";"/w7U8SMAHoSAAAAAAAAAWoJ4";"";"";"";"";"0001-L, 0001-R, 0008-L, 0008-R";"0010, 0023, 0022";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel9", "Geruest fuer Einzelmodul": true}
|
||||
10;"ILS 2.0 Separator";"0010";"Separator :10";"B/8";1;"-461.74, 1526.13, 2001.50";"150.00, 209.90, 3.00";"+PRUFl9EHoSAAAAAAAAAWoJ4";"/+33ADlFAE4g";"";"";"";"0022, 0023";"";"";"";{"Zuordnung": "0009-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
11;"ILS 2.0 Separator";"0011";"Separator :9";"C/7";1;"1241.80, 105.79, 1.50";"209.90, 150.00, 3.00";"EgiaAZ0+AAAAAAAAAAAAAAAA";"AC4qAAQhAAAA";"";"";"";"0024, 0022";"";"";"";{"Zuordnung": "0008-R", "ARTINR": "200000077", "Höhe": "0", "Drehung": 0, "Typ": ""}
|
||||
12;"ILS 2.0 Separator";"0012";"Separator :8";"D/7";1;"3696.84, 905.79, 2001.50";"209.90, 150.00, 3.00";"OVL2DdI+HoSAAAAAAAAAf///";"AJK/ACNhAE4g";"";"";"";"0022, 0024";"";"";"";{"Zuordnung": "0008-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 180, "Typ": ""}
|
||||
13;"ILS 2.0 Separator";"0013";"Separator :7";"E/6";1;"4538.26, -1493.47, 2001.50";"150.00, 209.90, 3.00";"RT+U6Ev0HoSAAAAAAAAAWoJ4";"ALFG/8NSAE4g";"";"";"";"0024, 0025";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
14;"ILS 2.0 Separator";"0014";"Separator :6";"E/5";1;"4538.26, -3169.65, 2001.50";"150.00, 209.90, 3.00";"RT+U0Iy8HoSAAAAAAAAApX2I";"ALFG/4aHAE4g";"";"";"";"0029, 0025";"";"";"";{"Zuordnung": "0002-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
|
||||
15;"ILS 2.0 Separator";"0015";"Separator :5";"E/4";1;"5338.26, -6534.21, 2001.50";"150.00, 209.90, 3.00";"UXSUm2GQHoSAAAAAAAAAWoJ4";"ANCG/v5rAE4g";"";"";"";"0025, 0029";"";"";"";{"Zuordnung": "0002-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
16;"ILS 2.0 Separator";"0016";"Separator :4";"F/4";1;"6938.26, -6618.23, 2001.50";"150.00, 209.90, 3.00";"ad6UmhlcHoSAAAAAAAAAWoJ4";"AQ8G/vsjAE4g";"";"";"";"0027, 0028";"";"";"";{"Zuordnung": "0004-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
17;"ILS 2.0 Separator";"0017";"Separator :3";"E/7";1;"6138.26, 302.58, 2001.50";"150.00, 209.90, 3.00";"XamUBYgiHoSAAAAAAAAApX2I";"AO/GAA4pAE4g";"";"";"";"0028, 0027";"";"";"";{"Zuordnung": "0004-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
|
||||
18;"ILS 2.0 Separator";"0018";"Separator :2";"F/8";1;"7153.84, 1885.99, 2001.50";"209.90, 150.00, 3.00";"bD6CHMcmHoSAAAAAAAAAAAAA";"ARUaAEmrAE4g";"";"";"";"0020, 0026";"";"";"";{"Zuordnung": "0005-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 0, "Typ": ""}
|
||||
19;"ILS 2.0 Separator";"0019";"Separator :1";"F/8";1;"6938.26, 2908.49, 2001.50";"150.00, 209.90, 3.00";"ad6UK3ccHoSAAAAAAAAAWoJ4";"AQ8GAG9FAE4g";"";"";"";"0026, 0026";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
20;"ILS 2.0 BTMT Beladung";"0020";"BTMT Beladung :1";"I/7-J/8";1;"14378.91, 1186.94, 1955.31";"645.71, 397.50, 316.02";"f///EJImHoSAAAAAAAAAAAAA";"AiYwADU7AE6r";"Aj4eACprAE4g";"";"";"0027, 0018";"";"";"";{"ARTINR": "829662012", "Höhe": "2000", "Drehung": 0}
|
||||
21;"ILS 2.0 BTMT Entladung";"0021";"BTMT Entladung :1";"A/7";1;"-2501.71, 212.89, 1847.09";"1468.14, 950.36, 467.01";"3bjiCN/+HoSAAAAAAAAAf/+r";"/7UPABabAE4f";"/4Hu//aMAD/1";"";"";"0023, 0023";"";"";"";{"ARTINR": "400100156", "Höhe": "2000", "Drehung": 179}
|
||||
22;"ILS Weiche";"0022";"ILS Weiche :1";"B/7";1;"338.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0011, 0012";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0008", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
|
||||
23;"ILS Weiche";"0023";"ILS Weiche :2";"B/7";1;"-461.74, 994.84, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0021, 0021";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0001", "Achse von Kreisel": "0001", "Beruehrung": "Kreis/Tangente"}
|
||||
24;"ILS Weiche";"0024";"ILS Weiche :3";"E/7";1;"4538.26, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0025, 0013";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0008", "Kreisel B": "0007", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
|
||||
25;"ILS Weiche";"0025";"ILS Weiche :4";"E/6";1;"4938.26, -2303.25, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0014, 0015";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0007", "Kreisel B": "0002", "Achse von Kreisel": "0007", "Beruehrung": "Kreis/Kreis"}
|
||||
26;"ILS Weiche";"0026";"ILS Weiche :5";"F/8";1;"6538.26, 1885.99, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0018, 0027";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0006", "Kreisel B": "0005", "Achse von Kreisel": "0006", "Beruehrung": "Kreis/Kreis"}
|
||||
27;"ILS Weiche";"0027";"ILS Weiche :6";"F/7";1;"6538.26, 1085.78, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0017, 0016";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0005", "Kreisel B": "0004", "Achse von Kreisel": "0005", "Beruehrung": "Kreis/Kreis"}
|
||||
28;"ILS Weiche";"0028";"ILS Weiche :7";"E/3";1;"6138.26, -7203.05, 2000.00";"100.25, 250.10, 30.00";"";"";"";"";"";"0029, 0029";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0004", "Kreisel B": "0003", "Achse von Kreisel": "0004", "Beruehrung": "Kreis/Kreis"}
|
||||
29;"ILS Weiche";"0029";"ILS Weiche :8";"E/3";1;"5338.26, -7202.95, 2000.00";"100.19, 250.07, 30.00";"";"";"";"";"";"0015, 0014";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0003", "Kreisel B": "0002", "Achse von Kreisel": "0003", "Beruehrung": "Kreis/Kreis"}
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 171.
|
+31
@@ -0,0 +1,31 @@
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
|
||||
1;"ILS 2.0 Kreisel";"0001";"Kreisel37";"A/7-B/8";1;"-2011.74, 994.84, 2000.00";"3100.00, 800.00, 1.00";"yab0Dy4YHoSAAAAAAAAAAAAA";"";"";"";"";"0009-L";"0024, 0021";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "2.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel37", "Geruest fuer Einzelmodul": true}
|
||||
2;"ILS 2.0 Kreisel";"0002";"Kreisel38";"E/3-E/6";1;"4940.03, -4953.25, 2000.00";"800.00, 5300.00, 1.00";"S2D+i/vOHoSAAAAAAAAAWoJ4";"";"";"";"";"0007-L, 0007-R, 0003-L";"0026, 0015, 0030, 0014";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.5", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel38", "Geruest fuer Einzelmodul": true}
|
||||
3;"ILS 2.0 Kreisel";"0003";"Kreisel39";"E/1-E/4";1;"5739.13, -9850.80, 2000.00";"800.00, 6095.71, 1.00";"V5J6gAABHoSAAAAAAAAAWoJ4";"";"";"";"";"0004-L, 0002-R";"0030, 0029";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "5.3", "Anzahl der Separatoren": "0", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel39", "Geruest fuer Einzelmodul": true}
|
||||
4;"ILS 2.0 Kreisel";"0004";"Kreisel40";"E/3-F/7";1;"6533.60, -3258.73, 2000.00";"800.00, 8689.42, 1.00";"Y7Hgi/sQHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-R, 0003-R";"0016, 0029, 0017, 0028";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "7.89", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel40", "Geruest fuer Einzelmodul": true}
|
||||
5;"ILS 2.0 Kreisel";"0005";"Kreisel41";"E/7-K/8";1;"11425.39, 1485.99, 2000.00";"10583.58, 800.00, 1.00";"XZdgFqymHoSAAAAAAAAAAAAA";"";"";"";"";"0006-L, 0006-R, 0004-L, 0004-R";"0018, 0027, 0028, 0020";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "9.78", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 0, "Name": "Kreisel41", "Geruest fuer Einzelmodul": true}
|
||||
6;"ILS 2.0 Kreisel";"0006";"Kreisel42";"E/8-F/10";1;"6533.60, 4145.31, 2000.00";"800.00, 4518.63, 1.00";"Y7HgHMcmHoSAAAAAAAAAWoJ4";"";"";"";"";"0005-L";"0019, 0027";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.72", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 90, "Name": "Kreisel42", "Geruest fuer Einzelmodul": true}
|
||||
7;"ILS 2.0 Kreisel";"0007";"Kreisel43";"E/6-E/8";1;"4940.59, 96.75, 2000.00";"800.00, 4800.00, 1.00";"S2Mu3NruHoSAAAAAAAAAWoJ4";"";"";"";"";"0008-L, 0008-R, 0002-L, 0002-R";"0022, 0025, 0013, 0026";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel43", "Geruest fuer Einzelmodul": true}
|
||||
8;"ILS 2.0 Kreisel";"0008";"Kreisel44";"B/7-E/7";1;"2440.85, 505.79, 2000.00";"4200.00, 800.00, 1.00";"BTNyB7e+HoSAAAAAAAAAAAAA";"";"";"";"";"0009-R, 0007-L";"0025, 0011, 0023, 0012";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "3.4", "Anzahl der Separatoren": "2", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel44", "Geruest fuer Einzelmodul": true}
|
||||
9;"ILS 2.0 Kreisel";"0009";"Kreisel45";"B/6-B/9";1;"-61.76, 1425.92, 2000.00";"800.00, 4800.00, 1.00";"/w7A8SMAHoSAAAAAAAAAWoJ4";"";"";"";"";"0001-L, 0001-R, 0008-L, 0008-R";"0010, 0024, 0023";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "4.0", "Anzahl der Separatoren": "1", "Kreiselart": "STANDARD", "Anzahl der Scanner": "0", "Anzahl der Rampen": "0", "Höhe in m": "2.0", "Drehrichtung": "GUZ", "Drehung": 90, "Name": "Kreisel45", "Geruest fuer Einzelmodul": true}
|
||||
10;"ILS 2.0 Separator";"0010";"Separator :11";"B/8";1;"-461.76, 1526.13, 2001.50";"150.00, 209.90, 3.00";"+PRAFl9EHoSAAAAAAAAAWoJ4";"/+33ADlFAE4g";"";"";"";"0023, 0024";"";"";"";{"Zuordnung": "0009-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
11;"ILS 2.0 Separator";"0011";"Separator :10";"C/7";1;"1244.39, 105.79, 1.50";"209.90, 150.00, 3.00";"EhK4AZ0+AAAAAAAAAAAAAAAA";"AC5EAAQhAAAA";"";"";"";"0025, 0023";"";"";"";{"Zuordnung": "0008-R", "ARTINR": "200000077", "Höhe": "0", "Drehung": 0, "Typ": ""}
|
||||
12;"ILS 2.0 Separator";"0012";"Separator :9";"D/7";1;"3699.43, 905.79, 2001.50";"209.90, 150.00, 3.00";"OV0UDdI+HoSAAAAAAAAAf///";"AJLZACNhAE4g";"";"";"";"0023, 0025";"";"";"";{"Zuordnung": "0008-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 180, "Typ": ""}
|
||||
13;"ILS 2.0 Separator";"0013";"Separator :8";"E/6";1;"4540.59, -1493.47, 2001.50";"150.00, 209.90, 3.00";"RUiu6Ev0HoSAAAAAAAAAWoJ4";"ALFd/8NSAE4g";"";"";"";"0025, 0026";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
14;"ILS 2.0 Separator";"0014";"Separator :7";"E/5";1;"4540.03, -3169.65, 2001.50";"150.00, 209.90, 3.00";"RUZ+0Iy8HoSAAAAAAAAApX2I";"ALFY/4aHAE4g";"";"";"";"0030, 0026";"";"";"";{"Zuordnung": "0002-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
|
||||
15;"ILS 2.0 Separator";"0015";"Separator :6";"E/4";1;"5340.03, -6534.21, 2001.50";"150.00, 209.90, 3.00";"UXt+m2GQHoSAAAAAAAAAWoJ4";"ANCY/v5rAE4g";"";"";"";"0026, 0030";"";"";"";{"Zuordnung": "0002-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
16;"ILS 2.0 Separator";"0016";"Separator :5";"F/4";1;"6933.60, -6618.23, 2001.50";"150.00, 209.90, 3.00";"acxgmhlcHoSAAAAAAAAAWoJ4";"AQ7Y/vsjAE4g";"";"";"";"0028, 0029";"";"";"";{"Zuordnung": "0004-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
17;"ILS 2.0 Separator";"0017";"Separator :4";"E/7";1;"6133.60, 302.58, 2001.50";"150.00, 209.90, 3.00";"XZdgBYgiHoSAAAAAAAAApX2I";"AO+YAA4pAE4g";"";"";"";"0029, 0028";"";"";"";{"Zuordnung": "0004-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 270, "Typ": ""}
|
||||
18;"ILS 2.0 Separator";"0018";"Separator :3";"F/8";1;"7149.18, 1885.99, 2001.50";"209.90, 150.00, 3.00";"bCxOHMcmHoSAAAAAAAAAAAAA";"ARTsAEmrAE4g";"";"";"";"0020, 0027";"";"";"";{"Zuordnung": "0005-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 0, "Typ": ""}
|
||||
19;"ILS 2.0 Separator";"0019";"Separator :2";"F/8";1;"6933.60, 2908.49, 2001.50";"150.00, 209.90, 3.00";"acxgK3ccHoSAAAAAAAAAWoJ4";"AQ7YAG9FAE4g";"";"";"";"0027, 0027";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
20;"ILS 2.0 BTMT Beladung";"0020";"BTMT Beladung :1";"I/7-J/8";1;"14374.25, 1186.94, 1955.31";"645.71, 397.50, 316.02";"f///EJImHoSAAAAAAAAAAAAA";"AiYBADU7AE6r";"Aj3vACprAE4g";"";"";"0028, 0018";"";"";"";{"ARTINR": "829662012", "Höhe": "2000", "Drehung": 0}
|
||||
21;"ILS 2.0 BTMT Entladung";"0021";"BTMT Entladung :1";"A/7";1;"-2501.71, 212.89, 1847.09";"1468.14, 950.36, 467.01";"3bjiCN/+HoSAAAAAAAAAf/+r";"/7UPABabAE4f";"/4Hu//aMAD/1";"";"";"0024, 0024";"";"";"";{"ARTINR": "400100156", "Höhe": "2000", "Drehung": 179}
|
||||
22;"ILS 2.0 Separator";"0022";"Separator :1";"E/8";1;"4540.59, 1258.01, 2001.50";"150.00, 209.90, 3.00";"RUiuEkfsHoSAAAAAAAAAWoJ4";"ALFdAC7MAE4g";"";"";"";"0026, 0025";"";"";"";{"Zuordnung": "0007-L", "ARTINR": "200000077", "Höhe": "2000", "Drehung": 90, "Typ": ""}
|
||||
23;"ILS Weiche";"0023";"ILS Weiche :1";"B/7";1;"338.24, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0011, 0012";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0008", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
|
||||
24;"ILS Weiche";"0024";"ILS Weiche :2";"B/7";1;"-461.76, 994.84, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0021, 0021";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0009", "Kreisel B": "0001", "Achse von Kreisel": "0001", "Beruehrung": "Kreis/Tangente"}
|
||||
25;"ILS Weiche";"0025";"ILS Weiche :3";"E/7";1;"4540.59, 505.79, 2000.00";"100.00, 250.00, 30.00";"";"";"";"";"";"0022, 0013";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0008", "Kreisel B": "0007", "Achse von Kreisel": "0008", "Beruehrung": "Kreis/Tangente"}
|
||||
26;"ILS Weiche";"0026";"ILS Weiche :4";"E/6";1;"4940.31, -2303.25, 2000.00";"250.07, 100.17, 30.00";"";"";"";"";"";"0014, 0015";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0007", "Kreisel B": "0002", "Achse von Kreisel": "0007", "Beruehrung": "Kreis/Kreis"}
|
||||
27;"ILS Weiche";"0027";"ILS Weiche :5";"F/8";1;"6533.60, 1885.99, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0018, 0028";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0006", "Kreisel B": "0005", "Achse von Kreisel": "0006", "Beruehrung": "Kreis/Kreis"}
|
||||
28;"ILS Weiche";"0028";"ILS Weiche :6";"F/7";1;"6533.60, 1085.78, 2000.00";"250.00, 100.00, 30.00";"";"";"";"";"";"0017, 0016";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0005", "Kreisel B": "0004", "Achse von Kreisel": "0005", "Beruehrung": "Kreis/Kreis"}
|
||||
29;"ILS Weiche";"0029";"ILS Weiche :7";"E/3";1;"6136.36, -7203.05, 2000.00";"100.25, 250.10, 30.00";"";"";"";"";"";"0030, 0030";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0004", "Kreisel B": "0003", "Achse von Kreisel": "0004", "Beruehrung": "Kreis/Kreis"}
|
||||
30;"ILS Weiche";"0030";"ILS Weiche :8";"E/3";1;"5339.58, -7202.95, 2000.00";"100.19, 250.07, 30.00";"";"";"";"";"";"0015, 0014";"";"";"";{"Erzeugt aus": "Kreisel-Beruehrpunkt", "Kreisel A": "0003", "Kreisel B": "0002", "Achse von Kreisel": "0003", "Beruehrung": "Kreis/Kreis"}
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 172.
|
@@ -0,0 +1,282 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/e_planning.py - Zuordnung Layout <-> E-Planung.
|
||||
|
||||
Die Geometrie ist synthetisch und nachgebaut nach den Befunden an Mubea
|
||||
(doc/E-Planung_Bruecke.md): Speicherlinien im 800-mm-Raster, Sensor auf dem
|
||||
Separator, Ventil ~270 mm daneben, Stausensor konstant ~1390 mm vom
|
||||
Ausschleuselement. Der Lagebezug ist hier die Identitaet.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import scl_skeleton
|
||||
from dxf_registration import Transform
|
||||
from e_planning import (
|
||||
EDevice,
|
||||
EplanDocument,
|
||||
LayoutPoint,
|
||||
Rule,
|
||||
assign,
|
||||
load_positionsdraw,
|
||||
load_rules,
|
||||
pair_drives,
|
||||
)
|
||||
from scl_context import build_context
|
||||
from scl_skeleton import load_document
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
|
||||
RULES = Path(__file__).resolve().parent.parent / "cfg" / "e_planning.ini"
|
||||
IDENT = Transform(dx=0.0, dy=0.0)
|
||||
|
||||
|
||||
def _dev(bmk: str, verw: str, x: float, y: float, **extra) -> EDevice:
|
||||
return EDevice(bmk=bmk, verw=verw, pos=(x, y),
|
||||
kennzeichnung="=A1+UH01-KF107", **extra)
|
||||
|
||||
|
||||
def _rolle(assignments, layout_id: str, role: str):
|
||||
return next((a for a in assignments
|
||||
if a.layout_id == layout_id and a.role == role), None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Einlesen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_positionsdraw_einlesen(tmp_path):
|
||||
datei = tmp_path / "x_positionsdraw.json"
|
||||
datei.write_text(json.dumps({
|
||||
"sensors": {
|
||||
"BG0642@UNKNOWN": {"B": "BG0642", "VERW": "IN SEP",
|
||||
"BEZEICHNUNG": "TEIL IN SEPARATOR STR. 20",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF107", "pos": [1.0, 2.0]},
|
||||
},
|
||||
"schaltschrank_elemente": {
|
||||
# Derselbe Motorschutz zweimal: einmal mit, einmal ohne Attribute
|
||||
"FC0200@": {"IO": "FC0200", "VERW": "MOTORSCHUTZ",
|
||||
"BEZEICHNUNG": "MOTORSCHUTZ CONVEYER #100", "pos": [5, 5]},
|
||||
"FC0200": {"B": "FC0200", "pos": [6, 6]},
|
||||
},
|
||||
"double_ids": {"BG0516": [[0, 0], [1, 1]]},
|
||||
}), encoding="utf-8")
|
||||
warnungen: list[str] = []
|
||||
geraete = {d.bmk: d for d in load_positionsdraw(datei, warnungen)}
|
||||
|
||||
assert set(geraete) == {"BG0642", "FC0200"}
|
||||
bg = geraete["BG0642"]
|
||||
assert (bg.verw, bg.pos, bg.cabinet) == ("IN SEP", (1.0, 2.0), False)
|
||||
assert (bg.controller, bg.station, bg.prefix, bg.number) == ("UH01", "KF107", "BG", 642)
|
||||
assert geraete["FC0200"].verw == "MOTORSCHUTZ"
|
||||
assert geraete["FC0200"].cabinet
|
||||
assert any("BG0516" in w for w in warnungen)
|
||||
|
||||
|
||||
def test_regeln_aus_der_projektkonfiguration():
|
||||
regeln, anker = load_rules(RULES)
|
||||
rollen = [r.role for r in regeln]
|
||||
assert anker == ["IN SEP"]
|
||||
# Reihenfolge = Dateireihenfolge; der Sensor muss vor dem Ventil kommen
|
||||
assert rollen.index("Separator.sensor") < rollen.index("Separator.stopper")
|
||||
stopper = next(r for r in regeln if r.role == "Separator.stopper")
|
||||
assert (stopper.layout, stopper.number_from, stopper.number_offset) == (
|
||||
["Separator"], "Separator.sensor", 1)
|
||||
|
||||
|
||||
def test_nummer_von_muss_eine_regel_sein(tmp_path):
|
||||
ini = tmp_path / "r.ini"
|
||||
ini.write_text("[Separator.stopper]\nverw = SEP\nnummer_von = Separator.gibtsnicht\n",
|
||||
encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="gibtsnicht"):
|
||||
load_rules(ini)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zuordnung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SENSOR = Rule(role="Separator.sensor", layout=["Separator"], verw=["IN SEP"],
|
||||
prefix="BG", max_mm=300)
|
||||
STOPPER = Rule(role="Separator.stopper", layout=["Separator"], verw=["SEP"],
|
||||
prefix="MB", number_from="Separator.sensor", number_offset=1,
|
||||
max_mm=600, target_mm=270)
|
||||
|
||||
|
||||
def _speicher():
|
||||
"""Drei Speicherlinien im 800-mm-Raster.
|
||||
|
||||
Das Ventil jeder Linie liegt 530 mm von ihrem Separator - und nur 270 mm
|
||||
vom Separator der Nachbarlinie. Raeumlich waere das falsch; die
|
||||
Nummernregel (MB = BG + 1) muss gewinnen.
|
||||
"""
|
||||
punkte = [LayoutPoint(f"00{i}", "Separator", 0.0, 800.0 * i) for i in (1, 2, 3)]
|
||||
geraete = []
|
||||
for i, bg in ((1, 642), (2, 644), (3, 646)):
|
||||
geraete.append(_dev(f"BG0{bg}", "IN SEP", 0.0, 800.0 * i))
|
||||
geraete.append(_dev(f"MB0{bg + 1}", "SEP", 0.0, 800.0 * i + 530.0))
|
||||
return punkte, geraete
|
||||
|
||||
|
||||
def test_ventil_ueber_die_nummer_nicht_ueber_den_abstand():
|
||||
punkte, geraete = _speicher()
|
||||
ergebnis = assign(punkte, geraete, [SENSOR, STOPPER], IDENT)
|
||||
|
||||
for sep, bg, mb in (("001", "BG0642", "MB0643"), ("002", "BG0644", "MB0645"),
|
||||
("003", "BG0646", "MB0647")):
|
||||
assert _rolle(ergebnis, sep, "Separator.sensor").bmk == bg
|
||||
stopper = _rolle(ergebnis, sep, "Separator.stopper")
|
||||
assert (stopper.bmk, stopper.rule) == (mb, "nummer")
|
||||
|
||||
|
||||
def test_jedes_e_bauteil_nur_einmal():
|
||||
"""Zwei Separatoren, ein Sensor dazwischen: nur der naehere bekommt ihn."""
|
||||
punkte = [LayoutPoint("A", "Separator", 0, 0), LayoutPoint("B", "Separator", 0, 250)]
|
||||
geraete = [_dev("BG0100", "IN SEP", 0, 100)]
|
||||
ergebnis = assign(punkte, geraete, [SENSOR], IDENT)
|
||||
assert [(a.layout_id, a.bmk) for a in ergebnis] == [("A", "BG0100")]
|
||||
assert ergebnis[0].runner_up is None
|
||||
|
||||
|
||||
def test_zu_weit_weg_bleibt_offen():
|
||||
punkte = [LayoutPoint("A", "Separator", 0, 0)]
|
||||
geraete = [_dev("BG0100", "IN SEP", 0, 400)]
|
||||
assert assign(punkte, geraete, [SENSOR], IDENT) == []
|
||||
|
||||
|
||||
def test_sollabstand_schlaegt_kleinsten_abstand():
|
||||
"""Mubea 0080: ein fremder Stausensor liegt 269 mm nah, der eigene 1391 mm."""
|
||||
stau = Rule(role="Ausschleus.stau", layout=["Ausschleus"], verw=["STAU*"],
|
||||
prefix="BG", max_mm=1600, target_mm=1390)
|
||||
punkte = [LayoutPoint("0080", "Ausschleus", 0, 0)]
|
||||
geraete = [_dev("BG0525", "STAU_REF-LT", 269, 0),
|
||||
_dev("BG0550", "STAU_REF-LT", -1391, 0)]
|
||||
ergebnis = assign(punkte, geraete, [stau], IDENT)
|
||||
assert [a.bmk for a in ergebnis] == ["BG0550"]
|
||||
|
||||
|
||||
def test_lagebezug_wird_angewendet():
|
||||
"""E-Koordinaten werden mit dem Lagebezug ins CSV gebracht."""
|
||||
punkte = [LayoutPoint("A", "Separator", 1000, 2000)]
|
||||
geraete = [_dev("BG0100", "IN SEP", 0, 0)]
|
||||
ergebnis = assign(punkte, geraete, [SENSOR], Transform(dx=1000, dy=2000))
|
||||
assert ergebnis[0].bmk == "BG0100" and ergebnis[0].distance == 0
|
||||
|
||||
|
||||
def test_schrankelemente_werden_nicht_raeumlich_zugeordnet():
|
||||
punkte = [LayoutPoint("A", "Separator", 0, 0)]
|
||||
geraete = [_dev("BG0100", "IN SEP", 0, 0, cabinet=True)]
|
||||
assert assign(punkte, geraete, [SENSOR], IDENT) == []
|
||||
|
||||
|
||||
def test_motor_und_motorschutz_ueber_die_nummer():
|
||||
geraete = [_dev("MA0200", "MOT_0,37kW", 0, 0, bezeichnung="PIN CONVEYOR#100"),
|
||||
EDevice(bmk="FC0200", verw="MOTORSCHUTZ", cabinet=True),
|
||||
_dev("MA0300", "MOT", 0, 0)]
|
||||
antriebe = {d.motor: d.protection for d in pair_drives(geraete)}
|
||||
assert antriebe == {"MA0200": "FC0200", "MA0300": ""}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bis in die SCL
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _eplan_fuer_mubea(csv_file: str = "mubea.csv") -> EplanDocument:
|
||||
"""Zuordnung fuer Separator 0012 (TRO02 der Fixture) - nur Sensor und Ventil."""
|
||||
punkte = [LayoutPoint("0012", "Separator", 0, 0)]
|
||||
geraete = [_dev("BG0712", "IN SEP", 0, 0), _dev("MB0713", "SEP", 0, 300)]
|
||||
return EplanDocument(csv_file=csv_file, eplan_file="Mubea_positionsdraw.json",
|
||||
created="2026-01-01 00:00",
|
||||
assignments=assign(punkte, geraete, [SENSOR, STOPPER], IDENT))
|
||||
|
||||
|
||||
def test_kontext_setzt_namen_ein_und_loescht_den_merker():
|
||||
document = load_document(FIXTURES / "mubea_tro.json")
|
||||
kontext = build_context(document, "mubea_tro.json", eplan=_eplan_fuer_mubea())
|
||||
tro02 = next(t for t in kontext.tros if t.id == "TRO02")
|
||||
sep = tro02.separators[0]
|
||||
assert (sep.layout, sep.sensor.value, sep.sensor.todo) == ("0012", "cInBG0712", False)
|
||||
assert (sep.stopper.value, sep.stopper.todo) == ("MB0713", False)
|
||||
# Was die E-Planung nicht liefert, bleibt offen
|
||||
assert sep.no.todo
|
||||
tro01 = next(t for t in kontext.tros if t.id == "TRO01")
|
||||
assert tro01.separators[0].sensor.todo
|
||||
assert kontext.eplan_source.startswith("Mubea_positionsdraw.json")
|
||||
|
||||
|
||||
def test_kontext_setzt_namen_auch_in_speicherlinien_ein():
|
||||
"""PinStore_Auto: die Linien sind Separatoren im Layout - ihre E-Planung-Namen
|
||||
kommen in store.lines an (und damit ins PinStore-Template), nicht nur in den
|
||||
Separatorblock."""
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from scl_skeleton import TEMPLATE_DIR, SclTemplates
|
||||
|
||||
linien = ["0066", "0069"]
|
||||
store = DerivedTro(tro_id="TRO04", type_name="PinStore_Auto", label="Speicher",
|
||||
hosts=["0016", "0017"], separators=linien, scanners=[],
|
||||
reason="Test", predecessors=["TRO03"], successors=["TRO05"])
|
||||
punkte = [LayoutPoint("0066", "Separator", 0, 0), LayoutPoint("0069", "Separator", 1000, 0)]
|
||||
geraete = [_dev("BG0766", "IN SEP", 0, 0), _dev("MB0767", "SEP", 0, 300),
|
||||
_dev("BG0769", "IN SEP", 1000, 0), _dev("MB0770", "SEP", 1000, 300)]
|
||||
eplan = EplanDocument(csv_file="mubea.csv", eplan_file="Mubea_positionsdraw.json",
|
||||
assignments=assign(punkte, geraete, [SENSOR, STOPPER], IDENT))
|
||||
document = TroDocument.from_analysis(Analysis(tros=[store]), source="mubea.csv")
|
||||
kontext = build_context(document, "mubea_tro.json", eplan=eplan)
|
||||
|
||||
zeilen = kontext.tros[0].store.lines
|
||||
assert [(z.layout, z.sensor.value, z.stopper.value) for z in zeilen] == [
|
||||
("0066", "cInBG0766", "MB0767"), ("0069", "cInBG0769", "MB0770")]
|
||||
assert not any(n.todo for z in zeilen for n in (z.sensor, z.stopper))
|
||||
|
||||
text = SclTemplates(TEMPLATE_DIR).render(kontext.tros[0])
|
||||
assert '#TRO04.arInSep[#nActNo].stSenTrolley := "DB_Inputs".Sensors["cInBG0766"];\n' in text
|
||||
assert '"MB0770" := #TRO04.arOutSeparatorStore[2];\n' in text
|
||||
|
||||
|
||||
def _lauf(tmp_path, monkeypatch, eplan: EplanDocument,
|
||||
*weitere: str) -> tuple[int, Path]:
|
||||
shutil.copy(FIXTURES / "mubea_tro.json", tmp_path / "mubea_tro.json")
|
||||
(tmp_path / "mubea_eplan.json").write_text(eplan.to_json(), encoding="utf-8")
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
|
||||
rc = scl_skeleton.main(["--json", "mubea_tro.json", "--eplan", "mubea_eplan.json",
|
||||
*weitere])
|
||||
return rc, tmp_path / "mubea_FB_Main.scl"
|
||||
|
||||
|
||||
def test_scl_skeleton_mit_eplan(tmp_path, monkeypatch):
|
||||
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea())
|
||||
text = scl.read_text(encoding="utf-8")
|
||||
assert rc == 0
|
||||
assert '#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0712"];\n' in text
|
||||
assert 'xOutStopper1 => "MB0713",\n' in text
|
||||
assert "// E-Planung: Mubea_positionsdraw.json" in text
|
||||
|
||||
|
||||
def test_scl_skeleton_mit_eplan_und_templates(tmp_path, monkeypatch):
|
||||
"""Die Namen aus der E-Planung kommen auch in den Templates an (TRO02 = 1Sep)."""
|
||||
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea(), "--templates")
|
||||
text = scl.read_text(encoding="utf-8")
|
||||
assert rc == 0
|
||||
region = text[text.index("REGION TRO02 "):]
|
||||
region = region[:region.index("\nEND_REGION")]
|
||||
assert "Aufbau nach Template FB_ILS_MTRO_1Sep.scl.j2" in region
|
||||
assert '#TRO02.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0712"];\n' in region
|
||||
assert 'xOutStopper1 => "MB0713",\n' in region
|
||||
|
||||
|
||||
def test_scl_skeleton_lehnt_fremde_zuordnung_ab(tmp_path, monkeypatch):
|
||||
rc, scl = _lauf(tmp_path, monkeypatch, _eplan_fuer_mubea("anderes.csv"))
|
||||
assert rc == 1
|
||||
assert not scl.exists()
|
||||
|
||||
|
||||
def test_eplan_json_rundlauf():
|
||||
doc = _eplan_fuer_mubea()
|
||||
zurueck = EplanDocument.from_json(doc.to_json())
|
||||
assert zurueck == doc
|
||||
assert json.loads(doc.to_json())["schema"] == "sps_skel-eplan-v1"
|
||||
assert set(zurueck.for_layout("0012")) == {"sensor", "stopper"}
|
||||
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/hw_import.py - TIA-Import + Positionsdatei -> HardwareDocument.
|
||||
|
||||
Die TIA-Datei wird hier als kleine xlsx gebaut (zipfile, wie sie der
|
||||
TIA-Export schreibt: Blatt "PLC Tags" und "Constants"); Namen, Adressen und
|
||||
Klartexte nach dem Mubea-Datensatz (data/Mubea/mubea-1_TIA.xlsx).
|
||||
"""
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import hw_import
|
||||
from hw_import import (
|
||||
HardwareDocument,
|
||||
build_hardware,
|
||||
conveyor_kind_of,
|
||||
conveyor_of,
|
||||
load_tia,
|
||||
project_name,
|
||||
read_xlsx,
|
||||
)
|
||||
|
||||
_HEAD = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||
'<{tag} xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
|
||||
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">')
|
||||
|
||||
|
||||
def _col(i: int) -> str:
|
||||
out = ""
|
||||
i += 1
|
||||
while i:
|
||||
i, r = divmod(i - 1, 26)
|
||||
out = chr(65 + r) + out
|
||||
return out
|
||||
|
||||
|
||||
def write_xlsx(path: Path, sheets: dict[str, list[list]], shared: bool = True) -> Path:
|
||||
"""Minimale xlsx: Texte als Shared Strings (oder inline), Zahlen als Werte."""
|
||||
strings: list[str] = []
|
||||
files = {}
|
||||
for n, (name, rows) in enumerate(sheets.items(), start=1):
|
||||
xml_rows = []
|
||||
for r, row in enumerate(rows, start=1):
|
||||
cells = []
|
||||
for c, value in enumerate(row):
|
||||
ref = f"{_col(c)}{r}"
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, (int, float)):
|
||||
cells.append(f'<c r="{ref}"><v>{value}</v></c>')
|
||||
elif shared:
|
||||
if value not in strings:
|
||||
strings.append(value)
|
||||
cells.append(f'<c r="{ref}" t="s"><v>{strings.index(value)}</v></c>')
|
||||
else:
|
||||
cells.append(f'<c r="{ref}" t="inlineStr"><is><t>{value}</t></is></c>')
|
||||
xml_rows.append(f'<row r="{r}">{"".join(cells)}</row>')
|
||||
files[f"xl/worksheets/sheet{n}.xml"] = (
|
||||
_HEAD.format(tag="worksheet") + f'<sheetData>{"".join(xml_rows)}</sheetData></worksheet>')
|
||||
sheet_xml = "".join(f'<sheet name="{name}" sheetId="{n}" r:id="rId{n}"/>'
|
||||
for n, name in enumerate(sheets, start=1))
|
||||
files["xl/workbook.xml"] = _HEAD.format(tag="workbook") + f"<sheets>{sheet_xml}</sheets></workbook>"
|
||||
rels = "".join(f'<Relationship Id="rId{n}" Type="worksheet" Target="worksheets/sheet{n}.xml"/>'
|
||||
for n in range(1, len(sheets) + 1))
|
||||
files["xl/_rels/workbook.xml.rels"] = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?><Relationships '
|
||||
'xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
|
||||
f"{rels}</Relationships>")
|
||||
if shared and strings:
|
||||
si = "".join(f"<si><t>{s}</t></si>" for s in strings)
|
||||
files["xl/sharedStrings.xml"] = _HEAD.format(tag="sst") + f"{si}</sst>"
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
for name, text in files.items():
|
||||
z.writestr(name, text)
|
||||
return path
|
||||
|
||||
|
||||
TAG_HEADER = ["Name", "Path", "Data Type", "Logical Address", "Comment", "Hmi Visible"]
|
||||
CONST_HEADER = ["Name", "Path", "Data Type", "Value", "Comment"]
|
||||
|
||||
|
||||
def tia_file(tmp_path: Path, extra_tags=(), extra_consts=()) -> Path:
|
||||
tags = [
|
||||
["BG0532", "Import", "Bool", "%E053.2", "SENSOR IN SEP KR-K0102", "True"],
|
||||
["MB0533", "Import", "Bool", "%A053.3", "STOPPER KR-K0102", "True"],
|
||||
["MA0202", "Import", "Bool", "%A020.2", "ANTRIEB KREISEL-K0102", "True"],
|
||||
["FC0202", "Import", "Bool", "%E020.2", "MOTORSCHUTZ KREISEL-K0102", "True"],
|
||||
["GF20000", "Import", "Bool", "%A2000.0", "INKREMENTALGEBER KREISEL K0102 (M0205)", "True"],
|
||||
["SF0001", "Import", "Bool", "%E000.1", "ANLAGE EIN", "True"],
|
||||
["BG0999", "Import", "Bool", "%E099.0", "NUR IM TIA", "True"],
|
||||
*extra_tags,
|
||||
]
|
||||
consts = [
|
||||
["cInBG0532", "Import", "Int", 0],
|
||||
["cOutMB0533", "Import", "Int", 0],
|
||||
["cOutMA0202", "Import", "Int", 1],
|
||||
["cInFC0202", "Import", "Int", 1],
|
||||
["cOutGF20000", "Import", "Int", 2],
|
||||
["cInSF0001", "Import", "Int", 2],
|
||||
["cInBG0999", "Import", "Int", 3],
|
||||
*extra_consts,
|
||||
]
|
||||
return write_xlsx(tmp_path / "x_TIA.xlsx",
|
||||
{"PLC Tags": [TAG_HEADER, *tags], "Constants": [CONST_HEADER, *consts]})
|
||||
|
||||
|
||||
def positions_file(tmp_path: Path) -> Path:
|
||||
data = {
|
||||
"sensors": {
|
||||
"BG0532@1": {"IO": "BG0532", "VERW": "IN SEP", "BEZEICHNUNG": "SENSOR IN SEP KR-K0102",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 200.0]},
|
||||
"MB0533@1": {"IO": "MB0533", "VERW": "SEP", "BEZEICHNUNG": "STOPPER KR-K0102",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF102", "pos": [100.0, 470.0]},
|
||||
"BG0514@1": {"IO": "BG0514", "VERW": "KETTENBRUCH",
|
||||
"BEZEICHNUNG": "SENSOR KETTENBRUCH CONVEYOR #101", "KENNZEICHNUNG": "",
|
||||
"pos": [5.0, 5.0]},
|
||||
},
|
||||
"schaltschrank_elemente": {
|
||||
"MA0202@1": {"IO": "MA0202", "VERW": "MOT", "BEZEICHNUNG": "ANTRIEB KREISEL-K0102",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF1DQ3"},
|
||||
"FC0202@1": {"IO": "FC0202", "BEZEICHNUNG": "MOTORSCHUTZ KREISEL-K0102",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF1DI2"},
|
||||
"QA7000@A/B": {"IO": "QA7000", "VERW": "IL-YE-LED", "BEZEICHNUNG": "HAUPTSCHUETZE",
|
||||
"KENNZEICHNUNG": "=A1+UH01-KF1FDQ1"},
|
||||
"SF0001@1": {"IO": "SF0001", "BEZEICHNUNG": "ANLAGE EIN"},
|
||||
"GF20000@1": {"IO": "GF20000",
|
||||
"BEZEICHNUNG": "INKREMENTALGEBER KREISEL K0102 (M0205)"},
|
||||
},
|
||||
"warnings": {"mapping_warnings": {"BG0514@1": "Ungueltiger Pfad in Kennzeichnung: "}},
|
||||
}
|
||||
path = tmp_path / "Xproj_positionsconv.json"
|
||||
path.write_text(json.dumps(data), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# xlsx und TIA-Import
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shared", [True, False])
|
||||
def test_xlsx_lesen(tmp_path, shared):
|
||||
path = write_xlsx(tmp_path / "t.xlsx", {"A": [["Name", "Wert"], ["x", 3], [None, "y"]]},
|
||||
shared=shared)
|
||||
assert read_xlsx(path) == {"A": [["Name", "Wert"], ["x", "3"], ["", "y"]]}
|
||||
|
||||
|
||||
def test_tia_blaetter_an_der_kopfzeile_erkannt(tmp_path):
|
||||
"""Blattnamen sind egal (deutscher Export), gezaehlt wird die Kopfzeile."""
|
||||
path = write_xlsx(tmp_path / "de_TIA.xlsx", {
|
||||
"PLC-Variablen": [["Name", "Pfad", "Datentyp", "Logische Adresse", "Kommentar"],
|
||||
["BG0532", "Import", "Bool", "%I53.2", "SENSOR"]],
|
||||
"Konstanten": [["Name", "Pfad", "Datentyp", "Wert", "Kommentar"],
|
||||
["cInBG0532", "Import", "Int", "7", ""]],
|
||||
"Leer": [["irgendwas"]],
|
||||
})
|
||||
warnings: list[str] = []
|
||||
tags, consts = load_tia(path, warnings)
|
||||
assert [(t.name, t.address, t.comment) for t in tags] == [("BG0532", "%I53.2", "SENSOR")]
|
||||
assert [(c.name, c.value) for c in consts] == [("cInBG0532", "7")]
|
||||
assert any("'Leer'" in w for w in warnings)
|
||||
|
||||
|
||||
def test_tia_ohne_tags_ist_ein_fehler(tmp_path):
|
||||
path = write_xlsx(tmp_path / "x.xlsx", {"Constants": [CONST_HEADER, ["cInBG1", "", "Int", 0]]})
|
||||
with pytest.raises(ValueError, match="kein Blatt mit Tags"):
|
||||
load_tia(path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Klartext -> Foerderer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text, name, kind", [
|
||||
("ANTRIEB KREISEL-K0102", "K0102", "Kreisel"),
|
||||
("PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)", "K0102", "Kreisel"),
|
||||
# dieselbe Strecke einmal als K0103, einmal als #103
|
||||
("MOTORSCHUTZ CONVEYOR-K0103", "K0103", "Strecke"),
|
||||
("MOTOR TEF LBT CONVEYOR-#103", "K0103", "Strecke"),
|
||||
("PIN CONVEYOR #109(EXPRESS)", "K0109", "Strecke"),
|
||||
# FC0204 im Text ist kein Foerderer
|
||||
("MOTORSCHUTZ FC0204 CONVEYOR #110", "K0110", "Strecke"),
|
||||
("SEPARATOR STR. 14", "", ""),
|
||||
])
|
||||
def test_foerderer_aus_klartext(text, name, kind):
|
||||
assert conveyor_of(text) == name
|
||||
assert conveyor_kind_of(text) == kind
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zusammenfuehren
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_beide_quellen_je_bmk_zusammengefuehrt(tmp_path):
|
||||
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj")
|
||||
dev = doc.by_bmk()
|
||||
|
||||
s = dev["BG0532"]
|
||||
assert (s.direction, s.address, s.constant, s.constant_value) == ("in", "%E053.2", "cInBG0532", 0)
|
||||
assert (s.verw, s.station, s.controller, s.pos) == ("IN SEP", "KF102", "UH01", (100.0, 200.0))
|
||||
assert s.sources == ["tia", "positions"]
|
||||
assert (s.conveyor, s.conveyor_kind) == ("K0102", "Kreisel")
|
||||
assert dev["MB0533"].constant == "cOutMB0533" and dev["MB0533"].direction == "out"
|
||||
|
||||
# nur in einer Quelle
|
||||
assert dev["QA7000"].sources == ["positions"] and dev["QA7000"].constant == ""
|
||||
assert dev["BG0999"].sources == ["tia"] and dev["BG0999"].pos is None
|
||||
|
||||
assert [d.bmk for d in doc.field_devices()] == ["BG0514", "BG0532", "MB0533"]
|
||||
assert doc.tia_sha256 and doc.positions_sha256
|
||||
assert doc.project == "Xproj"
|
||||
|
||||
|
||||
def test_antrieb_mit_motorschutz_und_geber(tmp_path):
|
||||
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path))
|
||||
[drive] = doc.drives
|
||||
assert (drive.motor, drive.protection, drive.conveyor, drive.conveyor_kind, drive.encoder) == (
|
||||
"MA0202", "FC0202", "K0102", "Kreisel", "GF20000")
|
||||
|
||||
|
||||
def test_gegenpruefung(tmp_path):
|
||||
doc = build_hardware(
|
||||
tia_file(tmp_path,
|
||||
extra_tags=[["BG0533", "Import", "Bool", "%E053.2", "GLEICHE ADRESSE", ""]],
|
||||
extra_consts=[["cInBG4711", "Import", "Int", 9]]),
|
||||
positions_file(tmp_path))
|
||||
checks = "\n".join(doc.checks)
|
||||
assert "nur in der Positionsdatei, nicht im TIA-Import (2): BG0514, QA7000" in checks
|
||||
assert "Feldgeraet nur im TIA-Import, nicht in der Positionsdatei (2): BG0533, BG0999" in checks
|
||||
assert "Signal ohne Index-Konstante cIn/cOut (1): BG0533" in checks
|
||||
assert "Index-Konstante ohne Signal (1): cInBG4711" in checks
|
||||
assert "Adresse mehrfach vergeben: %E053.2" in checks
|
||||
assert "Feldgeraet ohne Steuerung in KENNZEICHNUNG (1): BG0514" in checks
|
||||
assert any("BG0514" in w and "mapping_warnings" in w for w in doc.warnings)
|
||||
assert [c.name for c in doc.constants] == ["cInBG4711"]
|
||||
|
||||
|
||||
def test_nur_tia_ohne_positionsdatei(tmp_path):
|
||||
doc = build_hardware(tia_file(tmp_path), None)
|
||||
assert doc.positions_file == "" and doc.field_devices() == []
|
||||
assert all(d.sources == ["tia"] for d in doc.devices)
|
||||
|
||||
|
||||
def test_json_rundreise(tmp_path):
|
||||
doc = build_hardware(tia_file(tmp_path), positions_file(tmp_path), "Xproj")
|
||||
again = HardwareDocument.from_json(doc.to_json())
|
||||
assert again == doc
|
||||
assert json.loads(doc.to_json())["schema"] == "sps_skel-hw-v1"
|
||||
|
||||
|
||||
def test_projektname():
|
||||
assert project_name(None, Path("Mubea_positionsconv.json")) == "Mubea"
|
||||
assert project_name(None, Path("Mubea_positionsdraw.json")) == "Mubea"
|
||||
assert project_name(Path("mubea-1_TIA.xlsx"), None) == "mubea-1"
|
||||
|
||||
|
||||
def test_cli_mit_unterordner_in_skel_data(tmp_path, monkeypatch, capsys):
|
||||
"""--tia Mubea/x_TIA.xlsx wird ab SKEL_DATA gefunden (Befund der Nachpruefung)."""
|
||||
data = tmp_path / "data" / "Xproj"
|
||||
data.mkdir(parents=True)
|
||||
tia_file(data)
|
||||
positions_file(data)
|
||||
monkeypatch.setenv("SKEL_DATA", str(tmp_path / "data"))
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path / "results"))
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
rc = hw_import.main(["--tia", "Xproj/x_TIA.xlsx", "--positions",
|
||||
"Xproj/Xproj_positionsconv.json"])
|
||||
assert rc == 0
|
||||
written = HardwareDocument.from_json(
|
||||
(tmp_path / "results" / "Xproj_hw.json").read_text(encoding="utf-8"))
|
||||
assert written.project == "Xproj" and len(written.devices) == 9
|
||||
assert (tmp_path / "results" / "Xproj_hw.md").is_file()
|
||||
assert "HARDWAREPLANUNG EINLESEN" in capsys.readouterr().out
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/kreisel_junctions.py - Weichenknoten zwischen Kreiseln.
|
||||
|
||||
Die Geometrie stammt aus der Testanlage data/export.csv (9 Kreisel, 10
|
||||
Separatoren, 5 ILS Weichen; drei Beruehrpunkte fehlen im Export als Weiche).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from kreisel_junctions import find_contacts, find_junctions, kreisel_loop
|
||||
from material_flow import Element
|
||||
|
||||
|
||||
def _el(teile_id: str, kind: str, pos: tuple[float, float],
|
||||
bbox: tuple[float, float] | None = None, **merkmale: object) -> Element:
|
||||
return Element(
|
||||
number=teile_id, kind=kind, teile_art=kind, teile_id=teile_id,
|
||||
name=f"{kind} {teile_id}", plan_square="", neighbours=[], track_ids=[],
|
||||
merkmale=dict(merkmale), row=0, position=(pos[0], pos[1], 2000.0),
|
||||
bbox=(bbox[0], bbox[1], 1.0) if bbox else None,
|
||||
)
|
||||
|
||||
|
||||
def _kreisel(teile_id: str, pos: tuple[float, float], bbox: tuple[float, float],
|
||||
drehrichtung: str, drehung: float) -> Element:
|
||||
return _el(teile_id, "Kreisel", pos, bbox, Drehrichtung=drehrichtung, Drehung=drehung)
|
||||
|
||||
|
||||
# (Id, Position, Boundingbox, Drehrichtung, Drehung) aus data/export.csv
|
||||
KREISEL = [
|
||||
("0001", (-2011.74, 994.84), (3100.00, 800.00), "UZS", 0),
|
||||
("0002", (4940.03, -4953.25), (800.00, 5300.00), "UZS", 90),
|
||||
("0003", (5739.13, -9850.80), (800.00, 6095.71), "GUZ", 90),
|
||||
("0004", (6533.60, -3258.73), (800.00, 8689.42), "UZS", 90),
|
||||
("0005", (11425.39, 1485.99), (10583.58, 800.00), "GUZ", 0),
|
||||
("0006", (6533.60, 4145.31), (800.00, 4518.63), "UZS", 90),
|
||||
("0007", (4940.59, -453.25), (800.00, 3700.00), "GUZ", 90),
|
||||
("0008", (2440.85, 505.79), (4200.00, 800.00), "UZS", 0),
|
||||
("0009", (-61.76, 1425.92), (800.00, 4800.00), "GUZ", 90),
|
||||
]
|
||||
SEPARATOREN = [
|
||||
("0010", (-461.76, 1526.13)), ("0011", (1244.39, 105.79)),
|
||||
("0012", (3699.43, 905.79)), ("0013", (4540.59, -1493.47)),
|
||||
("0014", (4540.03, -3169.65)), ("0015", (5340.03, -6534.21)),
|
||||
("0016", (6933.60, -6618.23)), ("0017", (6133.60, 302.58)),
|
||||
("0018", (7149.18, 1885.99)), ("0019", (6933.60, 2908.49)),
|
||||
]
|
||||
WEICHEN = [
|
||||
("0022", (-461.76, 994.84)), ("0023", (4540.59, 505.79)),
|
||||
("0024", (4940.31, -2303.25)), ("0025", (6533.60, 1885.99)),
|
||||
("0026", (6533.60, 1085.78)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def testanlage() -> list[Element]:
|
||||
return ([_kreisel(*k) for k in KREISEL]
|
||||
+ [_el(i, "Separator", p) for i, p in SEPARATOREN]
|
||||
+ [_el(i, "Weiche", p) for i, p in WEICHEN])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drehrichtung", ["UZS", "GUZ"])
|
||||
def test_loop_point_project_roundtrip(drehrichtung: str) -> None:
|
||||
loop = kreisel_loop(_kreisel("K", (100.0, 200.0), (800.0, 3000.0), drehrichtung, 90))
|
||||
assert loop is not None
|
||||
for i in range(40):
|
||||
s = i * loop.length / 40
|
||||
x, y = loop.point(s)
|
||||
s2, dist, _sec = loop.project(x, y)
|
||||
assert dist < 1e-6
|
||||
assert min(abs(s2 - s), loop.length - abs(s2 - s)) < 1e-6
|
||||
|
||||
|
||||
def test_loop_direction_follows_drehrichtung() -> None:
|
||||
# Vertikaler Kreisel (Drehung 90), Bahnen bei x = -400 und x = +400:
|
||||
# UZS laeuft auf der linken (westlichen) Bahn nach Norden.
|
||||
uzs = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "UZS", 90))
|
||||
guz = kreisel_loop(_kreisel("K", (0.0, 0.0), (800.0, 3000.0), "GUZ", 90))
|
||||
s_uzs, _d, _sec = uzs.project(-400.0, 0.0)
|
||||
s_guz, _d, _sec = guz.project(-400.0, 0.0)
|
||||
assert uzs.direction(s_uzs) == pytest.approx((0.0, 1.0), abs=1e-6)
|
||||
assert guz.direction(s_guz) == pytest.approx((0.0, -1.0), abs=1e-6)
|
||||
|
||||
|
||||
def test_contacts_match_exported_weichen(testanlage: list[Element]) -> None:
|
||||
findings: list[str] = []
|
||||
_loops, contacts = find_contacts(testanlage, findings)
|
||||
by_pair = {c.kreisel: c for c in contacts}
|
||||
assert sorted(by_pair) == [
|
||||
("0001", "0009"), ("0002", "0003"), ("0002", "0007"), ("0003", "0004"),
|
||||
("0004", "0005"), ("0005", "0006"), ("0007", "0008"), ("0008", "0009"),
|
||||
]
|
||||
assert by_pair[("0002", "0007")].weiche_id == "0024"
|
||||
assert by_pair[("0005", "0006")].weiche_id == "0025"
|
||||
# Im Export fehlende Weichen bekommen eine abgeleitete Id und ein Finding
|
||||
assert by_pair[("0008", "0009")].weiche_id == "W0008_0009"
|
||||
assert not by_pair[("0008", "0009")].exported
|
||||
assert sum("ohne ILS Weiche im Export" in f for f in findings) == 3
|
||||
|
||||
|
||||
def test_junction_types_testanlage(testanlage: list[Element]) -> None:
|
||||
junctions = find_junctions(testanlage, [])
|
||||
result = sorted((j.type_name, tuple(j.separators),
|
||||
tuple(c.weiche_id for c in j.contacts)) for j in junctions)
|
||||
assert result == [
|
||||
("1Sep1Swi", ("0010",), ("0022",)),
|
||||
("1Sep1Swi", ("0011",), ("W0008_0009",)),
|
||||
("1Sep1Swi", ("0012",), ("0023",)),
|
||||
("2Sep1Swi", ("0013", "0014"), ("0024",)),
|
||||
("2Sep2Swi", ("0015", "0016"), ("W0002_0003", "W0003_0004")),
|
||||
("3Sep2Swi", ("0017", "0018", "0019"), ("0025", "0026")),
|
||||
]
|
||||
|
||||
|
||||
def test_separator_behind_other_weiche_does_not_control(testanlage: list[Element]) -> None:
|
||||
# 0010 liegt auf Kreisel 9 vor Weiche 0022; die Weiche 9/8 folgt erst
|
||||
# danach - sie steuert 0011 von Kreisel 8 aus, nicht 0010.
|
||||
junctions = find_junctions(testanlage, [])
|
||||
j98 = next(j for j in junctions if j.contacts[0].weiche_id == "W0008_0009")
|
||||
assert j98.separators == ["0011"]
|
||||
assert j98.sep_lane["0011"] == "0008-R"
|
||||
|
||||
|
||||
def test_separator_lane_is_physical_not_zuordnung(testanlage: list[Element]) -> None:
|
||||
# 0015 ist im Export Kreisel 0003 zugeordnet, liegt aber auf Kreisel 0002
|
||||
junctions = find_junctions(testanlage, [])
|
||||
j = next(j for j in junctions if "0015" in j.separators)
|
||||
assert j.sep_kreisel["0015"] == "0002"
|
||||
assert j.sep_lane["0015"] == "0002-R"
|
||||
|
||||
|
||||
def test_counter_rotating_contact_is_no_weiche() -> None:
|
||||
# Zwei UZS-Kreisel Stirn an Stirn laufen am Beruehrpunkt gegeneinander
|
||||
a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0)
|
||||
b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "UZS", 0)
|
||||
findings: list[str] = []
|
||||
_loops, contacts = find_contacts([a, b], findings)
|
||||
assert contacts == []
|
||||
assert any("gegenlaeufig" in f for f in findings)
|
||||
|
||||
|
||||
def test_co_rotating_end_contact_is_weiche() -> None:
|
||||
a = _kreisel("A", (0.0, 0.0), (3000.0, 800.0), "UZS", 0)
|
||||
b = _kreisel("B", (3000.0, 0.0), (3000.0, 800.0), "GUZ", 0)
|
||||
_loops, contacts = find_contacts([a, b], [])
|
||||
assert len(contacts) == 1
|
||||
assert math.hypot(contacts[0].x - 1500.0, contacts[0].y) < 5.0
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer den Kreisel-Umlauf im Materialflussgraphen (lib/material_flow.py).
|
||||
|
||||
Ein Kreisel ist ein geschlossener Umlauf: das Ende von L laeuft in den Anfang
|
||||
von R und das Ende von R in den Anfang von L. Frueher bekam jeder Kreisel nur
|
||||
EINE Bahnkante (aus der Drehrichtung) - jede Bahn ohne Gegenkante galt dann
|
||||
als Sackgasse bzw. unerreichbar, obwohl die Anlage vollstaendig durchlaufbar
|
||||
ist. Fixture: Testanlage test_system (dxfmakros results/export.csv, 9 Kreisel,
|
||||
8 Kreisel-Kreisel-Weichen, 1 Beladung, 1 Entladung).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from material_flow import (
|
||||
CIRCLE_RETURN_NOTE,
|
||||
EDGE_CIRCLE,
|
||||
EDGE_FLOW,
|
||||
EDGE_UNRESOLVED,
|
||||
Edge,
|
||||
Element,
|
||||
Node,
|
||||
_reachability_warnings,
|
||||
build_graph,
|
||||
read_elements,
|
||||
)
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "test_system_export.csv"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def elements() -> list[Element]:
|
||||
return read_elements(FIXTURE, [])
|
||||
|
||||
|
||||
def _graph(elements: list[Element]):
|
||||
warnings: list[str] = []
|
||||
return build_graph(elements, FIXTURE.name, warnings), warnings
|
||||
|
||||
|
||||
def test_jeder_kreisel_hat_beide_bahnkanten(elements):
|
||||
graph, _ = _graph(elements)
|
||||
for el in (e for e in elements if e.is_circle):
|
||||
left, right = f"{el.teile_id}-L", f"{el.teile_id}-R"
|
||||
circle = [e for e in graph.edges if e.kind == EDGE_CIRCLE
|
||||
and {e.src, e.dst} == {left, right}]
|
||||
assert {(e.src, e.dst) for e in circle} == {(left, right), (right, left)}
|
||||
# genau eine Hauptkante mit dem Drehrichtungs-Kuerzel
|
||||
assert sorted(e.note for e in circle) == sorted([el.rotation, CIRCLE_RETURN_NOTE])
|
||||
|
||||
|
||||
def test_keine_sackgassen_in_durchlaufbarer_anlage(elements):
|
||||
_, warnings = _graph(elements)
|
||||
bahn = [w for w in warnings if w.startswith("Bahn ")]
|
||||
assert bahn == []
|
||||
|
||||
|
||||
def test_kreis_kreis_weiche_ist_gerichtet(elements):
|
||||
"""Weiche 0006<->0005 ist "Kreis/Kreis" - liegt genau auf dem Uebergang
|
||||
Endkappe/Gerade, die Geometrie allein entschied dort per Rundung auf
|
||||
"Laengsseite, unbestimmt". Das Merkmal "Beruehrung" hat Vorrang."""
|
||||
graph, _ = _graph(elements)
|
||||
weiche = [e for e in graph.edges if e.note == "Weiche"
|
||||
and {e.src.split("-")[0], e.dst.split("-")[0]} == {"0005", "0006"}]
|
||||
assert weiche
|
||||
assert all(e.kind == EDGE_FLOW for e in weiche)
|
||||
|
||||
|
||||
def test_ohne_weichen_meldet_unerreichbare_bahnen(elements):
|
||||
"""Gegenprobe: ohne die Kreisel-Kreisel-Weichen (und ohne die rohen
|
||||
Kreisel-Nachbarn, die sonst als unbestimmte Kanten verbinden) ist nur der
|
||||
Kreisel der Beladung erreichbar - die Pruefung muss das melden."""
|
||||
ohne = [replace(e, neighbours=[]) if e.is_circle else e
|
||||
for e in elements if e.kind != "Weiche"]
|
||||
_, warnings = _graph(ohne)
|
||||
assert any("von keiner Beladung" in w for w in warnings)
|
||||
assert any("keine Entladung" in w for w in warnings)
|
||||
|
||||
|
||||
def _el(teile_id: str, kind: str) -> Element:
|
||||
return Element(
|
||||
number=teile_id, kind=kind, teile_art=kind, teile_id=teile_id,
|
||||
name=f"{kind} {teile_id}", plan_square="", neighbours=[], track_ids=[],
|
||||
merkmale={}, row=0, position=(0.0, 0.0, 0.0), bbox=None,
|
||||
)
|
||||
|
||||
|
||||
def test_unbestimmte_kante_in_beide_richtungen_begehbar():
|
||||
"""Eine Weiche mit unbestimmter Richtung (dir=both) darf nicht nur in
|
||||
src->dst-Richtung zaehlen."""
|
||||
k1, k2 = _el("0001", "Kreisel"), _el("0002", "Kreisel")
|
||||
nodes = {
|
||||
"0001-L": Node("0001-L", "Kreisel", k1, lane="L"),
|
||||
"0001-R": Node("0001-R", "Kreisel", k1, lane="R"),
|
||||
"0002-L": Node("0002-L", "Kreisel", k2, lane="L"),
|
||||
"0002-R": Node("0002-R", "Kreisel", k2, lane="R"),
|
||||
"B": Node("B", "Beladung", _el("B", "Beladung")),
|
||||
"E": Node("E", "Entladung", _el("E", "Entladung")),
|
||||
}
|
||||
edges = [
|
||||
Edge("0001-L", "0001-R", EDGE_CIRCLE, "GUZ"),
|
||||
Edge("0001-R", "0001-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
|
||||
Edge("0002-L", "0002-R", EDGE_CIRCLE, "GUZ"),
|
||||
Edge("0002-R", "0002-L", EDGE_CIRCLE, CIRCLE_RETURN_NOTE),
|
||||
# Beladung an Kreisel 2, Entladung an Kreisel 1, Weiche zeigt "falsch herum"
|
||||
Edge("B", "0002-L", EDGE_FLOW),
|
||||
Edge("0001-L", "E", EDGE_FLOW),
|
||||
Edge("0001-R", "0002-R", EDGE_UNRESOLVED, "Weiche"),
|
||||
]
|
||||
assert _reachability_warnings(nodes, edges) == []
|
||||
# gerichtet waere Kreisel 1 von der Beladung aus nicht erreichbar
|
||||
edges[-1] = Edge("0001-R", "0002-R", EDGE_FLOW, "Weiche")
|
||||
assert any("0001-L" in w and "von keiner Beladung" in w
|
||||
for w in _reachability_warnings(nodes, edges))
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/merge_hw_layout.py - TRO-JSON + Hardware-JSON -> Anlagen-JSON.
|
||||
|
||||
Synthetische Anlage nach den Mubea-Befunden (doc/E-Planung_Bruecke.md):
|
||||
Separator-Sensor auf dem Separator, Stopperventil ~270 mm daneben mit
|
||||
Nummer = Sensor + 1, Stausensor ~1390 mm vom Ausschleuselement, Kreisel mit
|
||||
ueberlappender Speicherlinie (Gefaellestrecke), Vario-Strecke mit Antrieb.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import merge_hw_layout
|
||||
from dxf_registration import Transform, load_transform, save_transform
|
||||
from merge_hw_layout import (
|
||||
build_anlage,
|
||||
load_overrides,
|
||||
load_rules,
|
||||
)
|
||||
from hw_import import HardwareDocument, HwDevice, conveyor_kind_of, conveyor_of, pair_drives
|
||||
from model import AnlageDocument, Analysis, DerivedTro, LayoutPart, TroDocument, derive_slots
|
||||
|
||||
RULES = Path(__file__).resolve().parent.parent / "cfg" / "e_planning.ini"
|
||||
IDENT = Transform(dx=0.0, dy=0.0)
|
||||
SEPS = {f"000{n}": ((n - 1) * 800.0, 0.0) for n in range(1, 7)}
|
||||
|
||||
|
||||
def _part(layout_id, kind, x, y, bbox=None, name=""):
|
||||
return LayoutPart(layout_id=layout_id, kind=kind, name=name or f"{kind}{layout_id}",
|
||||
position=(x, y, 0.0), bbox=bbox)
|
||||
|
||||
|
||||
def _tro(tro_id, type_name, seps, hosts=()):
|
||||
return DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id, hosts=list(hosts),
|
||||
separators=list(seps), scanners=[], reason="Test")
|
||||
|
||||
|
||||
def layout() -> TroDocument:
|
||||
parts = [_part(i, "Separator", x, y) for i, (x, y) in SEPS.items()]
|
||||
parts += [
|
||||
_part("0030", "Ausschleus", 0.0, 2000.0),
|
||||
_part("0010", "Kreisel", 2000.0, 5000.0, (6000.0, 800.0, 1.0), "Kreisel3"),
|
||||
# Speicherlinie, deren Huellbox den Kreisel ueberdeckt (wie bei Mubea)
|
||||
_part("0040", "Gefaellestrecke", 2000.0, 5000.0, (6000.0, 1200.0, 1.0), "GF_1"),
|
||||
_part("0020", "Strecke", 2000.0, -3000.0, (6000.0, 800.0, 1.0), "VF_1"),
|
||||
]
|
||||
tros = [
|
||||
_tro("TRO01", "1Sep", ["0001"], ["0010-R"]),
|
||||
_tro("TRO02", "1Sep1Swi", ["0002"], ["0010-R"]),
|
||||
_tro("TRO03", "Vario", ["0003"], ["0020"]),
|
||||
_tro("TRO04", "1Sep", ["0004"]),
|
||||
_tro("TRO05", "1Sep", ["0005"]),
|
||||
_tro("TRO06", "1Sep", ["0006"]),
|
||||
]
|
||||
return TroDocument.from_analysis(Analysis(tros=tros), source="x.csv", parts=parts)
|
||||
|
||||
|
||||
def _dev(bmk, verw, pos=None, text="", tia=True, cabinet=False, direction="in"):
|
||||
return HwDevice(bmk=bmk, verw=verw, bezeichnung=text, kennzeichnung="=A1+UH01-KF102",
|
||||
pos=pos, cabinet=cabinet, direction=direction,
|
||||
address="%E000.0" if direction == "in" else "%A000.0",
|
||||
constant=(f"cIn{bmk}" if direction == "in" else f"cOut{bmk}") if tia else "",
|
||||
conveyor=conveyor_of(text), conveyor_kind=conveyor_kind_of(text),
|
||||
sources=["tia", "positions"] if tia else ["positions"])
|
||||
|
||||
|
||||
def hardware(shift=(0.0, 0.0)) -> HardwareDocument:
|
||||
"""E-Zeichnung = Layout - shift (shift = Lagebezug E -> Layout)."""
|
||||
def at(x, y):
|
||||
return (x - shift[0], y - shift[1])
|
||||
|
||||
devices = []
|
||||
for n in range(1, 6):
|
||||
x, y = SEPS[f"000{n}"]
|
||||
devices.append(_dev(f"BG0{n}01", "IN SEP", at(x, y)))
|
||||
if n != 3:
|
||||
# MB0502 fehlt im TIA-Import -> Widerspruch
|
||||
devices.append(_dev(f"MB0{n}02", "SEP", at(x, y + 270.0), tia=(n != 5),
|
||||
direction="out"))
|
||||
devices += [
|
||||
_dev("BG0601", "STAU_REF-LT", at(0.0, 2000.0 + 1390.0)),
|
||||
_dev("BG0701", "SYNC P1", at(1000.0, 5100.0), "PRUEFUNG BT (KR-K0102 PUFFER)"),
|
||||
_dev("BG0702", "BT BELADEN", at(3000.0, 4900.0), "BT BELADEN KR-K0102"),
|
||||
_dev("BG0801", "KETTENBRUCH", at(1000.0, -3000.0), "KETTENBRUCH CONVEYOR #106"),
|
||||
_dev("BG0802", "UEBERGABE", at(3000.0, -3000.0), "UEBERGABE CONVEYOR #106"),
|
||||
_dev("BG0900", "UEBERNAHME", at(50000.0, 0.0), "SENSOR UEBERNAHME"),
|
||||
_dev("MA0202", "MOT", None, "ANTRIEB KREISEL-K0102", cabinet=True, direction="out"),
|
||||
_dev("FC0202", "", None, "MOTORSCHUTZ KREISEL-K0102", cabinet=True),
|
||||
_dev("MA0210", "MOT", None, "PIN CONVEYOR-#106", cabinet=True, direction="out"),
|
||||
_dev("FC0210", "", None, "MOTORSCHUTZ CONVEYOR #106", cabinet=True),
|
||||
]
|
||||
return HardwareDocument(project="X", positions_file="X_positionsconv.json",
|
||||
positions_sha256="abc", devices=devices,
|
||||
drives=pair_drives(devices))
|
||||
|
||||
|
||||
def run(doc=None, hw=None, overrides=None, transform=IDENT, tmp_path=None):
|
||||
rules, anchors = load_rules(RULES)
|
||||
return build_anlage(doc or layout(), hw or hardware(), rules, anchors,
|
||||
tmp_path or Path("."), "x_tro.json", "X_hw.json",
|
||||
overrides, transform=transform)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_separator_sensor_raeumlich_stopper_ueber_nummer():
|
||||
anlage = run()
|
||||
sensor = anlage.binding("TRO01", "separator1", "sensor")
|
||||
stopper = anlage.binding("TRO01", "separator1", "stopper")
|
||||
assert (sensor.bmk, sensor.constant, sensor.rule, sensor.status) == (
|
||||
"BG0101", "cInBG0101", "raeumlich", "zugeordnet")
|
||||
assert (stopper.bmk, stopper.constant, stopper.rule) == ("MB0102", "cOutMB0102", "nummer")
|
||||
assert stopper.layout_id == "0001"
|
||||
|
||||
|
||||
def test_bmk_nicht_im_tia_ist_widerspruch():
|
||||
b = run().binding("TRO05", "separator1", "stopper")
|
||||
assert (b.bmk, b.status, b.note) == ("MB0502", "widerspruch", "BMK nicht im TIA-Import")
|
||||
|
||||
|
||||
def test_pflichtrollen_ohne_bmk_sind_offen_optionale_nicht():
|
||||
anlage = run()
|
||||
roles = {(b.slot, b.role): b.status for b in anlage.bindings if b.tro == "TRO06"}
|
||||
assert roles == {("separator1", "sensor"): "offen", ("separator1", "stopper"): "offen"}
|
||||
# die Weiche hat noch keine Layout-ID -> Ventil offen
|
||||
assert anlage.binding("TRO02", "switch1", "valve").status == "offen"
|
||||
|
||||
|
||||
def test_bauteil_ohne_slot_bleibt_sichtbar():
|
||||
loose = [b for b in run().bindings if not b.tro]
|
||||
assert [(b.role, b.layout_id, b.bmk, b.status) for b in loose] == [
|
||||
("Ausschleus.stau", "0030", "BG0601", "ohne_slot")]
|
||||
|
||||
|
||||
def test_unzugeordnete_feldgeraete():
|
||||
assert "BG0900" in run().unassigned
|
||||
assert "BG0101" not in run().unassigned
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Foerderer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_foerderer_per_mehrheit_und_art():
|
||||
"""K0102 nennt "KR" -> nur Kreisel stimmen, obwohl die Speicherlinie den
|
||||
Kreisel ueberdeckt; #106 nennt "CONVEYOR" -> Strecke."""
|
||||
anlage = run()
|
||||
by_name = {c.eplan_name: c for c in anlage.conveyors}
|
||||
k = by_name["K0102"]
|
||||
assert (k.layout_id, k.layout_name, k.votes, k.total, k.status) == (
|
||||
"0010", "Kreisel3", 2, 2, "zugeordnet")
|
||||
assert (k.motor, k.protection) == ("MA0202", "FC0202")
|
||||
assert by_name["K0106"].layout_id == "0020"
|
||||
assert anlage.conveyor_for("0010-R").eplan_name == "K0102"
|
||||
|
||||
|
||||
def test_vario_antrieb_aus_dem_foerderer():
|
||||
anlage = run()
|
||||
motor = anlage.binding("TRO03", "drive", "motor")
|
||||
schutz = anlage.binding("TRO03", "drive", "motorProtection")
|
||||
assert (motor.bmk, motor.rule, motor.constant) == ("MA0210", "mehrheit", "cOutMA0210")
|
||||
assert schutz.bmk == "FC0210"
|
||||
assert anlage.binding("TRO03", "drive", "senJam").status == "offen"
|
||||
|
||||
|
||||
def test_foerderer_mit_zu_wenig_stimmen_bleibt_offen():
|
||||
hw = hardware()
|
||||
hw.devices = [d for d in hw.devices if d.bmk != "BG0802"]
|
||||
k = next(c for c in run(hw=hw).conveyors if c.eplan_name == "K0106")
|
||||
assert k.status == "offen" and "unsicher: 1 von 1" in k.note
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Handkorrekturen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_handkorrekturen(tmp_path):
|
||||
ini = tmp_path / "hw_overrides.ini"
|
||||
ini.write_text(
|
||||
"[x.csv.bindings]\n"
|
||||
"TRO06.separator1.sensor = BG0401 ; vom Nachbarn umhaengen\n"
|
||||
"TRO06.separator1.stopper = MB4711\n"
|
||||
"TRO02.separator1.stopper = -\n"
|
||||
"TRO99.separator1.sensor = BG0101\n"
|
||||
"[x.csv.conveyors]\n"
|
||||
"K0102 = -\n"
|
||||
"[andere.csv.bindings]\n"
|
||||
"TRO01.separator1.sensor = BG0999\n", encoding="utf-8")
|
||||
anlage = run(overrides=load_overrides(ini, "x.csv"))
|
||||
|
||||
moved = anlage.binding("TRO06", "separator1", "sensor")
|
||||
assert (moved.bmk, moved.rule, moved.source, moved.status) == (
|
||||
"BG0401", "hand", "hw_overrides.ini", "zugeordnet")
|
||||
# automatisch vergeben war BG0401 an TRO04 - dort jetzt offen
|
||||
assert anlage.binding("TRO04", "separator1", "sensor").status == "offen"
|
||||
unknown = anlage.binding("TRO06", "separator1", "stopper")
|
||||
assert (unknown.status, unknown.note) == ("widerspruch", "BMK unbekannt in der Hardwareplanung")
|
||||
assert anlage.binding("TRO02", "separator1", "stopper").status == "offen"
|
||||
assert any("TRO99.separator1" in w for w in anlage.warnings)
|
||||
# andere Anlage nicht angewendet
|
||||
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
|
||||
k = next(c for c in anlage.conveyors if c.eplan_name == "K0102")
|
||||
assert (k.status, k.rule, k.layout_id) == ("offen", "hand", "")
|
||||
# ohne Foerderer kein Antrieb ueber K0102, der Vario-Antrieb bleibt
|
||||
assert anlage.binding("TRO03", "drive", "motor").bmk == "MA0210"
|
||||
|
||||
|
||||
def test_falscher_schluessel_in_handkorrektur(tmp_path):
|
||||
ini = tmp_path / "hw_overrides.ini"
|
||||
ini.write_text("[x.csv.bindings]\nTRO01.sensor = BG0101\n", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="<TRO-ID>.<Slot>.<Rolle>"):
|
||||
load_overrides(ini, "x.csv")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Speicher: Linien-Slots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_speicher_bekommt_linien_slots_und_beide_slots_die_zuordnung():
|
||||
store = _tro("TRO01", "PinStore_Auto", ["0001", "0002", "0003"], ["0016", "0017", "0018"])
|
||||
assert [(s.slot, s.layout_id) for s in derive_slots(store)] == [
|
||||
("separator1", "0001"), ("line1", "0001"), ("line2", "0002"), ("line3", "0003"),
|
||||
("scanner1", "")]
|
||||
doc = TroDocument.from_analysis(Analysis(tros=[store]), source="x.csv",
|
||||
parts=[_part(i, "Separator", *SEPS[i]) for i in ("0001", "0002", "0003")])
|
||||
anlage = run(doc=doc)
|
||||
a = anlage.binding("TRO01", "separator1", "sensor")
|
||||
b = anlage.binding("TRO01", "line1", "sensor")
|
||||
assert a.bmk == b.bmk == "BG0101" and a.status == b.status == "zugeordnet"
|
||||
assert anlage.binding("TRO01", "line2", "stopper").bmk == "MB0202"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON und Lauf
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ohne_parts_kein_lauf():
|
||||
doc = layout()
|
||||
doc.parts = []
|
||||
with pytest.raises(ValueError, match="parts"):
|
||||
run(doc=doc)
|
||||
|
||||
|
||||
def test_anlage_json_rundreise():
|
||||
anlage = run()
|
||||
again = AnlageDocument.from_json(anlage.to_json())
|
||||
assert again == anlage
|
||||
data = json.loads(anlage.to_json())
|
||||
assert data["schema"] == "sps_skel-anlage-v1"
|
||||
assert data["layout"]["schema"] == "sps_skel-tro-v4"
|
||||
assert data["layout"]["parts"][0]["layoutId"] == "0001"
|
||||
|
||||
|
||||
def test_gespeicherter_lagebezug_gilt_nur_fuer_denselben_dateistand(tmp_path):
|
||||
t = Transform(dx=5.0, dy=6.0, csv_file="x.csv", dxf_file="E.json", fingerprint="aaa")
|
||||
save_transform(tmp_path, t)
|
||||
assert load_transform(tmp_path, "x.csv", "E.json", "aaa").dx == 5.0
|
||||
assert load_transform(tmp_path, "x.csv", "E.json", "bbb") is None
|
||||
# ohne Pruefsumme (tro_annotate) wie bisher ueber den Dateinamen
|
||||
assert load_transform(tmp_path, "x.csv", "E.json").dx == 5.0
|
||||
# alte Ablage ohne Pruefsumme gilt fuer einen Aufrufer mit Pruefsumme als veraltet
|
||||
save_transform(tmp_path, Transform(dx=1.0, dy=1.0, csv_file="x.csv", dxf_file="Alt.json"))
|
||||
assert load_transform(tmp_path, "x.csv", "Alt.json", "aaa") is None
|
||||
|
||||
|
||||
def test_cli_mit_lagebezug(tmp_path, monkeypatch, capsys):
|
||||
"""Ende zu Ende: E-Zeichnung um (100, 50) verschoben; Lagebezug wird
|
||||
ermittelt, mit Pruefsumme abgelegt und beim zweiten Lauf wiederverwendet."""
|
||||
results, cfg = tmp_path / "results", tmp_path / "cfg"
|
||||
results.mkdir()
|
||||
cfg.mkdir()
|
||||
(cfg / "e_planning.ini").write_text(RULES.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
(results / "x_tro.json").write_text(layout().to_json(), encoding="utf-8")
|
||||
hw = hardware(shift=(100.0, 50.0))
|
||||
(results / "X_hw.json").write_text(hw.to_json(), encoding="utf-8")
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(results))
|
||||
monkeypatch.setenv("SKEL_CFG", str(cfg))
|
||||
|
||||
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
|
||||
first = capsys.readouterr().out
|
||||
assert "Lagebezug neu ermittelt" in first
|
||||
anlage = AnlageDocument.from_json((results / "x_anlage.json").read_text(encoding="utf-8"))
|
||||
assert (anlage.registration.dx, anlage.registration.dy) == (100.0, 50.0)
|
||||
assert anlage.binding("TRO01", "separator1", "sensor").bmk == "BG0101"
|
||||
assert (results / "x_anlage.md").is_file()
|
||||
stored = json.loads((cfg / "dxf_registration.json").read_text(encoding="utf-8"))
|
||||
assert stored["x.csv|X_positionsconv.json"]["fingerprint"] == "abc"
|
||||
|
||||
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
|
||||
assert "Lagebezug neu ermittelt" not in capsys.readouterr().out
|
||||
|
||||
# neuer Stand der Positionsdatei, gleicher Name -> neu ermitteln
|
||||
hw.positions_sha256 = "neu"
|
||||
(results / "X_hw.json").write_text(hw.to_json(), encoding="utf-8")
|
||||
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 0
|
||||
assert "Lagebezug neu ermittelt" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_cli_alte_tro_json_ohne_parts(tmp_path, monkeypatch, capsys):
|
||||
results = tmp_path / "results"
|
||||
results.mkdir()
|
||||
doc = layout()
|
||||
doc.parts = []
|
||||
(results / "x_tro.json").write_text(doc.to_json(), encoding="utf-8")
|
||||
(results / "X_hw.json").write_text(hardware().to_json(), encoding="utf-8")
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(results))
|
||||
monkeypatch.setenv("SKEL_CFG", str(Path(RULES).parent))
|
||||
assert merge_hw_layout.main(["--tro", "x_tro.json", "--hw", "X_hw.json"]) == 1
|
||||
assert "tro_flow --json neu erzeugen" in capsys.readouterr().err
|
||||
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/model.py - die TRO-JSON zwischen tro_flow und scl_skeleton."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from model import SCHEMA, Analysis, Connection, DerivedTro, TroCandidate, TroDocument
|
||||
from scl_skeleton import render
|
||||
|
||||
|
||||
def _tro(tro_id: str, type_name: str, **extra) -> DerivedTro:
|
||||
data = dict(
|
||||
tro_id=tro_id,
|
||||
type_name=type_name,
|
||||
label=f"Label {tro_id}",
|
||||
hosts=["0001"],
|
||||
separators=["0040"],
|
||||
scanners=[],
|
||||
reason="Test",
|
||||
confidence="mittel",
|
||||
)
|
||||
data.update(extra)
|
||||
return DerivedTro(**data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def document() -> TroDocument:
|
||||
"""Kleine Anlage: SYS1 -> TRO01 (Weiche) -> TRO02 / TRO03 -> SYS2."""
|
||||
analysis = Analysis(
|
||||
tros=[
|
||||
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"],
|
||||
position=(1030.0, 4700.0, 1500.0)),
|
||||
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
|
||||
successors=["SYS2"]),
|
||||
_tro("TRO03", "Vario", separators=["0042"], predecessors=["TRO01"]),
|
||||
],
|
||||
boundaries=[
|
||||
_tro("SYS1", "Materialeingang", separators=[], successors=["TRO01"]),
|
||||
_tro("SYS2", "Materialausgang", separators=[], predecessors=["TRO02"]),
|
||||
],
|
||||
edges=[("SYS1", "TRO01"), ("TRO01", "TRO02"), ("TRO01", "TRO03"), ("TRO02", "SYS2")],
|
||||
edge_kinds={("TRO01", "TRO02"): "weiche"},
|
||||
findings=["ein Befund"],
|
||||
)
|
||||
return TroDocument.from_analysis(analysis, source="test.csv", note="Test")
|
||||
|
||||
|
||||
def test_round_trip_is_lossless(document):
|
||||
text = document.to_json()
|
||||
again = TroDocument.from_json(text)
|
||||
assert again.to_json() == text
|
||||
assert again.by_id("TRO01").successors == ["TRO02", "TRO03"]
|
||||
|
||||
|
||||
def test_json_uses_tro_json_keys(document):
|
||||
data = json.loads(document.to_json())
|
||||
assert data["schema"] == SCHEMA
|
||||
first = data["tros"][0]
|
||||
assert first["id"] == "TRO01"
|
||||
assert first["type"] == "1Sep1Swi"
|
||||
assert first["fbType"] == "FB_ILS_MTRO_1Sep1Swi"
|
||||
assert first["items"] == {"Separator": 1, "Switch": 1}
|
||||
assert first["position"] == {"x": 1030.0, "y": 4700.0, "z": 1500.0}
|
||||
assert data["connections"][1] == {"from": "TRO01", "to": "TRO02", "kind": "weiche"}
|
||||
assert data["candidates"] == []
|
||||
|
||||
|
||||
def test_candidate_round_trip():
|
||||
candidate = TroCandidate(name="Qualitaetskontrolle", type_name="1Sep1Swi",
|
||||
position=(-1026.0, 5250.05, 1478.5), belongs_to="TRO09")
|
||||
data = json.loads(candidate.model_dump_json(by_alias=True))
|
||||
assert data["type"] == "1Sep1Swi"
|
||||
assert data["position"] == {"x": -1026.0, "y": 5250.05, "z": 1478.5}
|
||||
assert data["belongsTo"] == "TRO09"
|
||||
assert TroCandidate.model_validate(data) == candidate
|
||||
assert candidate.describe_position() == "-1026, 5250"
|
||||
|
||||
|
||||
def test_items_and_fb_block_follow_the_type():
|
||||
tro = _tro("TRO01", "1Sep")
|
||||
assert tro.items == {"Separator": 1}
|
||||
tro.type_name = "1Sep2Swi"
|
||||
assert tro.items == {"Separator": 1, "Switch": 2, "Scanner": 1}
|
||||
assert tro.fb_block == "FB_ILS_MTRO_1Sep2Swi"
|
||||
|
||||
|
||||
def test_unknown_type_has_no_catalog_data():
|
||||
station = _tro("SYS1", "Materialeingang")
|
||||
assert station.definition() is None
|
||||
assert station.items == {}
|
||||
assert station.fb_block == ""
|
||||
|
||||
|
||||
def test_catalog_fields_cannot_be_assigned():
|
||||
tro = _tro("TRO01", "1Sep")
|
||||
with pytest.raises((AttributeError, ValidationError)):
|
||||
tro.fb_block = "FB_Anders"
|
||||
|
||||
|
||||
def test_unknown_key_is_rejected(document):
|
||||
data = json.loads(document.to_json())
|
||||
data["tros"][0]["separatorz"] = data["tros"][0].pop("separators")
|
||||
with pytest.raises(ValidationError, match="separatorz"):
|
||||
TroDocument.model_validate(data)
|
||||
|
||||
|
||||
def test_items_not_matching_catalog_are_rejected(document):
|
||||
data = json.loads(document.to_json())
|
||||
data["tros"][1]["items"] = {"Separator": 2}
|
||||
with pytest.raises(ValidationError, match="passt nicht zum Typ"):
|
||||
TroDocument.model_validate(data)
|
||||
|
||||
|
||||
def test_fb_type_not_matching_catalog_is_rejected(document):
|
||||
data = json.loads(document.to_json())
|
||||
data["tros"][1]["fbType"] = "FB_ILS_MTRO_Vario"
|
||||
with pytest.raises(ValidationError, match="fbType"):
|
||||
TroDocument.model_validate(data)
|
||||
|
||||
|
||||
def test_connection_accepts_alias_and_field_name():
|
||||
assert Connection(**{"from": "A", "to": "B"}) == Connection(src="A", dst="B")
|
||||
|
||||
|
||||
def test_scl_skeleton_renders_from_document(document):
|
||||
scl = render(document, "test_tro.json")
|
||||
assert 'TRO01 : "FB_ILS_MTRO_1Sep1Swi";' in scl
|
||||
assert 'TRO03 : "FB_ILS_MTRO_Vario";' in scl
|
||||
# Weiche: FC_Direction und eine Zielliste je Abgang
|
||||
assert '"FC_Direction"' in scl
|
||||
assert "// ueber TRO02: SYS2, TRO02" in scl
|
||||
# Vario-TRO bekommt den Vario-Parameterblock
|
||||
assert "#TRO03.stInVario.Settings" in scl
|
||||
# Systemgrenzen sind keine Instanzen
|
||||
assert "SYS1 :" not in scl
|
||||
assert "Anfang bei TRO01" in scl
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema v4: Slots und Staubereiche
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_slots_aus_katalog_und_layout(document):
|
||||
weiche = document.by_id("TRO01")
|
||||
assert [(s.slot, s.layout_id) for s in weiche.slots] == [
|
||||
("separator1", "0040"), ("switch1", "")]
|
||||
assert weiche.slots[0].roles == ["sensor", "stopper", "jamSensor?", "partSensor?"]
|
||||
vario = document.by_id("TRO03")
|
||||
assert [s.slot for s in vario.slots] == ["separator1", "drive"]
|
||||
assert vario.slots[1].layout_id == "0001" # Host = angetriebene Strecke
|
||||
# Systemgrenzen haben keine Slots
|
||||
assert document.by_id("SYS1").slots == []
|
||||
|
||||
|
||||
def test_ein_staubereich_je_tro_eingang_nach_separator(document):
|
||||
areas = {a.jam_id: a for a in document.jam_areas}
|
||||
assert sorted(areas) == ["J0040", "J0041", "J0042"]
|
||||
assert areas["J0040"].tro == "TRO01"
|
||||
assert areas["J0040"].predecessors == ["SYS1"]
|
||||
assert areas["J0040"].position == (1030.0, 4700.0, 1500.0)
|
||||
assert areas["J0041"].predecessors == ["TRO01"]
|
||||
|
||||
|
||||
def test_zusammenfuehrung_ein_staubereich_mehrere_vorgaenger():
|
||||
analysis = Analysis(tros=[
|
||||
_tro("TRO01", "1Sep", separators=["0001"], successors=["TRO03"]),
|
||||
_tro("TRO02", "1Sep", separators=["0002"], successors=["TRO03"]),
|
||||
_tro("TRO03", "1Sep", separators=["0003"], predecessors=["TRO01", "TRO02"]),
|
||||
])
|
||||
doc = TroDocument.from_analysis(analysis, source="t")
|
||||
eingang = [a for a in doc.jam_areas if a.tro == "TRO03"]
|
||||
assert [(a.jam_id, a.predecessors) for a in eingang] == [("J0003", ["TRO01", "TRO02"])]
|
||||
|
||||
|
||||
def test_zwei_separatoren_zwei_eingaenge_aufteilung_offen():
|
||||
analysis = Analysis(tros=[
|
||||
_tro("TRO01", "2Sep2Swi", separators=["0005", "0036"], predecessors=["TRO09"]),
|
||||
])
|
||||
areas = TroDocument.from_analysis(analysis, source="t").jam_areas
|
||||
assert [(a.jam_id, a.entry, a.predecessors) for a in areas] == [
|
||||
("J0005", 1, ["TRO09"]), ("J0036", 2, [])]
|
||||
assert all("offen" in a.note for a in areas)
|
||||
|
||||
|
||||
def test_v4_round_trip_und_v3_ohne_jam_areas(document):
|
||||
data = json.loads(document.to_json())
|
||||
assert data["schema"] == "sps_skel-tro-v4"
|
||||
assert data["jamAreas"][0]["id"] == "J0040"
|
||||
assert data["tros"][0]["slots"][0]["layoutId"] == "0040"
|
||||
assert TroDocument.from_json(document.to_json()) == document
|
||||
# v3-Datei: keine jamAreas/slots -> beim Rendern abgeleitet
|
||||
del data["jamAreas"]
|
||||
for tro in data["tros"]:
|
||||
del tro["slots"]
|
||||
data["schema"] = "sps_skel-tro-v3"
|
||||
alt = TroDocument.from_json(json.dumps(data))
|
||||
assert alt.jam_areas == []
|
||||
assert [a.jam_id for a in alt.jam_areas_or_derived()] == ["J0040", "J0041", "J0042"]
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/parameter.py und cfg/parameter.ini."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import parameter
|
||||
from parameter import ParameterError, get_float, get_int
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BASE = ROOT / "cfg" / parameter.PARAMETER_FILE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frisch(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Parameter-Cache leeren, damit SKEL_CFG im Test wirkt - und danach wieder."""
|
||||
parameter._parser.cache_clear()
|
||||
yield monkeypatch
|
||||
parameter._parser.cache_clear()
|
||||
|
||||
|
||||
def _base() -> configparser.ConfigParser:
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
|
||||
parser.read(BASE, encoding="utf-8")
|
||||
return parser
|
||||
|
||||
|
||||
def test_base_values_are_read(frisch: pytest.MonkeyPatch) -> None:
|
||||
frisch.delenv("SKEL_CFG", raising=False)
|
||||
assert get_float("kreisel_junctions", "contact_tolerance") == 50.0
|
||||
assert get_int("tro_flow", "min_store_lanes") == 2
|
||||
|
||||
|
||||
def test_skel_cfg_overrides_single_value(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
(tmp_path / parameter.PARAMETER_FILE).write_text(
|
||||
"[kreisel_junctions]\ncontact_tolerance = 75 ; Anlage X\n", encoding="utf-8")
|
||||
frisch.setenv("SKEL_CFG", str(tmp_path))
|
||||
assert get_float("kreisel_junctions", "contact_tolerance") == 75.0
|
||||
# nicht ueberschriebene Werte kommen weiter aus der Grunddatei
|
||||
assert get_float("kreisel_junctions", "separator_tolerance") == 100.0
|
||||
|
||||
|
||||
def test_missing_key_is_an_error(frisch: pytest.MonkeyPatch) -> None:
|
||||
frisch.delenv("SKEL_CFG", raising=False)
|
||||
with pytest.raises(ParameterError, match=r"\[tro_flow\] gibt_es_nicht fehlt"):
|
||||
get_float("tro_flow", "gibt_es_nicht")
|
||||
|
||||
|
||||
def test_non_number_is_an_error(frisch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
(tmp_path / parameter.PARAMETER_FILE).write_text(
|
||||
"[tro_flow]\nmin_store_lanes = zwei\n", encoding="utf-8")
|
||||
frisch.setenv("SKEL_CFG", str(tmp_path))
|
||||
with pytest.raises(ParameterError, match="keine ganze Zahl"):
|
||||
get_int("tro_flow", "min_store_lanes")
|
||||
|
||||
|
||||
def test_every_key_is_used_by_its_module() -> None:
|
||||
"""Jeder Schluessel der Grunddatei wird in lib/<Abschnitt>.py gelesen -
|
||||
keine verwaisten Werte, die nichts mehr steuern."""
|
||||
parser = _base()
|
||||
for section in parser.sections():
|
||||
source = (ROOT / "lib" / f"{section}.py").read_text(encoding="utf-8")
|
||||
for key in parser.options(section):
|
||||
assert re.search(rf'"{key}"', source), f"[{section}] {key} wird nicht gelesen"
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/scl_context.py - TroDocument -> Render-Kontext.
|
||||
|
||||
Die komplette Ausgabe sichern die Golden-Files (test_scl_skeleton_golden.py);
|
||||
hier stehen die Ableitungen einzeln, auf die sich Templates (Schritt 3) und
|
||||
Namensregeln (Schritt 4) aus doc/SCL_Templates.md verlassen.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from scl_context import SclName, build_context, flow_order
|
||||
from scl_skeleton import load_document
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
|
||||
|
||||
|
||||
def _tro(tro_id: str, type_name: str, **extra) -> DerivedTro:
|
||||
data = dict(
|
||||
tro_id=tro_id,
|
||||
type_name=type_name,
|
||||
label=f"Label {tro_id}",
|
||||
hosts=["0001"],
|
||||
separators=["0040"],
|
||||
scanners=[],
|
||||
reason="Test",
|
||||
)
|
||||
data.update(extra)
|
||||
return DerivedTro(**data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kontext():
|
||||
"""SYS1 -> TRO01 (1 Weiche) -> TRO02 -> SYS2 und TRO01 -> TRO03 (Vario)."""
|
||||
analysis = Analysis(tros=[
|
||||
_tro("TRO01", "1Sep1Swi", predecessors=["SYS1"], successors=["TRO02", "TRO03"]),
|
||||
_tro("TRO02", "1Sep", separators=["0041"], predecessors=["TRO01"],
|
||||
successors=["SYS2"]),
|
||||
_tro("TRO03", "Vario", separators=[], predecessors=["TRO01"]),
|
||||
])
|
||||
document = TroDocument.from_analysis(analysis, source="test.csv")
|
||||
return build_context(document, "test_tro.json", created="2026-01-01 00:00")
|
||||
|
||||
|
||||
def _by_id(context, tro_id):
|
||||
return next(t for t in context.tros if t.id == tro_id)
|
||||
|
||||
|
||||
def test_reihenfolge_und_startgrund(kontext):
|
||||
assert [t.id for t in kontext.tros] == ["TRO01", "TRO02", "TRO03"]
|
||||
assert kontext.start_reason == "Materialeingang ueber SYS1"
|
||||
assert kontext.created == "2026-01-01 00:00"
|
||||
|
||||
|
||||
def test_nummernteil_wie_geschrieben():
|
||||
"""{nr} fuer die Namensregeln: fuehrende Nullen bleiben stehen."""
|
||||
analysis = Analysis(tros=[_tro("TRO07", "1Sep"), _tro("TRO107", "1Sep"),
|
||||
_tro("XY", "1Sep")])
|
||||
ctx = build_context(TroDocument.from_analysis(analysis, source="t"), "t")
|
||||
assert {t.id: t.nr for t in ctx.tros} == {"TRO07": "07", "TRO107": "107", "XY": ""}
|
||||
|
||||
|
||||
def test_staubereiche_nummerierung(kontext):
|
||||
"""Eingaenge ab 1, Ausgaenge ab 2 - wie in den Referenz-Aufrufstellen."""
|
||||
tro = _by_id(kontext, "TRO01")
|
||||
assert [(j.n, j.neighbour) for j in tro.jam_entries] == [
|
||||
(1, "SYS1 (Station, kein TRO)")]
|
||||
assert [(j.n, j.neighbour) for j in tro.jam_exits] == [(2, "TRO02"), (3, "TRO03")]
|
||||
|
||||
|
||||
def test_eine_weiche_bedient_alle_abgaenge(kontext):
|
||||
tro = _by_id(kontext, "TRO01")
|
||||
assert len(tro.switches) == 1
|
||||
weiche = tro.switches[0]
|
||||
assert weiche.to1_destinations == 2
|
||||
assert weiche.direction_target == "#TRO01.nInDirection"
|
||||
assert [(e.via, e.targets) for e in tro.switch_exits] == [
|
||||
("TRO02", ["SYS2", "TRO02"]), ("TRO03", ["TRO03"])]
|
||||
|
||||
|
||||
def test_mehrere_weichen_keine_geratene_aufteilung():
|
||||
"""2Sep2Swi (mubea): nTo1Destinations bleibt offen, Richtung je Weiche."""
|
||||
ctx = build_context(load_document(FIXTURES / "mubea_tro.json"), "mubea_tro.json")
|
||||
tro = next(t for t in ctx.tros if t.type == "2Sep2Swi")
|
||||
assert [w.to1_destinations for w in tro.switches] == [None, None]
|
||||
assert [w.direction_target for w in tro.switches] == [
|
||||
f"#{tro.id}.nInDirection1", f"#{tro.id}.nInDirection2"]
|
||||
|
||||
|
||||
def test_separatoren_aus_dem_layout(kontext):
|
||||
"""Mehr Separatoren laut Typ als im Layout -> '?' statt Fehler."""
|
||||
assert [s.layout for s in _by_id(kontext, "TRO01").separators] == ["0040"]
|
||||
assert [s.layout for s in _by_id(kontext, "TRO03").separators] == ["?"]
|
||||
|
||||
|
||||
def test_vario_nur_beim_vario_typ(kontext):
|
||||
vario = _by_id(kontext, "TRO03")
|
||||
assert vario.has_vario
|
||||
assert list(vario.vario_sensors) == [
|
||||
"stSenCarrInPos", "stSenFinger", "stSenLastPos", "stSenJam"]
|
||||
assert _by_id(kontext, "TRO02").vario_sensors == {}
|
||||
|
||||
|
||||
def test_heute_sind_alle_namen_offen(kontext):
|
||||
"""Bis zu den Namensregeln (Schritt 4) ist jeder Name ein TODO-Platzhalter."""
|
||||
namen: list[SclName] = []
|
||||
for tro in kontext.tros:
|
||||
namen += [tro.main_tro, tro.safety, tro.motor_protection, tro.vario_motor,
|
||||
tro.machine_state, tro.priority_udt, tro.prio_dir2, tro.carousel_run]
|
||||
namen += [j.sensor for j in tro.jam_exits]
|
||||
assert all(j.sensor is None for j in tro.jam_entries)
|
||||
namen += [n for s in tro.separators for n in (s.no, s.sensor, s.stopper)]
|
||||
namen += [n for w in tro.switches for n in (w.no, w.valve, w.jam_db)]
|
||||
namen += [j.db for j in tro.jam_entries + tro.jam_exits]
|
||||
namen += list(tro.vario_sensors.values())
|
||||
assert namen and all(n.todo for n in namen)
|
||||
|
||||
|
||||
def test_flow_order_kreislauf_ohne_eingang():
|
||||
analysis = Analysis(tros=[
|
||||
_tro("TRO02", "1Sep", predecessors=["TRO01"], successors=["TRO01"]),
|
||||
_tro("TRO01", "1Sep", predecessors=["TRO02"], successors=["TRO02"]),
|
||||
])
|
||||
geordnet, grund = flow_order(analysis.tros)
|
||||
assert [t.tro_id for t in geordnet] == ["TRO01", "TRO02"]
|
||||
assert grund.startswith("Kreislauf ohne Eingang")
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Golden-File-Tests fuer scl_skeleton.py - die komplette FB_Main-Ausgabe.
|
||||
|
||||
Schritt 1 aus doc/SCL_Templates.md (§8.3, §11): die heutige Ausgabe wird
|
||||
festgeschrieben, bevor der Generator auf Templates umgebaut wird. Solange kein
|
||||
Template eingeschaltet ist, muss jede SCL byte-gleich zur Referenz bleiben.
|
||||
|
||||
Eingaben: tests/fixtures/scl/<name>_tro.json - feste Kopien der TRO-JSON aus
|
||||
tro_flow --json, damit nur scl_skeleton geprueft wird und nicht tro_flow.
|
||||
Erwartung: tests/fixtures/scl/<name>_FB_Main.scl, Zeitstempel maskiert.
|
||||
|
||||
Eine gewollte Aenderung der Ausgabe (z. B. ein Template wird eingeschaltet):
|
||||
Diff pruefen, dann die Referenzen neu schreiben mit
|
||||
|
||||
set SKEL_UPDATE_GOLDEN=1 & python -m pytest tests/test_scl_skeleton_golden.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import scl_skeleton
|
||||
from scl_skeleton import TEMPLATE_DIR, SclTemplates, load_document, render
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
|
||||
|
||||
# mubea: alle fuenf dort vorkommenden Typen, auch 2Sep2Swi (ohne Template);
|
||||
# HundM05: die Referenzanlage der TRO-Templates.
|
||||
PROJEKTE = ["mubea", "HundM05"]
|
||||
|
||||
# Die einzige Zeile, die sich von Lauf zu Lauf aendert.
|
||||
_ZEITSTEMPEL = re.compile(r"^// Erzeugt: .*$", re.MULTILINE)
|
||||
_MASKE = "// Erzeugt: <maskiert>"
|
||||
|
||||
|
||||
def _maskiert(text: str) -> str:
|
||||
return _ZEITSTEMPEL.sub(_MASKE, text)
|
||||
|
||||
|
||||
def _aktualisieren() -> bool:
|
||||
return os.environ.get("SKEL_UPDATE_GOLDEN") == "1"
|
||||
|
||||
|
||||
def _vergleichen(ist: str, referenz: Path) -> None:
|
||||
if _aktualisieren():
|
||||
referenz.write_text(ist, encoding="utf-8", newline="\n")
|
||||
pytest.skip(f"Referenz neu geschrieben: {referenz.name}")
|
||||
assert referenz.is_file(), (
|
||||
f"{referenz.name} fehlt - mit SKEL_UPDATE_GOLDEN=1 erzeugen")
|
||||
soll = referenz.read_text(encoding="utf-8")
|
||||
if ist != soll:
|
||||
# Erste abweichende Zeile nennen; der volle Diff ist bei ~900 Zeilen
|
||||
# in der pytest-Ausgabe nicht lesbar.
|
||||
ist_z, soll_z = ist.splitlines(), soll.splitlines()
|
||||
for nr, (a, b) in enumerate(zip(ist_z, soll_z), start=1):
|
||||
if a != b:
|
||||
pytest.fail(f"{referenz.name} Zeile {nr}:\n soll: {b}\n ist : {a}")
|
||||
pytest.fail(f"{referenz.name}: {len(ist_z)} statt {len(soll_z)} Zeilen")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", PROJEKTE)
|
||||
def test_render_wie_referenz(name: str) -> None:
|
||||
"""render() liefert byte-gleich die festgeschriebene SCL."""
|
||||
quelle = f"{name}_tro.json"
|
||||
document = load_document(FIXTURES / quelle)
|
||||
_vergleichen(_maskiert(render(document, quelle)), FIXTURES / f"{name}_FB_Main.scl")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", PROJEKTE)
|
||||
def test_render_mit_templates_wie_referenz(name: str) -> None:
|
||||
"""--templates: TROs mit Template aus dem Template, der Rest generisch.
|
||||
|
||||
Eigene Referenz <name>_FB_Main_templates.scl. Eine Aenderung an einem
|
||||
Template zeigt sich hier als Diff - pruefen, dann neu schreiben.
|
||||
"""
|
||||
quelle = f"{name}_tro.json"
|
||||
document = load_document(FIXTURES / quelle)
|
||||
text = render(document, quelle, templates=SclTemplates(TEMPLATE_DIR))
|
||||
_vergleichen(_maskiert(text), FIXTURES / f"{name}_FB_Main_templates.scl")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", PROJEKTE)
|
||||
def test_cli_schreibt_referenz(name: str, tmp_path: Path, monkeypatch) -> None:
|
||||
"""Der CLI-Weg: Exit 0, Dateiname <name>_FB_Main.scl, Inhalt wie render().
|
||||
|
||||
Das ist der Vertrag, auf den sich ein Workflow verlaesst (doc/SCL_Templates.md §12).
|
||||
"""
|
||||
if _aktualisieren():
|
||||
pytest.skip("Referenzen werden nur ueber test_render_wie_referenz geschrieben")
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
|
||||
exit_code = scl_skeleton.main(["--json", str(FIXTURES / f"{name}_tro.json")])
|
||||
assert exit_code == 0
|
||||
|
||||
ausgabe = tmp_path / f"{name}_FB_Main.scl"
|
||||
assert ausgabe.is_file()
|
||||
soll = (FIXTURES / f"{name}_FB_Main.scl").read_text(encoding="utf-8")
|
||||
assert _maskiert(ausgabe.read_text(encoding="utf-8")) == soll
|
||||
|
||||
|
||||
def test_cli_templates_und_strict(tmp_path: Path, monkeypatch) -> None:
|
||||
"""--templates schreibt die Template-Referenz; --strict bricht ab, solange
|
||||
ein FB-Typ der Anlage (mubea: u. a. 2Sep2Swi) kein Template hat."""
|
||||
if _aktualisieren():
|
||||
pytest.skip("Referenzen werden nur ueber die render-Tests geschrieben")
|
||||
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
|
||||
json_file = str(FIXTURES / "mubea_tro.json")
|
||||
|
||||
assert scl_skeleton.main(["--json", json_file, "--templates"]) == 0
|
||||
soll = (FIXTURES / "mubea_FB_Main_templates.scl").read_text(encoding="utf-8")
|
||||
ist = (tmp_path / "mubea_FB_Main.scl").read_text(encoding="utf-8")
|
||||
assert _maskiert(ist) == soll
|
||||
|
||||
(tmp_path / "mubea_FB_Main.scl").unlink()
|
||||
assert scl_skeleton.main(["--json", json_file, "--templates", "--strict"]) == 1
|
||||
assert not (tmp_path / "mubea_FB_Main.scl").exists()
|
||||
|
||||
assert scl_skeleton.main(["--json", json_file, "--templates",
|
||||
str(tmp_path / "gibt_es_nicht")]) == 1
|
||||
|
||||
|
||||
def test_todo_marker_bleiben_zaehlbar() -> None:
|
||||
"""Die Zahl der offenen Stellen ist eine Qualitaetsgroesse im Workflow.
|
||||
|
||||
Sie darf sich nur mit Absicht aendern (dann Referenz neu schreiben).
|
||||
"""
|
||||
for name in PROJEKTE:
|
||||
text = (FIXTURES / f"{name}_FB_Main.scl").read_text(encoding="utf-8")
|
||||
assert text.count(scl_skeleton.TODO) > 0, name
|
||||
@@ -0,0 +1,352 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Rundlauf der FB-Templates gegen die realen Aufrufstellen (doc/SCL_Templates.md §8.1).
|
||||
|
||||
Je Template: Kontext mit den ECHTEN Werten der Referenz-Aufrufstelle bauen
|
||||
(TRO-Nummer, Sensoren, Ventile, Staubereiche ...), Template rendern und mit dem
|
||||
REGION-Block der Referenz in doc/TRO_Katalog/scl_templates/ vergleichen.
|
||||
Verglichen wird normalisiert: Kommentare und Leerzeilen weg, Leerraum
|
||||
zusammengefasst. Stimmt das, hat die Parametrisierung nichts verloren -
|
||||
gleiche Zeilen, gleiche Reihenfolge, gleiche Werte.
|
||||
|
||||
Wird eine Referenzdatei durch eine neuere Aufrufstelle ersetzt, zeigt dieser
|
||||
Test genau die Zeilen, denen das Template folgen muss.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from model import Analysis, DerivedTro, TroDocument
|
||||
from scl_context import SclName, SclTro, build_context
|
||||
from scl_skeleton import TEMPLATE_DIR, SclTemplates, load_document, render
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
REFERENZ = ROOT / "doc" / "TRO_Katalog" / "scl_templates"
|
||||
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "scl"
|
||||
|
||||
|
||||
def _name(wert: str) -> SclName:
|
||||
"""Ein bekannter Name aus der Referenz - also nicht mehr offen."""
|
||||
return SclName(value=wert, todo=False)
|
||||
|
||||
|
||||
def _normalisiert(text: str) -> list[str]:
|
||||
zeilen = []
|
||||
for zeile in text.splitlines():
|
||||
zeile = re.sub(r"//.*$", "", zeile) # Kommentar
|
||||
zeile = re.sub(r"\s+", " ", zeile).strip() # Leerraum
|
||||
# "a:=b" und "a := b" sind dasselbe SCL (Referenzen schreiben beides)
|
||||
zeile = re.sub(r"\s*(:=|=>)\s*", r" \1 ", zeile)
|
||||
if zeile:
|
||||
zeilen.append(zeile)
|
||||
return zeilen
|
||||
|
||||
|
||||
def _referenz_region(datei: str) -> list[str]:
|
||||
"""Vom ersten 'REGION' bis zum letzten 'END_REGION' der Referenzdatei."""
|
||||
zeilen = (REFERENZ / datei).read_text(encoding="utf-8-sig").splitlines()
|
||||
anfang = next(i for i, z in enumerate(zeilen) if z.startswith("REGION "))
|
||||
ende = max(i for i, z in enumerate(zeilen) if z.strip() == "END_REGION")
|
||||
return _normalisiert("\n".join(zeilen[anfang:ende + 1]))
|
||||
|
||||
|
||||
def _jam(jam, db: str) -> None:
|
||||
"""Staubereich mit bekanntem DB (Traeger und Daten wie ueblich darin)."""
|
||||
jam.db = _name(db)
|
||||
jam.carriers = _name(f"{db}.arCarrier")
|
||||
jam.data = _name(f"{db}.stData")
|
||||
|
||||
|
||||
def _scanner(sc, interface: str, tag: str, hw_results: str) -> None:
|
||||
sc.interface = _name(interface)
|
||||
sc.tag = _name(tag)
|
||||
sc.hw_results = _name(hw_results)
|
||||
|
||||
|
||||
def _kontext(tro_id: str, type_name: str, vorgaenger: list[str],
|
||||
nachfolger: list[str], separators: list[str] | None = None,
|
||||
hosts: list[str] | None = None) -> SclTro:
|
||||
"""Kontext eines TRO mit der Topologie der Referenzstelle."""
|
||||
tro = DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id,
|
||||
hosts=hosts or [], separators=separators or ["0001"],
|
||||
scanners=[], reason="Referenz",
|
||||
predecessors=vorgaenger, successors=nachfolger)
|
||||
document = TroDocument.from_analysis(Analysis(tros=[tro]), source="referenz")
|
||||
return build_context(document, "referenz").tros[0]
|
||||
|
||||
|
||||
def _vergleich(ist: list[str], soll: list[str]) -> None:
|
||||
for nr, (a, b) in enumerate(zip(ist, soll), start=1):
|
||||
assert a == b, f"Zeile {nr}:\n Referenz: {b}\n Template: {a}"
|
||||
assert len(ist) == len(soll), (
|
||||
f"{len(ist)} statt {len(soll)} Zeilen; zuviel/fehlend: "
|
||||
f"{ist[len(soll):] or soll[len(ist):]}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def templates() -> SclTemplates:
|
||||
return SclTemplates(TEMPLATE_DIR)
|
||||
|
||||
|
||||
def test_1sep1swi_wie_tro107(templates):
|
||||
"""FB_ILS_MTRO_1Sep1Swi gegen TRO 107 (=A01+UH01-KF00 FB_Main)."""
|
||||
t = _kontext("TRO107", "1Sep1Swi", ["TRO106"], ["TRO108", "TRO110"])
|
||||
t.region_title = "TRO 107(1Sep1Swi)"
|
||||
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep107.1")
|
||||
sep.sensor = _name("cInBG3306")
|
||||
sep.stopper = _name("MB3306")
|
||||
|
||||
t.jam_exits[0].sensor = _name("cInBG3305")
|
||||
t.jam_exits[1].sensor = _name("cInBG3330")
|
||||
|
||||
weiche = t.switches[0]
|
||||
weiche.no = _name("cSwi107.1")
|
||||
weiche.valve = _name("MB3304")
|
||||
weiche.to1_destinations = 3 # Referenz: 3 Ziele bei 2 Abgaengen (§13)
|
||||
weiche.jam_db = _name('"DB_JamArea0102".stJam0102_1')
|
||||
|
||||
t.prio_dir2 = _name("TRUE")
|
||||
t.priority_udt = _name("udt_TRO107")
|
||||
t.main_tro = _name("cMainTro107")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
|
||||
t.carousel_run = _name("#fbCarousel0102.xOutConveyorFullSpeed")
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
|
||||
t.jam_entries[0].db = _name('"DB_JamArea0102".stJam0102_1')
|
||||
t.jam_exits[0].db = _name('"DB_JamArea0103".stJam0103_1')
|
||||
t.jam_exits[1].db = _name('"DB_JamArea0102".stJam0102_2')
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep1Swi.scl"))
|
||||
|
||||
|
||||
def test_1sep_wie_tro100(templates):
|
||||
"""FB_ILS_MTRO_1Sep gegen TRO 100 (=A01+UH01-KF00 FB_Main)."""
|
||||
t = _kontext("TRO100", "1Sep", ["TRO099"], ["TRO101"])
|
||||
t.region_title = "TRO 100 (1Sep)"
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep100.1")
|
||||
sep.sensor = _name("cInBG3242")
|
||||
sep.stopper = _name("MB3242")
|
||||
t.jam_exits[0].sensor = _name("cInBG3243")
|
||||
t.main_tro = _name("cMainTro100")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
|
||||
t.carousel_run = _name("#fbCarousel0101.xOutConveyorFullSpeed")
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
|
||||
_jam(t.jam_entries[0], '"DB_JamArea0100".stJam0100_1')
|
||||
_jam(t.jam_exits[0], '"DB_JamArea0101".stJam0101_1')
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep.scl"))
|
||||
|
||||
|
||||
def test_vario_wie_tro115(templates):
|
||||
"""FB_ILS_MTRO_Vario gegen TRO 115."""
|
||||
t = _kontext("TRO115", "Vario", ["TRO114"], ["TRO116"])
|
||||
t.region_title = "TRO 115 (Vario)"
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep115.1")
|
||||
sep.sensor = _name("cInBG3025")
|
||||
sep.stopper = _name("MB3025")
|
||||
for name, wert in (("stSenCarrInPos", "cInBG3026"), ("stSenFinger", "cInBG3027"),
|
||||
("stSenLastPos", "cInBG3000"), ("stSenJam", "cInBG3001")):
|
||||
t.vario_sensors[name] = _name(wert)
|
||||
t.is_border = _name("TRUE")
|
||||
t.main_tro = _name("cMainTro115")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
|
||||
t.motor_protection = _name('"FC0145"')
|
||||
t.error_quit = _name('"SF0002"')
|
||||
t.automatic = _name('#t_Automatik_OF1 AND "SF0120"')
|
||||
_jam(t.jam_in, '"DB_JamArea4104".stJam4104_1')
|
||||
_jam(t.jam_out, '"DB_JamArea4103".stJam4103_1')
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
|
||||
t.vario_motor = _name("MA0145")
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario.scl"))
|
||||
|
||||
|
||||
def test_vario_workstation_wie_tro421(templates):
|
||||
"""FB_ILS_MTRO_Vario_workStation gegen die Aufrufstelle TRO 421 (UH04)."""
|
||||
t = _kontext("TRO421", "Vario_workStation", ["TRO420"], ["TRO422"])
|
||||
t.region_title = "TRO 421 (Vario)"
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep421.1")
|
||||
sep.sensor = _name("cInBG3326")
|
||||
sep.stopper = _name("MB3326")
|
||||
for name, wert in (("stSenFinger", "cInBG3315"), ("stSenLastPos", "cInBG3317"),
|
||||
("stSenCarrInPos", "cInBG3324"), ("stSenJam", "cInBG3320")):
|
||||
t.vario_sensors[name] = _name(wert)
|
||||
t.work_station.loaded_error_sensor = _name("cInBG3325")
|
||||
t.work_station.station_sensor = _name("cInBG3314")
|
||||
t.work_station.station_release = _name('#"rTrig_Workstation 1".CLK')
|
||||
t.main_tro = _name("cMainTro421")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMain.Q')
|
||||
t.automatic = _name("#t_automatik")
|
||||
t.release = '"DB_TRO_Test".TRO421'
|
||||
t.motor_protection = _name('"FC0062"')
|
||||
_jam(t.jam_in, '"DB_JamArea4004".stJam4004_2')
|
||||
_jam(t.jam_out, '"DB_JamArea4005".stJam4005_1')
|
||||
t.machine_state = _name("#stInOutControlUnitCabinet")
|
||||
t.vario_motor = _name("MA0062")
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_Vario_workStation_CallSite.scl"))
|
||||
|
||||
|
||||
def test_1sep2swi_wie_tro267(templates):
|
||||
"""FB_ILS_MTRO_1Sep2Swi gegen TRO 267 (Scanner BX2100, 3 Abgaenge)."""
|
||||
t = _kontext("TRO267", "1Sep2Swi", ["TRO266"], ["TRO268", "TRO269", "TRO270"])
|
||||
t.region_title = "TRO 267 (1Sep2Switch)+Scanner(BX2100)"
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep267.1")
|
||||
sep.sensor = _name("cInBG4440")
|
||||
sep.part_sensor = _name("cInBG4465")
|
||||
sep.stopper = _name("MB4440")
|
||||
for jam, sensor in zip(t.jam_exits, ("cInBG4444", "cInBG4455", "cInBG4454")):
|
||||
jam.sensor = _name(sensor)
|
||||
for weiche, ventil in zip(t.switches, ("MB4444", "MB4455")):
|
||||
weiche.valve = _name(ventil)
|
||||
weiche.to1_destinations = 3 # Referenz: 3 je Weiche
|
||||
t.switches[0].no = _name("cSwi267.1")
|
||||
_scanner(t.scanners[0], "cScannerBX2100", "bx2100",
|
||||
"=A01+UC021-BX2100~Ergebnisse_–_64_Bytes_1")
|
||||
t.main_tro = _name("cMainTro267")
|
||||
t.safety = _name('"cTodoTRUE"')
|
||||
t.release = '"DB_TRO_Test".TRO267'
|
||||
t.carousel_run = _name("#fbCarousel1110.xOutConveyorFullSpeed")
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
|
||||
_jam(t.jam_entries[0], '"DB_JamArea1110".stJam1110_4')
|
||||
for jam, nr in zip(t.jam_exits[:2], ("5001", "5000")): # Uebergaben
|
||||
jam.carriers = _name(f"#arCarrierHandoverJam{nr}")
|
||||
jam.data = _name(f"#stJamDataHandoverJam{nr}")
|
||||
_jam(t.jam_exits[2], '"DB_JamArea1110".stJam1110_5')
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep2Swi.scl"))
|
||||
|
||||
|
||||
def test_1sep_sscc_wie_tro105(templates):
|
||||
"""FB_ILS_MTRO_1Sep_SSCC gegen TRO 105 (Scanner BX1104, SSCC-Scanner BX1107)."""
|
||||
t = _kontext("TRO105", "1Sep_SSCC", ["TRO104"], ["TRO106"])
|
||||
t.region_title = "TRO 105 (1Sep)+Scanner"
|
||||
sep = t.separators[0]
|
||||
sep.no = _name("cSep105.1")
|
||||
sep.sensor = _name("cInBG3280")
|
||||
sep.stopper = _name("MB3280")
|
||||
t.jam_exits[0].sensor = _name("cInBG3281")
|
||||
_scanner(t.scanners[0], "cScannerBX1104", "bx1104",
|
||||
"BX1104~Ergebnisse_–_64_Bytes_1")
|
||||
_scanner(t.sscc.scanner, "#stCognexIntfTro105", "bx1107",
|
||||
"BX1107~Ergebnisse_–_64_Bytes_1")
|
||||
t.sscc.end_sensor = _name("cInBG3281")
|
||||
t.sscc.hmi = _name("SSCC1107")
|
||||
t.main_tro = _name("cMainTro105")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF1.Q')
|
||||
t.carousel_run = _name("TRUE")
|
||||
t.release = ('"FB_CommunicationWCSBL_DB".fbTeleHandler.xOutConnectionActive '
|
||||
'OR "IBN".xFahrenOhneTcpPartner')
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF1")
|
||||
_jam(t.jam_entries[0], '"DB_JamArea0104".stJam0104_3')
|
||||
_jam(t.jam_exits[0], '"DB_JamArea0106".stJam0106_2')
|
||||
t.correction_station = _name("0")
|
||||
|
||||
ist = _normalisiert(templates.render(t))
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_1Sep_SSCC.scl"))
|
||||
|
||||
|
||||
def test_pinstore_auto_wie_tro208(templates):
|
||||
"""FB_ILS_MTRO_PinStore_Auto gegen BLOCK 1.1.1 (TRO 208).
|
||||
|
||||
Die Referenz hat 19 Linien, schreibt aber nur die Linien 1-3 aus (bei
|
||||
"Separator Store Out" nur Linie 1, "weitere analog"). Der Kontext hat
|
||||
deshalb 19 Linien; was die Referenz abkuerzt, wird vor dem Vergleich aus
|
||||
der Template-Ausgabe genommen.
|
||||
"""
|
||||
linien = [f"{n:04d}" for n in range(1, 20)]
|
||||
t = _kontext("TRO208", "PinStore_Auto", ["TRO207"], ["TRO209"],
|
||||
separators=linien, hosts=linien)
|
||||
t.region_title = "BLOCK 1.1.1 (TRO 208)"
|
||||
st = t.store
|
||||
st.encoder = _name("#fbEncoderGF2200")
|
||||
st.dest = _name("cDest11100")
|
||||
st.conv_stations = _name("cHooksSorterMaxPin111")
|
||||
st.stations_max = _name("cStationsMax111")
|
||||
st.threshold = _name("cNoPositionsBlock1")
|
||||
st.reference_sensor = _name("cInBG3001")
|
||||
st.station = _name("111")
|
||||
st.quit_local = _name('"SF0121" OR "SF0117"')
|
||||
st.test_target = _name('"IBN".nTestTarget111')
|
||||
st.run_in = _name("#fbSortConv1101.xOutConveyorFullSpeed")
|
||||
st.run_out = _name("#fbExitConv1103.xOutConveyorFullSpeed")
|
||||
st.db = _name('"DB_Storage1.1.1"')
|
||||
st.removal = _name('"DB_HandshakeStorage".Str_StorageRemoval.'
|
||||
'arrudt_StorageRemovalHandshakeTROsLvl1[1]')
|
||||
st.conv_db = _name('"DB_ConveyorStations".stConveyorStations111')
|
||||
st.pin_sensor = _name("cInBG3000")
|
||||
st.pin_stopper = _name("MB3000")
|
||||
for linie, (laenge, sensor, stau, stopper) in zip(st.lines, (
|
||||
("4", "cInBG3270", "cInBG3030", "MB3270"),
|
||||
('"cStorage111ExitJam2"', "cInBG3271", "cInBG3031", "MB3271"),
|
||||
('"cStorage111ExitJam3"', "cInBG3272", "cInBG3032", "MB3272"))):
|
||||
linie.jam_length = _name(laenge)
|
||||
linie.sensor = _name(sensor)
|
||||
linie.jam_sensor = _name(stau)
|
||||
linie.stopper = _name(stopper)
|
||||
_scanner(t.scanners[0], "cScannerBX2104", "bx2104",
|
||||
"=A01+UH02-BX2104~Ergebnisse_–_64_Bytes_1")
|
||||
t.safety = _name('"DB_InterfaceSafety".stFromSafety.EStopMainOF2.Q')
|
||||
t.release = '"DB_TRO_Test".TRO208'
|
||||
t.machine_state = _name("#stInOutControlUnitCabinetOF2")
|
||||
_jam(t.jam_in, '"DB_JamArea1101".stJam1101_2')
|
||||
_jam(t.jam_out, '"DB_JamArea1103".stJam1103_1')
|
||||
t.correction_station = _name("1")
|
||||
|
||||
ist, abgekuerzt = [], False
|
||||
for zeile in _normalisiert(templates.render(t)):
|
||||
treffer = re.fullmatch(r"REGION Sep Store (\d+)", zeile)
|
||||
if treffer:
|
||||
abgekuerzt = int(treffer.group(1)) > 3
|
||||
if abgekuerzt: # REGION Sep Store 4..19
|
||||
abgekuerzt = zeile != "END_REGION"
|
||||
continue
|
||||
if re.search(r"arInJamSensor\[(\d+)\]", zeile) and \
|
||||
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 3:
|
||||
continue
|
||||
if re.search(r"arOutSeparatorStore\[(\d+)\]", zeile) and \
|
||||
int(re.search(r"\[(\d+)\]", zeile).group(1)) > 1:
|
||||
continue
|
||||
ist.append(zeile)
|
||||
_vergleich(ist, _referenz_region("FB_ILS_MTRO_PinStore_Auto.scl"))
|
||||
|
||||
|
||||
def test_template_ohne_bekannte_namen_hat_nur_markierte_luecken(templates):
|
||||
"""Mit den heutigen Platzhaltern: jede offene Stelle traegt den TODO-Marker."""
|
||||
t = _kontext("TRO07", "1Sep1Swi", ["SYS1"], ["TRO08", "TRO09"])
|
||||
text = templates.render(t)
|
||||
for zeile in text.splitlines():
|
||||
if "____" in zeile.split("//")[0]:
|
||||
assert "TODO(E-Planung)" in zeile, zeile
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["mubea", "HundM05"])
|
||||
def test_jedes_template_rendert_fuer_echte_anlagen(name, templates):
|
||||
"""StrictUndefined: ein Tippfehler im Template faellt hier auf, nicht erst beim Kunden."""
|
||||
document = load_document(FIXTURES / f"{name}_tro.json")
|
||||
text = render(document, f"{name}_tro.json", templates=templates)
|
||||
regionen = [z for z in text.splitlines() if z.startswith("REGION ")]
|
||||
assert len(regionen) == len(document.tros)
|
||||
kopf = next(z for z in text.splitlines() if z.startswith("// Aus Template :"))
|
||||
for fb in {t.fb_block for t in document.tros if templates.has(t.fb_block)}:
|
||||
assert fb in kopf
|
||||
|
||||
|
||||
def test_strikter_undefined_bricht_ab(tmp_path):
|
||||
"""Unbekannte Variable im Template -> Fehler statt leerer SCL-Stelle."""
|
||||
(tmp_path / "FB_X.scl.j2").write_text("REGION {{ t.gibt_es_nicht }}\n", encoding="utf-8")
|
||||
t = _kontext("TRO01", "1Sep", [], [])
|
||||
t.fb = "FB_X"
|
||||
with pytest.raises(Exception):
|
||||
SclTemplates(tmp_path).render(t)
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer die Mouseover-Texte des --track-Graphen (material_flow.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from material_flow import TrackNode, _tooltip_context, _track_node_tooltip
|
||||
from test_kreisel_junctions import KREISEL, SEPARATOREN, WEICHEN, _el, _kreisel
|
||||
|
||||
|
||||
def _anlage() -> list:
|
||||
kreisel = [_kreisel(*k) for k in KREISEL]
|
||||
for k in kreisel:
|
||||
k.name = f"Kreisel{int(k.teile_id)}" # wie im Export
|
||||
kreisel[2].track_ids = ["0015"] # Kreisel 0003 laut Export
|
||||
kreisel[4].track_ids = ["0025", "0026", "0020"] # Kreisel 0005
|
||||
seps = []
|
||||
for teile_id, pos in SEPARATOREN:
|
||||
zuordnung = {"0015": "0003-L", "0010": "0009-L"}.get(teile_id, "")
|
||||
seps.append(_el(teile_id, "Separator", pos, Zuordnung=zuordnung))
|
||||
weichen = [_el(i, "Weiche", p) for i, p in WEICHEN]
|
||||
weichen[0].merkmale.update({"Kreisel A": "0009", "Kreisel B": "0001"})
|
||||
beladung = _el("0020", "Beladung", (14374.25, 1186.94))
|
||||
return kreisel + seps + weichen + [beladung]
|
||||
|
||||
|
||||
def _tooltip(elements: list, teile_id: str, kind: str) -> list[str]:
|
||||
ctx = _tooltip_context(elements)
|
||||
el = next(e for e in elements if e.teile_id == teile_id)
|
||||
return _track_node_tooltip(TrackNode(teile_id, kind, el), ctx).split("\\n")
|
||||
|
||||
|
||||
def test_separator_shows_zuordnung_kette_and_geometry_conflict() -> None:
|
||||
lines = _tooltip(_anlage(), "0015", "Separator")
|
||||
assert "Zuordnung: Kreisel3 (0003) Bahn L" in lines
|
||||
assert "TrackIds-Kette: Kreisel3 (0003)" in lines
|
||||
assert "Geometrie: liegt auf Kreisel2 (0002) Bahn R" in lines
|
||||
assert "! Zuordnung Kreisel 0003, liegt aber auf Kreisel 0002" in lines
|
||||
|
||||
|
||||
def test_separator_without_conflict_has_no_warning() -> None:
|
||||
lines = _tooltip(_anlage(), "0010", "Separator")
|
||||
assert "Geometrie: liegt auf Kreisel9 (0009) Bahn L" in lines
|
||||
assert not any(line.startswith("! Zuordnung") for line in lines)
|
||||
|
||||
|
||||
def test_weiche_shows_both_kreisel() -> None:
|
||||
lines = _tooltip(_anlage(), "0022", "Weiche")
|
||||
assert "Kreisel A: Kreisel9 (0009)" in lines
|
||||
assert "Kreisel B: Kreisel1 (0001)" in lines
|
||||
assert "Geometrie: Beruehrpunkt Kreisel 0001 / 0009" in lines
|
||||
|
||||
|
||||
def test_beladung_shows_its_kreisel() -> None:
|
||||
lines = _tooltip(_anlage(), "0020", "Beladung")
|
||||
assert "TrackIds-Kette: Kreisel5 (0005)" in lines
|
||||
assert lines[-1].startswith("Geometrie: naechster Kreisel Kreisel5 (0005)")
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer lib/tro_ids.py: ID-Datei lesen/schreiben, TROs ueber die Lage
|
||||
wiederfinden (doc/Konzept_Layout_Hardware_SCL.md, Abschnitt TRO-IDs)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import pytest
|
||||
|
||||
from tro_ids import (
|
||||
IdEntry,
|
||||
TroIds,
|
||||
assign_ids,
|
||||
insert_entries,
|
||||
load_ids,
|
||||
other_sections,
|
||||
render_ids,
|
||||
render_new_entries,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeTro:
|
||||
separators: list[str]
|
||||
type_name: str = "1Sep"
|
||||
label: str = ""
|
||||
tro_id: str = ""
|
||||
hosts: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
ANCHORS = {
|
||||
"0001": (0.0, 0.0),
|
||||
"0002": (1000.0, 0.0),
|
||||
"0003": (2000.0, 0.0),
|
||||
"0004": (2000.0, 500.0),
|
||||
}
|
||||
|
||||
|
||||
def _tros(*seps: list[str]) -> list[FakeTro]:
|
||||
return [FakeTro(separators=list(s)) for s in seps]
|
||||
|
||||
|
||||
def test_ohne_eintraege_wird_durchnummeriert():
|
||||
tros = _tros(["0001"], ["0002"], ["0003"])
|
||||
result = assign_ids(tros, ANCHORS, [])
|
||||
assert result.renumbered
|
||||
assert [t.tro_id for t in tros] == ["TRO01", "TRO02", "TRO03"]
|
||||
|
||||
|
||||
def test_wiederfinden_ueber_lage_auch_umbenannt():
|
||||
# Neuer Export: andere TeileIds, gleiche Lage, andere Reihenfolge
|
||||
anchors = {"0101": (2000.0, 0.0), "0102": (0.0, 0.0), "0103": (1000.0, 30.0)}
|
||||
tros = _tros(["0101"], ["0102"], ["0103"])
|
||||
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("K0102_1", 1000.0, 0.0),
|
||||
IdEntry("TRO03", 2000.0, 0.0)]
|
||||
result = assign_ids(tros, anchors, entries)
|
||||
assert [t.tro_id for t in tros] == ["TRO03", "TRO01", "K0102_1"]
|
||||
assert not result.new and not result.missing
|
||||
|
||||
|
||||
def test_neue_tros_naechste_freie_nummer_weggefallene_nicht_wiederverwendet():
|
||||
tros = _tros(["0001"], ["0003"])
|
||||
entries = [IdEntry("TRO01", 0.0, 0.0), IdEntry("TRO02", 1000.0, 0.0)]
|
||||
result = assign_ids(tros, ANCHORS, entries)
|
||||
assert [t.tro_id for t in tros] == ["TRO01", "TRO03"]
|
||||
assert result.new == ["TRO03"]
|
||||
assert [e.tro_id for e in result.missing] == ["TRO02"]
|
||||
|
||||
|
||||
def test_ausserhalb_toleranz_gilt_als_neu():
|
||||
tros = _tros(["0001"])
|
||||
entries = [IdEntry("TRO01", 150.0, 0.0)]
|
||||
result = assign_ids(tros, ANCHORS, entries, tolerance=100.0)
|
||||
assert tros[0].tro_id == "TRO02"
|
||||
assert result.missing[0].tro_id == "TRO01"
|
||||
assert assign_ids(tros, ANCHORS, entries, tolerance=200.0).kept[0][0] == "TRO01"
|
||||
|
||||
|
||||
def test_zusammengelegte_tros_naechster_eintrag_gewinnt():
|
||||
# zwei Separatoren, frueher zwei TROs, jetzt ein 2Sep-TRO
|
||||
tros = _tros(["0003", "0004"])
|
||||
entries = [IdEntry("TRO05", 2000.0, 0.0), IdEntry("TRO06", 2000.0, 510.0)]
|
||||
result = assign_ids(tros, ANCHORS, entries)
|
||||
assert tros[0].tro_id == "TRO05"
|
||||
assert result.merged_into == {"TRO06": "TRO05"}
|
||||
|
||||
|
||||
def test_renumber_ignoriert_eintraege():
|
||||
tros = _tros(["0001"], ["0002"])
|
||||
result = assign_ids(tros, ANCHORS, [IdEntry("X", 1000.0, 0.0)], renumber=True)
|
||||
assert result.renumbered
|
||||
assert [t.tro_id for t in tros] == ["TRO01", "TRO02"]
|
||||
|
||||
|
||||
def test_datei_rundlauf_und_anhaengen(tmp_path):
|
||||
path = tmp_path / "anlage_tro_ids.ini"
|
||||
tros = _tros(["0001"], ["0002"])
|
||||
assign_ids(tros, ANCHORS, [])
|
||||
path.write_text(render_ids(tros, ANCHORS, "anlage.csv")
|
||||
+ "\n[separator]\n; spaeter\n", encoding="utf-8")
|
||||
entries = load_ids(path)
|
||||
assert [(e.tro_id, e.x, e.y) for e in entries] == [
|
||||
("TRO01", 0.0, 0.0), ("TRO02", 1000.0, 0.0)]
|
||||
|
||||
tros = _tros(["0001"], ["0002"], ["0003"])
|
||||
result = assign_ids(tros, ANCHORS, entries)
|
||||
insert_entries(path, render_new_entries(tros, ANCHORS, result.new, "anlage.csv"))
|
||||
assert [e.tro_id for e in load_ids(path)] == ["TRO01", "TRO02", "TRO03"]
|
||||
# neuer Eintrag steht in [tro], nicht im folgenden Abschnitt
|
||||
assert other_sections(path) == "[separator]\n; spaeter"
|
||||
assert TroIds.load(path).exists
|
||||
|
||||
|
||||
@pytest.mark.parametrize("inhalt, meldung", [
|
||||
("[tro]\nTRO01 = 1, 2\nTRO01 = 3, 4\n", "already exists"),
|
||||
("[tro]\nSYS1 = 1, 2\n", "reserviert"),
|
||||
("[tro]\nTRO-1 = 1, 2\n", "SCL-Bezeichner"),
|
||||
("[tro]\nTRO01 = 1\n", "erwartet"),
|
||||
("[andere]\n", "fehlt"),
|
||||
])
|
||||
def test_fehlerhafte_datei(tmp_path, inhalt, meldung):
|
||||
path = tmp_path / "x.ini"
|
||||
path.write_text(inhalt, encoding="utf-8")
|
||||
with pytest.raises(ValueError, match=meldung):
|
||||
load_ids(path)
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests fuer tro_flow.link_tros - Uebergang durch eine Kreisel-Kreisel-Weiche.
|
||||
|
||||
Fixture: Testanlage test_system (dxfmakros results/export.csv) mit einem
|
||||
zusaetzlichen Separator 0022 auf Kreisel 0007. Kreisel 0009 hat nur einen
|
||||
eigenen Separator (0010 vor Weiche 0024 zu Kreisel 0001 mit der Entladung);
|
||||
in Kreisel 0009 gelangt man nur ueber Weiche 0023, die Separator 0011 auf
|
||||
Kreisel 0008 steuert. Die Kreiselketten kennen Weichen nicht als Glied - ohne
|
||||
eigene Weichenkante fehlte TRO(0011) -> TRO(0010), der Ausgang war vom
|
||||
Eingang aus unerreichbar und stattdessen zeigte TRO(0010) rueckwaerts auf
|
||||
TRO(0011).
|
||||
|
||||
TROs werden ueber ihre Separatoren angesprochen, nicht ueber die IDs (die
|
||||
haengen an der ID-Datei der Anlage).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from material_flow import build_graph, read_elements
|
||||
from tro_flow import BOUNDARY_ENTRY, BOUNDARY_EXIT, analyse_tros
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
def _analyse(name: str):
|
||||
path = FIXTURES / name
|
||||
elements = read_elements(path, [])
|
||||
graph = build_graph(elements, path.name, [])
|
||||
return analyse_tros(graph, elements)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def analysis():
|
||||
return _analyse("test_system_sep22_export.csv")
|
||||
|
||||
|
||||
def _tro_mit_sep(analysis, sep_id: str) -> str:
|
||||
return next(t.tro_id for t in analysis.tros if sep_id in t.separators)
|
||||
|
||||
|
||||
def _erreichbar(edges, start: str) -> set[str]:
|
||||
nach: dict[str, list[str]] = {}
|
||||
for a, b in edges:
|
||||
nach.setdefault(a, []).append(b)
|
||||
seen, stack = {start}, [start]
|
||||
while stack:
|
||||
for nxt in nach.get(stack.pop(), []):
|
||||
if nxt not in seen:
|
||||
seen.add(nxt)
|
||||
stack.append(nxt)
|
||||
return seen
|
||||
|
||||
|
||||
def test_weiche_fuehrt_auf_partnerkreisel(analysis):
|
||||
von, nach = _tro_mit_sep(analysis, "0011"), _tro_mit_sep(analysis, "0010")
|
||||
assert (von, nach) in analysis.edges
|
||||
|
||||
|
||||
def test_keine_rueckwaertskante_ueber_die_weiche(analysis):
|
||||
von, nach = _tro_mit_sep(analysis, "0010"), _tro_mit_sep(analysis, "0011")
|
||||
assert (von, nach) not in analysis.edges
|
||||
|
||||
|
||||
def test_ausgang_vom_eingang_erreichbar(analysis):
|
||||
# Nicht fuer test_system_export.csv: dort fehlt auf Kreisel 0007 der
|
||||
# Separator vor der Weiche zu Kreisel 0008 (erst 0022 hier steuert sie) -
|
||||
# die Anlage ist dort physisch nicht durchgaengig.
|
||||
eingang = next(b.tro_id for b in analysis.boundaries if b.type_name == BOUNDARY_ENTRY)
|
||||
ausgang = next(b.tro_id for b in analysis.boundaries if b.type_name == BOUNDARY_EXIT)
|
||||
assert ausgang in _erreichbar(analysis.edges, eingang)
|
||||
Reference in new issue
Block a user