From 404d500271b7b4aa84de519287049437b74c8e4f Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 22 Oct 2026 16:26:10 +0200 Subject: [PATCH] scl_context: Staubereichs-Namen je Foerderer in Fliessrichtung Staubereiche heissen DB_JamArea.stJam_ (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 --- CLAUDE.md | 4 +- cfg/hw_overrides.ini | 28 ++++++ cfg/parameter.ini | 8 ++ doc/Python_Scripts.md | 36 ++++++-- lib/merge_hw_layout.py | 10 ++- lib/scl_context.py | 155 +++++++++++++++++++++++++++++----- lib/scl_skeleton.py | 6 +- tests/test_merge_hw_layout.py | 11 +++ tests/test_scl_anlage.py | 92 +++++++++++++++++++- 9 files changed, 312 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e82ee4..031ff61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. 8. **`scl_skeleton.py --json _anlage.json`** (stage 4, names) — reads the plant JSON (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".stJam_` + (conveyor = Kreisel/Vario motor, `` along the flow; `scl_context.jam_names`); + open/contradicting bindings stay `TODO(E-Planung)`; inserted names are checked against `_hw.json` (`--strict` aborts). Templates are unchanged. See `doc/Python_Scripts.md` §5b. diff --git a/cfg/hw_overrides.ini b/cfg/hw_overrides.ini index ae58c2f..6ea99e5 100644 --- a/cfg/hw_overrides.ini +++ b/cfg/hw_overrides.ini @@ -28,3 +28,31 @@ ; [mubea.csv.conveyors] ; 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 diff --git a/cfg/parameter.ini b/cfg/parameter.ini index ee9f520..c060d51 100644 --- a/cfg/parameter.ini +++ b/cfg/parameter.ini @@ -197,3 +197,11 @@ conveyor_min_share = 0.6 ; Layout-Separatoren einen Partner findet (0..1) - Hinweis auf verschiedene ; Planungsstaende von Layout und E-Zeichnung. registration_min_share = 0.8 + + +[scl_context] +; Staubereichs-Namen "DB_JamArea".stJam_: 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 diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index 3b07c58..97e3b8d 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -607,7 +607,26 @@ layout is `anlage.layout`; names come from `bindings[]` by TRO-ID + slot + role | `line` `sensor` / `stopper` / `jamSensor` | PinStore storage lines | | `switch` `valve` | `xOutSwExTo3` — the slot names the switch part from `switches` of the TRO-JSON (Ausschleuselement or ILS Weiche; `switch` belongs to `separator` in 2Sep types) | | `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.xOutConveyorFullSpeed` (Kreisel only); jam DB `"DB_JamArea".stJam_____` — still `TODO`, the running number `` is not fixed yet | +| `conveyors[]` on the TRO's host | `xInCarouselRun` = `#fbCarousel.xOutConveyorFullSpeed` (Kreisel only) | +| `conveyors[]` + drive bindings | jam DB `"DB_JamArea".stJam_` (`scl_context.jam_names`, see below) | + +**Jam-area names** (rule of the PLC programmers, Dematic 2026-10-22). +`` is the conveyor the jam area sits on — the number of its Kreisel or +Vario motor — and `` 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). `` = 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`, the `Ausschleus.stau` 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 `widerspruch` with the conveyor's motor as a suggestion — each TRO has its own motor; set it in `hw_overrides.ini`. `scl_skeleton` then keeps - `MA____` instead of falling back to the conveyor's motor. + `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): `[.bindings] TRO01.separator1.sensor = BG0532` (or `= -` to clear) and `[.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. Result on Dematic (`dematic_merge_tro.json` + `dematic_hw.json`, rules -`e_planning_dematic.ini`): 113 slot roles assigned (all 27 storage lines with -sensor and stopper, 23 of 25 separators, 5 of 6 switch valves, 6 Vario drives), -39 open (Vario sensors, scanners, VF_4/VF_6 where mechanical parts are still -missing), 4 contradictions (the two Vario TROs on VF_6). Then -`scl_skeleton --templates --strict`: 115 names inserted, all known in TIA, -open gaps 730 → 615. +`e_planning_dematic.ini`, corrections for VF_4/VF_6 in `hw_overrides.ini`): 123 +slot roles assigned (all 27 storage lines with sensor and stopper, all 25 +separators, 5 of 6 switch valves, all 7 Vario drives), 33 open (Vario sensors, +scanners, one switch valve), 0 contradictions. Then +`scl_skeleton --templates --strict`: 200 names inserted (75 of them jam-area +names), all devices known in TIA, open gaps 730 → 494. **Stage 4:** `scl_skeleton --json _anlage.json` reads the plant JSON directly (§5b). `--eplan` still works for a TRO-JSON and is phased out together diff --git a/lib/merge_hw_layout.py b/lib/merge_hw_layout.py index 04ca484..fed64c4 100644 --- a/lib/merge_hw_layout.py +++ b/lib/merge_hw_layout.py @@ -541,14 +541,17 @@ def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]: 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. Tragen mehrere TROs einen Antriebs-Slot auf demselben Layout-Foerderer (Dematic VF_6: zwei Vario-TROs, drei Vario-Motoren), hat jeder TRO seinen eigenen Motor - welcher, sagt die Mehrheit je Foerderer nicht. Dann wird der Antrieb ein Widerspruch (BMK als Vorschlag), Korrektur von Hand. + Rollen in ``skip`` (TRO, Slot, Rolle - von Hand gesetzt) bleiben unberuehrt. """ + skip = skip or set() devices = hw.by_bmk() by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"} 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], []) for role, attr in _DRIVE_ROLES.items(): 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 dev = devices.get(bmk) 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) if overrides is not None: 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) used = {b.bmk for b in bindings if b.bmk} diff --git a/lib/scl_context.py b/lib/scl_context.py index a71153b..7cf84cc 100644 --- a/lib/scl_context.py +++ b/lib/scl_context.py @@ -41,6 +41,7 @@ from typing import TYPE_CHECKING from pydantic import BaseModel, ConfigDict, Field from model import AnlageDocument, ConveyorBinding, JamArea, TroBase, TroDocument +from parameter import get_float from tro_catalog import TroItem if TYPE_CHECKING: @@ -63,11 +64,15 @@ __all__ = [ "HwSource", "build_context", "flow_order", + "jam_names", "reachable_via", ] 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") @@ -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, - jam_id: str = "") -> SclJam: + jam_id: str = "", known: bool = False) -> SclJam: + name = _bekannt if known else _offen 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"), + n=n, jam_id=jam_id, neighbour=neighbour, db=name(jam_db), sensor=sensor, + 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____' -def _jam_db(area: JamArea | None, hw: HwSource) -> str: - """Platzhalter des Staubereichs-DB, mit Foerderernummer soweit bekannt. +def _jam_db(area: JamArea | None, hw: HwSource, + namen: dict[str, str] | None = None) -> tuple[str, bool]: + """Name des Staubereichs-DB und ob er feststeht. - Schema der Referenzen: "DB_JamArea".stJam_. ist der - Foerderer, auf dem der Staubereich liegt (Anlagen-JSON, conveyors[]); - - die laufende Nummer entlang des Flusses auf diesem Foerderer - ist - noch nicht festgelegt (doc/Konzept_Layout_Hardware_SCL.md "Staubereiche"). - Der Name bleibt deshalb immer offen. + Schema der Referenzen: "DB_JamArea".stJam_ (jam_names). + Steht der Name nicht fest, bleibt ein Platzhalter - mit Foerderernummer, + soweit der Layout-Foerderer sie kennt. """ + 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 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): ist der + Foerderer, auf dem der Staubereich liegt (Nummer des Kreisel- bzw. + Vario-Motors), zaehlt auf diesem Foerderer in Fliessrichtung hoch. + = 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], - 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 Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05).""" eingaenge = [] @@ -633,7 +743,8 @@ def _entries(tro: TroBase, eigene: list[JamArea], 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(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 @@ -686,7 +797,8 @@ def _carousel_run(tro: TroBase, hw: HwSource) -> SclName: def _tro_context(tro: TroBase, alle: dict[str, TroBase], 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 n_sep = items.get(TroItem.SEPARATOR, 1) n_swi = items.get(TroItem.SWITCH, 0) @@ -728,15 +840,17 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase], ] 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 Foerderernummer (aus der - # Anlagen-JSON) und laufende Nummer (nicht festgelegt) und bleibt offen. - jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, hw) + # Eingaenge der Nachfolger. Der DB-Name (jam_names) braucht Foerderer- + # nummer und laufende Nummer; ohne beide bleibt er offen. + jam_entries = _entries(tro, bereiche.get(tro_id, []), alle, hw, namen) jam_exits = [] for n, nach in enumerate(tro.successors, start=2): 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____"), - 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( 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(): bereiche.setdefault(area.tro, []).append(area) geordnet, startgrund = flow_order(document.tros, start_id) + namen = jam_names(document, geordnet, hw) return SclFile( source=source, created=created or datetime.now().strftime("%Y-%m-%d %H:%M"), 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 "", hw_source=hw.source if anlage is not None else "", ) diff --git a/lib/scl_skeleton.py b/lib/scl_skeleton.py index 1e7c901..5157c3d 100644 --- a/lib/scl_skeleton.py +++ b/lib/scl_skeleton.py @@ -585,13 +585,15 @@ def unknown_names(context: SclFile, hw: HardwareDocument) -> list[str]: """Eingesetzte Namen, die die Hardwareplanung nicht kennt. 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 |= {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} 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: diff --git a/tests/test_merge_hw_layout.py b/tests/test_merge_hw_layout.py index 2a2a830..58eaca3 100644 --- a/tests/test_merge_hw_layout.py +++ b/tests/test_merge_hw_layout.py @@ -196,6 +196,17 @@ def test_zwei_antriebs_tros_auf_einem_foerderer_sind_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): """hw_import --dxf: E-Symbole stehen schon in Layout-Koordinaten - kein Lagebezug wird ermittelt oder abgelegt.""" diff --git a/tests/test_scl_anlage.py b/tests/test_scl_anlage.py index c086ed2..a1cd5e0 100644 --- a/tests/test_scl_anlage.py +++ b/tests/test_scl_anlage.py @@ -103,15 +103,99 @@ def test_kreisel_freigabe_und_staubereich_aus_der_foerderernummer(): tro01 = _by_id(ctx, "TRO01") assert tro01.carousel_run.value == "#fbCarousel0102.xOutConveyorFullSpeed" assert not tro01.carousel_run.todo - # Staubereich: Foerderer bekannt, laufende Nummer nicht -> weiter offen - assert tro01.jam_entries[0].db.value == '"DB_JamArea0102".stJam0102_____' - assert tro01.jam_entries[0].db.todo - # Ausgang zu TRO03 liegt auf VF_1 (K0106) + # Staubereich am Kreisel: Foerderer K0102, in Fliessrichtung gezaehlt + assert tro01.jam_entries[0].db.value == '"DB_JamArea0102".stJam0102_1' + assert not tro01.jam_entries[0].db.todo + 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.todo # TRO03 sitzt auf einer Strecke, nicht am Kreisel -> Freigabe bleibt offen assert _by_id(ctx, "TRO03").carousel_run.todo +def test_staubereiche_je_foerderer_in_fliessrichtung_gezaehlt(): + """Regel der SPS (Dematic): "DB_JamArea".stJam_, 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(): tro = _by_id(_kontext(anlage()), "TRO03") assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA0210", False)