scl_context: Staubereichs-Namen je Foerderer in Fliessrichtung

Staubereiche heissen DB_JamArea<NNNN>.stJam<NNNN>_<k> (Regel der
SPS-Programmierung, Dematic): NNNN = Foerderer, auf dem der Staubereich
liegt (Kreisel- bzw. Vario-Motor), k zaehlt auf diesem Foerderer in
Fliessrichtung. Foerderer: eigener Vario-Antrieb, sonst Separator auf einem
Kreisel (staubereich_kreisel_max_mm in cfg/parameter.ini [scl_context]),
sonst Kreisel-Host, sonst letzter Antriebs-TRO davor auf derselben Strecke.
k folgt der Nachfolgerkette auf dem Foerderer, nicht der REGION-Reihenfolge
(Breitensuche, Dematic Kreisel3 sonst TRO20 vor TRO23). Programm-DBs zaehlen
in der --strict-Gegenpruefung nicht als Geraete.

merge_hw_layout: von Hand gesetzte Antriebe (hw_overrides.ini) bekommen
keinen zusaetzlichen automatischen Widerspruch-Eintrag mehr.

hw_overrides.ini Dematic: VF_6 = MA0043 (TRO04/05) + MA0044 (TRO06/07),
VF_4 = MA0045 (TRO01/02, K0045 = 0008); Separatoren nach Klartext statt
kleinstem Abstand - TRO04 hatte BG0111 von VF_4.

