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
+7 -3
View File
@@ -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}
+135 -20
View File
@@ -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<NNNN>".stJam<NNNN>_<k>. <NNNN> ist der
Foerderer, auf dem der Staubereich liegt (Anlagen-JSON, conveyors[]);
<k> - 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<NNNN>".stJam<NNNN>_<k> (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): <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],
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 "",
)
+4 -2
View File
@@ -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: