latest changes

This commit is contained in:
2026-08-20 11:38:51 +02:00
parent 8bd0739653
commit d7edc55116
23 changed files with 4769 additions and 44 deletions
+230 -9
View File
@@ -66,11 +66,14 @@ Each CLI tool is invoked through its `bin/<name>.bat` (Windows) / `bin/<name>.sh
3. runs the module with `py` (falling back to `python`).
```bat
bin\material_flow.bat --file mubea.csv --tosvg
bin\tro_flow.bat --file mubea.csv --tosvg --doc
bin\material_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\tro_flow.bat --file mubea.csv --tosvg --use-cords --doc
bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend
```
(`--use-cords` lays both graphs out at the real plant coordinates; drop it for
Graphviz's computed left-to-right arrangement.)
Conventions shared by all three tools:
- `--file` / `--dxf` accept either a bare filename — resolved against
@@ -97,6 +100,27 @@ Reads the CSV export and builds a directed graph of the mechanical objects:
counter-clockwise = left→right).
- **Separator** / **Scanner** → no node of their own; shown via `Zuordnung`
(assignment) on the label of their owning node.
- **Einschleuselement** / **Ausschleuselement** / **Weiche** (from the richer export)
→ no node of their own; they supply the **direction** of the Kreisel↔Strecke edges
(`Ausschleusung` = Kreisel→Strecke at the strecke's *Anfang*, `Einschleusung` =
Strecke→Kreisel at its *Ende*, read from their `Kreisel`/`Strecke`/`Streckenende`
`Merkmale`) and **mark the Weiche** on a Kreisel↔Kreisel link. A strecke whose two
ends touch the *same* Kreisel (return loop) is split across that Kreisel's two lanes.
This makes Kreisel↔Strecke direction deterministic instead of height-inferred.
- **BTMT Be-/Entladestation** (entry/exit points) → one node each, recognised by
`TeileArt`/`Bezeichnung` containing `Beladung`/`BTMT-Beladung` (→ **entry**, a
material *source*) or `Entladung`/`SC_Entladung`/`Abwurf` (→ **exit**, a *sink*).
An entry gets an outgoing edge, an exit an incoming edge, to the object named in
its `Nachbarn`; if the export lists no neighbour (dxfmakros does not export BTMT
yet — see below), the nearest object by 2-D distance is used and a note is
printed. Recognition is **sps_skel-side only** — nothing in dxfmakros changes;
it simply activates when such a row appears in the CSV.
> **Note on BTMT / entry-exit data.** dxfmakros' `EXPORTCSV` currently does **not**
> emit the BTMT stations (their block names match no export pattern; documented as
> *"Export noch offen"*). Until that is addressed, a freshly exported `mubea.csv`
> will contain the 61 conveyor items but **no** BTMT rows, so no entry/exit nodes
> appear. The recognition above is ready for when BTMT rows are present.
Flow direction between conveyor segments is derived from height change
(`Hoehe_Von_mm`/`Hoehe_Bis_mm`) or, failing that, from `Antriebfahrtrichtung`
@@ -109,11 +133,13 @@ attached.
|---|---|---|---|
| `--file` | `NAME` | `export.csv` | CSV input file, resolved against `%SKEL_DATA%`, or a full path. |
| `--tosvg` | — | off | Also render the `.dot` file to `.svg` via Graphviz `dot`. |
| `--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`. |
**Output** (in `%SKEL_RESULTS%`): `<name>_material_flow.dot` (always),
`<name>_material_flow.svg` (with `--tosvg`), `<name>_material_flow.md`
(with `--doc`).
**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`).
**Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed
(Graphviz).
@@ -210,19 +236,195 @@ overridden.
could not be proven (or exceeds `--max-residual`) — resolve with `--offset` or
by raising `--max-residual`.
## 5a. `tro_extract.py` — read the TROs back out of the drawing into JSON
The counter-direction to `tro_annotate.py`:
```
tro_annotate.py CSV + derivation -> markers in the drawing
tro_extract.py drawing -> JSON
```
It reads **only the drawing**, never the CSV. So what comes back is what is
actually in the DXF — including anything adjusted by hand in BricsCAD (markers
moved, type changed via `TRO_EDIT`). That is the point: after a review, the
drawing is the current state, not the export.
It finds the objects `tro_annotate.py` tagged with XDATA (AppID
`SPS_SKEL_TRO`): the `TRO_SYM_<type>` block references (attributes
`ID`/`TYPE`/`ITEMS`/`CONFIDENCE`/`SEPARATORS`, plus `FB_BLOCK` when annotated
with `--fb`), the flow arrows (`"<from>-><to>"`), and the open-point markers
(`"OFFEN:<name>"`). Each TRO also gets `predecessors`/`successors` computed
from the arrows.
Coordinates appear twice: `positionDxf` is the drawing coordinate,
`position` the plant coordinate. The conversion uses the registration written
by `tro_annotate.py` (`<stem>_registration.json`; the `_annotated` suffix and
the file's own `dxf_file` field are both taken into account when locating it).
If no registration is found, `position` is **left out rather than guessed**,
and a warning says so.
Switches: `--dxf NAME` (default `export_annotated.dxf`, looked up in
`%SKEL_RESULTS%`), `--out NAME` (default `<drawing>_tro.json`). Exit codes:
`0` ok, `1` input error, `2` no TRO markers in the drawing.
`fbType` is worth one note: unless the drawing was annotated with `--fb`, the
FB block is not an attribute in the DXF. It is then looked up from
`tro_catalog.py` and marked `"fbTypeFrom": "tro_catalog"` — so a consumer can
tell a value that came out of the drawing from one that was resolved.
## 5b. `scl_skeleton.py` — generate the FB_Main skeleton as SCL
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
scl_skeleton.py JSON -> FB_Main.scl
```
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`.
**It will not compile as delivered, and that is deliberate.** Every value that
has to come from the electrical planning is a visible gap marked
`TODO(E-Planung)` — never an invented value, because a plausible-looking wrong
sensor address is far worse than an empty one. Grep for that marker and nothing
is missed.
| filled from the layout | left as `TODO(E-Planung)` |
|---|---|
| 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…`) |
| destination list per switch exit (graph reachability) | times marked "pruefen" |
| cross-reference to the layout parts (separator numbers, coordinate) | |
Two derivations worth knowing about. **The destination list per switch exit** is
computed by walking the TRO graph from each exit without going back through the
switch — that is exactly what `FC_Direction` needs to map destination to
direction, and it does *not* require the I/O list. And **`nTo1Destinations`** is
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
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.
## 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. Ten types are defined in `TRO_CATALOG`, 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). Everything is a Pydantic v2 model
CAD marker shape. **Eleven** 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
> 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
> repository or in `doc/TRO_Katalog/scl_templates/`, and has to be created in
> 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.
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.
## 6a. `tro_overrides.py` — manual corrections to the derived TROs (library, no CLI)
`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
identified as its own transfer point. Those findings come from a site review,
and they must survive the next run — the `.dot`/`.svg`/`.md`/`.ini` outputs are
all regenerated from scratch every time, so editing them is not an option.
They therefore live in **`%SKEL_CFG%\tro_overrides.ini`** (hand-maintained,
never generated). Section names are prefixed with the CSV file they apply to,
so several plants — and several exports of the same plant — can coexist:
```ini
[export.csv.tro.SEP0011] ; type correction, keyed on separator 0011
type = 1Sep1Swi ; a type from lib/tro_catalog.py
reason = Weiche vor Ort bestaetigt (Review 2026-08-10)
confidence = bestaetigt ; default when omitted
[mubea.csv.openpoint.Qualitaetskontrolle] ; explicitly NOT a TRO yet
x = -1026.00 ; plant coordinates in mm (CSV system)
y = 5250.05
z = 1478.50
expect = 1Sep1Swi ; type expected here
belongs_to = TRO09 ; TRO that owns the part today
note = ...
```
A fourth kind folds several derived TROs into one control object:
```ini
[export.csv.merge.Beruehrpunkt_K2_K1_gesamt]
type = 2Sep2Swi
separators = 0005, 0057 ; at least two, addressed by separator
host = 0002-R ; primary flow node; the others are kept
label = ...
reason = ...
```
A third kind creates a TRO the derivation missed entirely:
```ini
[export.csv.split.Beruehrpunkt_K2_K1] ; pull a separator out of its TRO
type = 1Sep1Swi
separators = 0057 ; comma-separated
scanners = ; optional, follow the separators
host = 0001-L ; flow node the new TRO sits on
label = ...
reason = ...
```
Three kinds of entry, deliberately different in weight:
- **`tro`** — changes the type of an already-derived TRO. Applied after the
TRO numbers are assigned (the sections address them by ID) and before the FB
block and component list are pulled from the catalog, so the type change
drags both along. A manually set type also suppresses the automatic
"check whether `1Sep_SSCC` is needed" hint for that TRO.
- **`split`** — creates a new TRO from separators taken out of an existing one.
For transfer points the derivation misses because the separator's
`Zuordnung` points at a conveyor (Strecke/Gefaellestrecke) instead of the
Kreisel lane it actually switches on. Applied *before* the numbers are
assigned, so the new TROs slot into the normal ordering — which means
**adding a split renumbers everything after it**. That is exactly why type
corrections should be keyed `SEP<number>`, not `TRO<number>`.
- **`merge`** — folds several TROs into one. Runs *after* the splits, so a
freshly split-out TRO can be part of a merge. The first affected TRO survives
and absorbs the others' separators, scanners and hosts; `host` picks which
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.
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.
## 7. `dxf_registration.py` — CSV↔DXF coordinate registration (library, no CLI)
Derives the rigid transform (rotation + offset, no scaling) between the CSV
@@ -235,6 +437,25 @@ Persists/loads the verified transform per CSV/DXF filename pair under
`%SKEL_CFG%\dxf_registration.json`. Used exclusively by `tro_annotate.py`; not
run directly.
`ANCHOR_BLOCKS` holds **name patterns** (fnmatch, case-insensitive), not fixed
names, because two naming worlds occur in this project:
| Drawing | Separator blocks | Scanner blocks |
|---|---|---|
| original customer drawing (`500573_60_1.dxf`) | `S-SP`, `S-LP` | `SCAN` |
| macro-generated (`Mubea.dxf`, from dxfmakros) | `Separator_SP_2D`/`_3D` | `Scanner_2D`/`_3D` |
`Staustrecke_Separator_SP_*` is deliberately **not** an anchor: it is a
conveyor module with a built-in separator, so its insertion point is the module
origin rather than the separator, which would feed a second cluster — offset by
exactly that distance — into the vote.
Note that the two roles can carry slightly different systematic offsets (in
`Mubea.dxf`: separators land on +100/0, scanners on +400/0), so a mixed fit
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.
## 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