Dematic: alle 25 Staubereiche benannt, 123 Slot-Rollen, 0 Widersprueche,
offene Stellen 615 -> 494.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 16:26:10 +02:00
parent 2fbd2f119d
commit 404d500271
9 changed files with 312 additions and 38 deletions
+3 -1
View File
@@ -235,7 +235,9 @@ byte — rewrite them (`SKEL_UPDATE_GOLDEN=1`) only for a reviewed, intended cha
numbers in `cfg/parameter.ini [merge_hw_layout]`. See `doc/Python_Scripts.md` §5e. numbers in `cfg/parameter.ini [merge_hw_layout]`. See `doc/Python_Scripts.md` §5e.
8. **`scl_skeleton.py --json <csv>_anlage.json`** (stage 4, names) — reads the plant JSON 8. **`scl_skeleton.py --json <csv>_anlage.json`** (stage 4, names) — reads the plant JSON
(schema detected) and fills sensors, valves, drives and conveyor numbers from `bindings`/ (schema detected) and fills sensors, valves, drives and conveyor numbers from `bindings`/
`conveyors` via `scl_context.HwSource`; open/contradicting bindings stay `TODO(E-Planung)`; `conveyors` via `scl_context.HwSource`, and jam-area names `"DB_JamArea<NNNN>".stJam<NNNN>_<k>`
(conveyor = Kreisel/Vario motor, `<k>` along the flow; `scl_context.jam_names`);
open/contradicting bindings stay `TODO(E-Planung)`;
inserted names are checked against `<projekt>_hw.json` (`--strict` aborts). Templates are inserted names are checked against `<projekt>_hw.json` (`--strict` aborts). Templates are
unchanged. See `doc/Python_Scripts.md` §5b. unchanged. See `doc/Python_Scripts.md` §5b.
+28
View File
@@ -28,3 +28,31 @@
; [mubea.csv.conveyors] ; [mubea.csv.conveyors]
; K0105 = 0001 ; K0105 = 0001
; ================================================================ ; ================================================================
; ----------------------------------------------------------------
; Dematic (dematic_merge.csv + dematic_merge..dxf), 2026-10-22
; ----------------------------------------------------------------
; VF_6 (0010) traegt zwei Varios: MA0043 (TRO04 + TRO05 dahinter) und MA0044
; (TRO06 + TRO07); VF_4 (0008) den Vario MA0045 (TRO01 + TRO02) - Angabe der
; SPS-Programmierung, bestaetigt durch die Klartexte der Zeichnung.
; Die E-Symbole liegen dort 0,4-1 m versetzt in verschiedene Richtungen; der
; kleinste Abstand gab TRO04 den Separator von VF_4 (BG0111). Zuordnung
; deshalb nach Klartext:
; BG0080 "in Separator vor ILS-VARIO (MA0043)" -> TRO04 (0082)
; BG0085 "in Separator vor Eintaeter nach (MA0043)" -> TRO05 (0083)
; BG0111 "in Separator vor ILS-VARIO (MA0045)" -> TRO01 (0074; das
; Layout-Bauteil steht ~5,5 m daneben - Mechanik noch unvollstaendig)
[dematic_merge.csv.bindings]
TRO01.separator1.sensor = BG0111
TRO01.separator1.stopper = MB0111
TRO04.separator1.sensor = BG0080
TRO04.separator1.stopper = MB0080
TRO05.separator1.sensor = BG0085
TRO05.separator1.stopper = MB0085
TRO04.drive.motor = MA0043
TRO04.drive.motorProtection = FC0043
TRO06.drive.motor = MA0044
TRO06.drive.motorProtection = FC0044
[dematic_merge.csv.conveyors]
K0045 = 0008
+8
View File
@@ -197,3 +197,11 @@ conveyor_min_share = 0.6
; Layout-Separatoren einen Partner findet (0..1) - Hinweis auf verschiedene ; Layout-Separatoren einen Partner findet (0..1) - Hinweis auf verschiedene
; Planungsstaende von Layout und E-Zeichnung. ; Planungsstaende von Layout und E-Zeichnung.
registration_min_share = 0.8 registration_min_share = 0.8
[scl_context]
; Staubereichs-Namen "DB_JamArea<NNNN>".stJam<NNNN>_<k>: ein Staubereich liegt
; auf einem Kreisel, wenn sein Separator hoechstens so weit von dessen
; Huellbox entfernt ist (die Separatoren sitzen auf den Bahnen am Rand der
; Box; Dematic 0048/0040: 0 mm, naechster Separator einer Strecke >500 mm).
staubereich_kreisel_max_mm = 300.0
+28 -8
View File
@@ -607,7 +607,26 @@ layout is `anlage.layout`; names come from `bindings[]` by TRO-ID + slot + role
| `line<n>` `sensor` / `stopper` / `jamSensor` | PinStore storage lines | | `line<n>` `sensor` / `stopper` / `jamSensor` | PinStore storage lines |
| `switch<n>` `valve` | `xOutSw<n>ExTo3` — the slot names the switch part from `switches` of the TRO-JSON (Ausschleuselement or ILS Weiche; `switch<i>` belongs to `separator<i>` in 2Sep types) | | `switch<n>` `valve` | `xOutSw<n>ExTo3` — the slot names the switch part from `switches` of the TRO-JSON (Ausschleuselement or ILS Weiche; `switch<i>` belongs to `separator<i>` in 2Sep types) |
| `drive` `motor` / `motorProtection` / `sen*` | Vario motor, `xInMotorProtection`, `stSenCarrInPos` … (motor/protection fall back to the conveyor of the TRO's host) | | `drive` `motor` / `motorProtection` / `sen*` | Vario motor, `xInMotorProtection`, `stSenCarrInPos` … (motor/protection fall back to the conveyor of the TRO's host) |
| `conveyors[]` on the TRO's host | `xInCarouselRun` = `#fbCarousel<NNNN>.xOutConveyorFullSpeed` (Kreisel only); jam DB `"DB_JamArea<NNNN>".stJam<NNNN>_____` — still `TODO`, the running number `<k>` is not fixed yet | | `conveyors[]` on the TRO's host | `xInCarouselRun` = `#fbCarousel<NNNN>.xOutConveyorFullSpeed` (Kreisel only) |
| `conveyors[]` + drive bindings | jam DB `"DB_JamArea<NNNN>".stJam<NNNN>_<k>` (`scl_context.jam_names`, see below) |
**Jam-area names** (rule of the PLC programmers, Dematic 2026-10-22).
`<NNNN>` is the conveyor the jam area sits on — the number of its Kreisel or
Vario motor — and `<k>` counts up along the material flow on that conveyor
(Vario MA0051 → `stJam0051_1`, `stJam0051_2`). The conveyor of a jam area, in
this order: (1) the TRO's own drive (Vario); (2) its separator lies on a
Kreisel (within `staubereich_kreisel_max_mm` of the Kreisel's bounding box,
`cfg/parameter.ini [scl_context]`) — catches TROs whose host is the outgoing
line; (3) the host is a Kreisel; (4) the host is a line carrying drive TROs →
the drive of the last drive TRO before it on that line (a 1Sep behind a Vario
belongs to that Vario's conveyor; VF_6 carries two Varios). `<k>` = 1 is the
area the flow reaches first on that conveyor (on a Kreisel: where material
comes on); from there the count follows the successor chain on the same
conveyor, not the REGION order — that is a breadth-first search over the whole
plant and reaches a Kreisel TRO through another conveyor first. A jam area
without a conveyor stays a placeholder and does not count. Dematic: all 25 jam
areas named. Jam DB names are program DBs, not devices — the `--strict`
cross-check skips them.
Not filled yet: jam sensors at the exits (`stInSenJam<n>`, the `Ausschleus.stau` Not filled yet: jam sensors at the exits (`stInSenJam<n>`, the `Ausschleus.stau`
bindings have no slot), scanners, separator/switch/MainTro numbers. Switch bindings have no slot), scanners, separator/switch/MainTro numbers. Switch
@@ -860,7 +879,8 @@ Steps:
on the same layout conveyor (Dematic VF_6: two Vario TROs), the drive is a on the same layout conveyor (Dematic VF_6: two Vario TROs), the drive is a
`widerspruch` with the conveyor's motor as a suggestion — each TRO has its `widerspruch` with the conveyor's motor as a suggestion — each TRO has its
own motor; set it in `hw_overrides.ini`. `scl_skeleton` then keeps own motor; set it in `hw_overrides.ini`. `scl_skeleton` then keeps
`MA____` instead of falling back to the conveyor's motor. `MA____` instead of falling back to the conveyor's motor. A drive set by
hand in `hw_overrides.ini` is left alone by this step.
5. **Corrections** from `hw_overrides.ini` (last): 5. **Corrections** from `hw_overrides.ini` (last):
`[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and `[<csv>.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and
`[<csv>.conveyors] K0102 = 0010`. `[<csv>.conveyors] K0102 = 0010`.
@@ -886,12 +906,12 @@ conveyors (Aufgabestation #100/#101, Vario #106–#109) are outside the CSV or
have a single vote and stay open. have a single vote and stay open.
Result on Dematic (`dematic_merge_tro.json` + `dematic_hw.json`, rules Result on Dematic (`dematic_merge_tro.json` + `dematic_hw.json`, rules
`e_planning_dematic.ini`): 113 slot roles assigned (all 27 storage lines with `e_planning_dematic.ini`, corrections for VF_4/VF_6 in `hw_overrides.ini`): 123
sensor and stopper, 23 of 25 separators, 5 of 6 switch valves, 6 Vario drives), slot roles assigned (all 27 storage lines with sensor and stopper, all 25
39 open (Vario sensors, scanners, VF_4/VF_6 where mechanical parts are still separators, 5 of 6 switch valves, all 7 Vario drives), 33 open (Vario sensors,
missing), 4 contradictions (the two Vario TROs on VF_6). Then scanners, one switch valve), 0 contradictions. Then
`scl_skeleton --templates --strict`: 115 names inserted, all known in TIA, `scl_skeleton --templates --strict`: 200 names inserted (75 of them jam-area
open gaps 730 → 615. names), all devices known in TIA, open gaps 730 → 494.
**Stage 4:** `scl_skeleton --json <csv>_anlage.json` reads the plant JSON **Stage 4:** `scl_skeleton --json <csv>_anlage.json` reads the plant JSON
directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together
+7 -3
View File
@@ -541,14 +541,17 @@ def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding], def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
hw: HardwareDocument) -> list[Binding]: hw: HardwareDocument,
skip: set[tuple[str, str, str]] | None = None) -> list[Binding]:
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers. """Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers.
Tragen mehrere TROs einen Antriebs-Slot auf demselben Layout-Foerderer Tragen mehrere TROs einen Antriebs-Slot auf demselben Layout-Foerderer
(Dematic VF_6: zwei Vario-TROs, drei Vario-Motoren), hat jeder TRO seinen (Dematic VF_6: zwei Vario-TROs, drei Vario-Motoren), hat jeder TRO seinen
eigenen Motor - welcher, sagt die Mehrheit je Foerderer nicht. Dann wird eigenen Motor - welcher, sagt die Mehrheit je Foerderer nicht. Dann wird
der Antrieb ein Widerspruch (BMK als Vorschlag), Korrektur von Hand. der Antrieb ein Widerspruch (BMK als Vorschlag), Korrektur von Hand.
Rollen in ``skip`` (TRO, Slot, Rolle - von Hand gesetzt) bleiben unberuehrt.
""" """
skip = skip or set()
devices = hw.by_bmk() devices = hw.by_bmk()
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"} by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
sharing: dict[str, list[str]] = {} sharing: dict[str, list[str]] = {}
@@ -568,7 +571,7 @@ def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
shared = sharing.get(s.layout_id.split("-", 1)[0], []) shared = sharing.get(s.layout_id.split("-", 1)[0], [])
for role, attr in _DRIVE_ROLES.items(): for role, attr in _DRIVE_ROLES.items():
bmk = getattr(conv, attr) bmk = getattr(conv, attr)
if role not in roles or not bmk: if role not in roles or not bmk or (tro.tro_id, s.slot, role) in skip:
continue continue
dev = devices.get(bmk) dev = devices.get(bmk)
status = "zugeordnet" if dev and dev.in_tia else "widerspruch" status = "zugeordnet" if dev and dev.in_tia else "widerspruch"
@@ -742,7 +745,8 @@ def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
bindings = bind_slots(doc, assignments, rules, hw) bindings = bind_slots(doc, assignments, rules, hw)
if overrides is not None: if overrides is not None:
bindings = _apply_overrides(doc, bindings, conveyors, overrides, hw, warnings) bindings = _apply_overrides(doc, bindings, conveyors, overrides, hw, warnings)
bindings += bind_drives(doc, conveyors, hw) bindings += bind_drives(doc, conveyors, hw,
{(b.tro, b.slot, b.role) for b in bindings if b.rule == "hand"})
bindings = _open_and_conflicts(doc, bindings) bindings = _open_and_conflicts(doc, bindings)
used = {b.bmk for b in bindings if b.bmk} used = {b.bmk for b in bindings if b.bmk}
+135 -20
View File
@@ -41,6 +41,7 @@ from typing import TYPE_CHECKING
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from model import AnlageDocument, ConveyorBinding, JamArea, TroBase, TroDocument from model import AnlageDocument, ConveyorBinding, JamArea, TroBase, TroDocument
from parameter import get_float
from tro_catalog import TroItem from tro_catalog import TroItem
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -63,11 +64,15 @@ __all__ = [
"HwSource", "HwSource",
"build_context", "build_context",
"flow_order", "flow_order",
"jam_names",
"reachable_via", "reachable_via",
] ]
TODO = "TODO(E-Planung)" TODO = "TODO(E-Planung)"
# Zahlenwerte aus cfg/parameter.ini [scl_context] - Bedeutung steht dort.
STAUBEREICH_KREISEL_MAX_MM = get_float("scl_context", "staubereich_kreisel_max_mm")
_CONFIG = ConfigDict(extra="forbid") _CONFIG = ConfigDict(extra="forbid")
@@ -524,10 +529,11 @@ def _scanner(n: int, suffix: str, interface: str = "cScanner____") -> SclScanner
def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None, def _jam(n: int, neighbour: str, jam_db: str, sensor: SclName | None = None,
jam_id: str = "") -> SclJam: jam_id: str = "", known: bool = False) -> SclJam:
name = _bekannt if known else _offen
return SclJam( return SclJam(
n=n, jam_id=jam_id, neighbour=neighbour, db=_offen(jam_db), sensor=sensor, n=n, jam_id=jam_id, neighbour=neighbour, db=name(jam_db), sensor=sensor,
carriers=_offen(f"{jam_db}.arCarrier"), data=_offen(f"{jam_db}.stData"), carriers=name(f"{jam_db}.arCarrier"), data=name(f"{jam_db}.stData"),
) )
@@ -610,22 +616,126 @@ def _separator(i: int, tro_id: str, layout: str, hw: HwSource) -> SclSeparator:
_JAM_DB = '"DB_JamArea____".stJam____' _JAM_DB = '"DB_JamArea____".stJam____'
def _jam_db(area: JamArea | None, hw: HwSource) -> str: def _jam_db(area: JamArea | None, hw: HwSource,
"""Platzhalter des Staubereichs-DB, mit Foerderernummer soweit bekannt. namen: dict[str, str] | None = None) -> tuple[str, bool]:
"""Name des Staubereichs-DB und ob er feststeht.
Schema der Referenzen: "DB_JamArea<NNNN>".stJam<NNNN>_<k>. <NNNN> ist der Schema der Referenzen: "DB_JamArea<NNNN>".stJam<NNNN>_<k> (jam_names).
Foerderer, auf dem der Staubereich liegt (Anlagen-JSON, conveyors[]); Steht der Name nicht fest, bleibt ein Platzhalter - mit Foerderernummer,
<k> - die laufende Nummer entlang des Flusses auf diesem Foerderer - ist soweit der Layout-Foerderer sie kennt.
noch nicht festgelegt (doc/Konzept_Layout_Hardware_SCL.md "Staubereiche").
Der Name bleibt deshalb immer offen.
""" """
if area is not None and namen and area.jam_id in namen:
return namen[area.jam_id], True
conveyor = hw.conveyor(area.host) if area is not None else None conveyor = hw.conveyor(area.host) if area is not None else None
nr = _foerderer_nr(conveyor) if conveyor is not None else "" nr = _foerderer_nr(conveyor) if conveyor is not None else ""
return f'"DB_JamArea{nr}".stJam{nr}_____' if nr else _JAM_DB return (f'"DB_JamArea{nr}".stJam{nr}_____' if nr else _JAM_DB), False
def jam_names(document: TroDocument, geordnet: list[TroBase],
hw: HwSource) -> dict[str, str]:
"""Staubereichs-Namen je Kennung (J0048 -> '"DB_JamArea0042".stJam0042_1').
Regel der SPS-Programmierer (Dematic, 2026-10-22): <NNNN> ist der
Foerderer, auf dem der Staubereich liegt (Nummer des Kreisel- bzw.
Vario-Motors), <k> zaehlt auf diesem Foerderer in Fliessrichtung hoch.
<k> = 1 ist der Staubereich, den der Fluss (flow_order) auf diesem
Foerderer zuerst erreicht - auf einem Kreisel die Stelle, an der das
Material auf den Kreisel kommt; von dort geht es die Nachfolger entlang
(_entlang), nicht nach REGION-Reihenfolge: die ist Breitensuche ueber die
ganze Anlage und erreicht einen Kreisel-TRO oft zuerst ueber einen anderen
Foerderer (Dematic Kreisel3: TRO17 -> TRO14 -> TRO23 -> TRO20).
Foerderer eines Staubereichs, in dieser Reihenfolge:
1. der TRO hat einen eigenen Antrieb (Vario) -> dessen Foerderer;
2. der Separator liegt auf einem Kreisel (hoechstens
STAUBEREICH_KREISEL_MAX_MM von dessen Huellbox) -> der Kreisel;
3. der Host ist ein Kreisel -> der Kreisel;
4. der Host ist eine Strecke mit Antriebs-TROs -> der Antrieb des
letzten Antriebs-TRO davor auf dieser Strecke (ein 1Sep hinter einem
Vario gehoert zu dessen Foerderer; VF_6 traegt zwei Varios).
Sonst bleibt der Staubereich offen und zaehlt nicht mit.
"""
anlage = hw.anlage
if anlage is None:
return {}
rang = {t.tro_id: i for i, t in enumerate(geordnet)}
nr_of_motor = {c.motor: _foerderer_nr(c) for c in anlage.conveyors if c.motor}
def antrieb(tro_id: str) -> str:
b = anlage.binding(tro_id, "drive", "motor")
if b is None or b.status != "zugeordnet" or not b.bmk:
return ""
return nr_of_motor.get(b.bmk, "")
antriebe: dict[str, list[tuple[int, str]]] = {}
for t in geordnet:
nr = antrieb(t.tro_id)
hosts = list(getattr(t, "hosts", []) or [])
if nr and hosts:
antriebe.setdefault(hosts[0].split("-", 1)[0], []).append((rang[t.tro_id], nr))
kreisel = [(p, anlage.conveyor_for(p.layout_id)) for p in document.parts
if p.kind == "Kreisel"]
kreisel = [(p, c) for p, c in kreisel if c is not None]
def auf_kreisel(separator: str) -> str:
part = document.part(separator) if separator else None
if part is None or part.position is None:
return ""
nah = [(d, c) for p, c in kreisel
if (d := p.distance_to_box(part.position[0], part.position[1])) is not None
and d <= STAUBEREICH_KREISEL_MAX_MM]
return _foerderer_nr(min(nah, key=lambda x: x[0])[1]) if nah else ""
def foerderer(area: JamArea) -> str:
nr = antrieb(area.tro) or auf_kreisel(area.separator)
if not nr and hw.is_carousel(area.host):
conv = hw.conveyor(area.host)
nr = _foerderer_nr(conv) if conv is not None else ""
if not nr:
davor = [n for r, n in antriebe.get(area.host.split("-", 1)[0], [])
if r < rang.get(area.tro, -1)]
nr = davor[-1] if davor else ""
return nr
areas = sorted(document.jam_areas_or_derived(),
key=lambda a: (rang.get(a.tro, len(rang)), a.entry))
je_foerderer: dict[str, list[JamArea]] = {}
for area in areas:
nr = foerderer(area)
if nr:
je_foerderer.setdefault(nr, []).append(area)
nachfolger = {t.tro_id: list(t.successors) for t in geordnet}
namen: dict[str, str] = {}
for nr, liste in je_foerderer.items():
for k, area in enumerate(_entlang(liste, nachfolger), start=1):
namen[area.jam_id] = f'"DB_JamArea{nr}".stJam{nr}_{k}'
return namen
def _entlang(areas: list[JamArea], nachfolger: dict[str, list[str]]) -> list[JamArea]:
"""Staubereiche eines Foerderers in Fliessrichtung.
``areas`` kommt nach flow_order sortiert; der erste ist der Anfang. Danach
jeweils der naechste Eingang desselben TRO, sonst ein Staubereich eines
Nachfolger-TRO auf diesem Foerderer, sonst (Kette unterbrochen) der
naechste nach flow_order.
"""
rest = list(areas)
folge = [rest.pop(0)]
while rest:
jetzt = folge[-1].tro
weiter = ([a for a in rest if a.tro == jetzt]
or [a for a in rest if a.tro in nachfolger.get(jetzt, [])]
or rest)
folge.append(weiter[0])
rest.remove(weiter[0])
return folge
def _entries(tro: TroBase, eigene: list[JamArea], def _entries(tro: TroBase, eigene: list[JamArea],
alle: dict[str, TroBase], hw: HwSource) -> list[SclJam]: alle: dict[str, TroBase], hw: HwSource,
namen: dict[str, str] | None = None) -> list[SclJam]:
"""Ein JamEntr je eigenem Staubereich - nicht je Vorgaenger: mehrere """Ein JamEntr je eigenem Staubereich - nicht je Vorgaenger: mehrere
Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05).""" Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05)."""
eingaenge = [] eingaenge = []
@@ -633,7 +743,8 @@ def _entries(tro: TroBase, eigene: list[JamArea],
von = ", ".join(_nachbar(v, alle) for v in area.predecessors) or "-" von = ", ".join(_nachbar(v, alle) for v in area.predecessors) or "-"
if area.note: if area.note:
von = f"{von} ({area.note})" von = f"{von} ({area.note})"
eingaenge.append(_jam(area.entry, von, _jam_db(area, hw), jam_id=area.jam_id)) db, known = _jam_db(area, hw, namen)
eingaenge.append(_jam(area.entry, von, db, jam_id=area.jam_id, known=known))
return eingaenge return eingaenge
@@ -686,7 +797,8 @@ def _carousel_run(tro: TroBase, hw: HwSource) -> SclName:
def _tro_context(tro: TroBase, alle: dict[str, TroBase], def _tro_context(tro: TroBase, alle: dict[str, TroBase],
hw: HwSource | None = None, hw: HwSource | None = None,
bereiche: dict[str, list[JamArea]] | None = None) -> SclTro: bereiche: dict[str, list[JamArea]] | None = None,
namen: dict[str, str] | None = None) -> SclTro:
items = tro.items items = tro.items
n_sep = items.get(TroItem.SEPARATOR, 1) n_sep = items.get(TroItem.SEPARATOR, 1)
n_swi = items.get(TroItem.SWITCH, 0) n_swi = items.get(TroItem.SWITCH, 0)
@@ -728,15 +840,17 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase],
] if n_swi else [] ] if n_swi else []
# Staubereiche aus der TRO-JSON (jamAreas): Eingaenge sind die eigenen # Staubereiche aus der TRO-JSON (jamAreas): Eingaenge sind die eigenen
# (ein JamEntr je eigenem Staubereich, nicht je Vorgaenger), Ausgaenge die # (ein JamEntr je eigenem Staubereich, nicht je Vorgaenger), Ausgaenge die
# Eingaenge der Nachfolger. Der DB-Name braucht Foerderernummer (aus der # Eingaenge der Nachfolger. Der DB-Name (jam_names) braucht Foerderer-
# Anlagen-JSON) und laufende Nummer (nicht festgelegt) und bleibt offen. # nummer und laufende Nummer; ohne beide bleibt er offen.
jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, hw) jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, hw, namen)
jam_exits = [] jam_exits = []
for n, nach in enumerate(tro.successors, start=2): for n, nach in enumerate(tro.successors, start=2):
area = _exit_area(tro_id, nach, bereiche) area = _exit_area(tro_id, nach, bereiche)
jam_exits.append(_jam(n, _nachbar(nach, alle), _jam_db(area, hw), db, known = _jam_db(area, hw, namen)
jam_exits.append(_jam(n, _nachbar(nach, alle), db,
sensor=_offen("cInBG____"), sensor=_offen("cInBG____"),
jam_id=area.jam_id if area is not None else "")) jam_id=area.jam_id if area is not None else "",
known=known))
return SclTro( return SclTro(
id=tro_id, id=tro_id,
@@ -810,11 +924,12 @@ def build_context(document: TroDocument, source: str, start_id: str = "",
for area in document.jam_areas_or_derived(): for area in document.jam_areas_or_derived():
bereiche.setdefault(area.tro, []).append(area) bereiche.setdefault(area.tro, []).append(area)
geordnet, startgrund = flow_order(document.tros, start_id) geordnet, startgrund = flow_order(document.tros, start_id)
namen = jam_names(document, geordnet, hw)
return SclFile( return SclFile(
source=source, source=source,
created=created or datetime.now().strftime("%Y-%m-%d %H:%M"), created=created or datetime.now().strftime("%Y-%m-%d %H:%M"),
start_reason=startgrund, start_reason=startgrund,
tros=[_tro_context(t, alle, hw, bereiche) for t in geordnet], tros=[_tro_context(t, alle, hw, bereiche, namen) for t in geordnet],
eplan_source=hw.source if eplan is not None else "", eplan_source=hw.source if eplan is not None else "",
hw_source=hw.source if anlage is not None else "", hw_source=hw.source if anlage is not None else "",
) )
+4 -2
View File
@@ -585,13 +585,15 @@ def unknown_names(context: SclFile, hw: HardwareDocument) -> list[str]:
"""Eingesetzte Namen, die die Hardwareplanung nicht kennt. """Eingesetzte Namen, die die Hardwareplanung nicht kennt.
Geprueft werden Symbole und Konstanten (cInBG0532, MB0533, "FC0202"); Geprueft werden Symbole und Konstanten (cInBG0532, MB0533, "FC0202");
Instanzzugriffe wie #fbCarousel0102.xOut... sind keine globalen Namen. Instanzzugriffe wie #fbCarousel0102.xOut... und Zugriffe auf Programm-DBs
wie "DB_JamArea0102".stJam0102_1 sind keine Geraete der Hardwareplanung.
""" """
bekannt = {n for d in hw.devices for n in (d.bmk, d.constant) if n} bekannt = {n for d in hw.devices for n in (d.bmk, d.constant) if n}
bekannt |= {c.name for c in hw.constants} bekannt |= {c.name for c in hw.constants}
bekannt |= {n for d in hw.drives for n in (d.motor, d.protection, d.encoder) if n} bekannt |= {n for d in hw.drives for n in (d.motor, d.protection, d.encoder) if n}
return [name for name in known_names(context) return [name for name in known_names(context)
if not name.startswith("#") and name.strip('"') not in bekannt] if not name.startswith(("#", '"DB_'))
and name.strip('"') not in bekannt]
def _schema_of(path: Path) -> str: def _schema_of(path: Path) -> str:
+11
View File
@@ -196,6 +196,17 @@ def test_zwei_antriebs_tros_auf_einem_foerderer_sind_widerspruch():
assert anlage.binding(tro, "drive", "motorProtection").status == "widerspruch" assert anlage.binding(tro, "drive", "motorProtection").status == "widerspruch"
def test_antrieb_von_hand_wird_nicht_ueberschrieben():
"""hw_overrides.ini legt den Motor je Vario fest - die automatische
Antriebszuordnung haengt keinen zweiten (Widerspruch-)Eintrag daneben."""
overrides = merge_hw_layout.Overrides(bindings={("TRO03", "drive", "motor"): "MA0210"})
anlage = run(doc=layout(second_vario=True), overrides=overrides)
motors = [b for b in anlage.bindings
if (b.tro, b.slot, b.role) == ("TRO03", "drive", "motor")]
assert [(b.bmk, b.rule, b.status) for b in motors] == [("MA0210", "hand", "zugeordnet")]
assert anlage.binding("TRO06", "drive", "motor").status == "widerspruch"
def test_gemeinsame_zeichnung_ohne_lagebezug(tmp_path): def test_gemeinsame_zeichnung_ohne_lagebezug(tmp_path):
"""hw_import --dxf: E-Symbole stehen schon in Layout-Koordinaten - kein """hw_import --dxf: E-Symbole stehen schon in Layout-Koordinaten - kein
Lagebezug wird ermittelt oder abgelegt.""" Lagebezug wird ermittelt oder abgelegt."""
+88 -4
View File
@@ -103,15 +103,99 @@ def test_kreisel_freigabe_und_staubereich_aus_der_foerderernummer():
tro01 = _by_id(ctx, "TRO01") tro01 = _by_id(ctx, "TRO01")
assert tro01.carousel_run.value == "#fbCarousel0102.xOutConveyorFullSpeed" assert tro01.carousel_run.value == "#fbCarousel0102.xOutConveyorFullSpeed"
assert not tro01.carousel_run.todo assert not tro01.carousel_run.todo
# Staubereich: Foerderer bekannt, laufende Nummer <k> nicht -> weiter offen # Staubereich am Kreisel: Foerderer K0102, in Fliessrichtung gezaehlt
assert tro01.jam_entries[0].db.value == '"DB_JamArea0102".stJam0102_____' assert tro01.jam_entries[0].db.value == '"DB_JamArea0102".stJam0102_1'
assert tro01.jam_entries[0].db.todo assert not tro01.jam_entries[0].db.todo
# Ausgang zu TRO03 liegt auf VF_1 (K0106) assert tro01.jam_exits[0].db.value == '"DB_JamArea0102".stJam0102_2'
# Ausgang zu TRO03 liegt auf VF_1 (K0106); ohne Antrieb des Vario steht
# die laufende Nummer nicht fest -> offen
assert tro01.jam_exits[1].db.value == '"DB_JamArea0106".stJam0106_____' assert tro01.jam_exits[1].db.value == '"DB_JamArea0106".stJam0106_____'
assert tro01.jam_exits[1].db.todo
# TRO03 sitzt auf einer Strecke, nicht am Kreisel -> Freigabe bleibt offen # TRO03 sitzt auf einer Strecke, nicht am Kreisel -> Freigabe bleibt offen
assert _by_id(ctx, "TRO03").carousel_run.todo assert _by_id(ctx, "TRO03").carousel_run.todo
def test_staubereiche_je_foerderer_in_fliessrichtung_gezaehlt():
"""Regel der SPS (Dematic): "DB_JamArea<NNNN>".stJam<NNNN>_<k>, NNNN =
Kreisel- bzw. Vario-Motor, k zaehlt in Fliessrichtung. Nachbau von VF_6:
zwei Varios auf einer Strecke, je ein 1Sep dahinter; davor ein TRO, dessen
Separator auf dem Kreisel liegt, obwohl sein Host die Strecke ist."""
def tro(tro_id, typ, sep, host, vor, nach):
return _tro(tro_id, typ, [sep], [host], predecessors=[vor], successors=[nach])
tros = [
tro("TRO01", "1Sep", "0048", "0008", "SYS1", "TRO02"),
tro("TRO02", "Vario", "0082", "0010", "TRO01", "TRO03"),
tro("TRO03", "1Sep", "0083", "0010", "TRO02", "TRO04"),
tro("TRO04", "Vario", "0084", "0010", "TRO03", "TRO05"),
tro("TRO05", "1Sep", "0085", "0010", "TRO04", "TRO06"),
tro("TRO06", "1Sep", "0049", "0004-L", "TRO05", "SYS2"),
]
parts = [
LayoutPart(layout_id="0004", kind="Kreisel", name="Kreisel4",
position=(0.0, 0.0, 0.0), bbox=(800.0, 10000.0, 1.0)),
LayoutPart(layout_id="0010", kind="Strecke", name="VF_6",
position=(8000.0, 0.0, 0.0), bbox=(12000.0, 800.0, 1.0)),
LayoutPart(layout_id="0048", kind="Separator", name="S48",
position=(350.0, 100.0, 0.0)),
]
layout = TroDocument.from_analysis(Analysis(tros=tros), source="vf6.csv", parts=parts)
doc = AnlageDocument(
layout=layout,
bindings=[_bind("TRO02", "drive", "motor", "MA0043", "cOutMA0043"),
_bind("TRO04", "drive", "motor", "MA0044", "cOutMA0044")],
conveyors=[
ConveyorBinding(eplan_name="K0042", layout_id="0004", motor="MA0042"),
ConveyorBinding(eplan_name="K0043", layout_id="0010", motor="MA0043"),
ConveyorBinding(eplan_name="K0044", layout_id="0010", motor="MA0044",
status="widerspruch"),
],
)
ctx = _kontext(doc)
namen = {t.id: t.jam_entries[0].db for t in ctx.tros}
assert {k: v.value for k, v in namen.items()} == {
"TRO01": '"DB_JamArea0042".stJam0042_1', # Separator auf dem Kreisel
"TRO02": '"DB_JamArea0043".stJam0043_1',
"TRO03": '"DB_JamArea0043".stJam0043_2', # 1Sep hinter Vario MA0043
"TRO04": '"DB_JamArea0044".stJam0044_1',
"TRO05": '"DB_JamArea0044".stJam0044_2',
"TRO06": '"DB_JamArea0042".stJam0042_2', # Host Kreisel, weiter gezaehlt
}
assert not any(v.todo for v in namen.values())
# Ausgang = Eingang des Nachfolgers, derselbe Name
assert _by_id(ctx, "TRO02").jam_exits[0].carriers.value == \
'"DB_JamArea0043".stJam0043_2.arCarrier'
def test_staubereiche_am_kreisel_entlang_des_umlaufs():
"""Die Breitensuche erreicht T4 ueber die Strecke (T5) vor T6; auf dem
Kreisel kommt aber erst T6, dann T4 - gezaehlt wird entlang des Umlaufs."""
def tro(tro_id, host, vor, nach):
return _tro(tro_id, "1Sep", [f"9{tro_id[-1]}00"], [host],
predecessors=vor, successors=nach)
tros = [
tro("TRO01", "0004-L", ["SYS1"], ["TRO02", "TRO05"]),
tro("TRO02", "0004-L", ["TRO01"], ["TRO03"]),
tro("TRO03", "0004-L", ["TRO02"], ["TRO06"]),
tro("TRO05", "0010", ["TRO01"], ["TRO04"]),
tro("TRO06", "0004-R", ["TRO03"], ["TRO04"]),
tro("TRO04", "0004-R", ["TRO06", "TRO05"], ["SYS2"]),
]
layout = TroDocument.from_analysis(
Analysis(tros=tros), source="loop.csv",
parts=[_part("0004", "Kreisel", "Kreisel4"), _part("0010", "Strecke", "VF")])
doc = AnlageDocument(layout=layout, conveyors=[
ConveyorBinding(eplan_name="K0042", layout_id="0004", motor="MA0042")])
ctx = _kontext(doc)
assert [t.id for t in ctx.tros][:5] == ["TRO01", "TRO02", "TRO05", "TRO03", "TRO04"]
k = {t.id: t.jam_entries[0].db.value.rsplit("_", 1)[-1] for t in ctx.tros
if not t.jam_entries[0].db.todo}
assert k == {"TRO01": "1", "TRO02": "2", "TRO03": "3", "TRO06": "4", "TRO04": "5"}
# Strecke ohne Foerderer der Hardwareplanung: bleibt offen
assert _by_id(ctx, "TRO05").jam_entries[0].db.todo
def test_vario_antrieb_ueber_den_foerderer(): def test_vario_antrieb_ueber_den_foerderer():
tro = _by_id(_kontext(anlage()), "TRO03") tro = _by_id(_kontext(anlage()), "TRO03")
assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA0210", False) assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA0210", False)