Files
sps_skel/tests/test_scl_anlage.py
s.ayadi 404d500271 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>
2026-10-22 16:26:10 +02:00

282 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Tests fuer scl_skeleton mit der Anlagen-JSON (Stufe 4).
Die Anlagen-JSON (merge_hw_layout) traegt das Layout und die Zuordnungen der
Hardwareplanung je TRO-ID + Slot + Rolle. scl_context.HwSource setzt daraus
die Namen ein; was offen oder widerspruechlich ist, bleibt TODO(E-Planung).
Die Zuordnungen sind hier von Hand gebaut - wie merge_hw_layout sie findet,
pruefen die Tests in test_merge_hw_layout.py.
"""
import scl_skeleton
from hw_import import HardwareDocument, HwDevice
from model import (
AnlageDocument,
Analysis,
Binding,
ConveyorBinding,
DerivedTro,
LayoutPart,
TroDocument,
)
from scl_context import TODO, build_context
def _tro(tro_id, type_name, seps, hosts, **extra):
return DerivedTro(tro_id=tro_id, type_name=type_name, label=tro_id,
hosts=list(hosts), separators=list(seps), scanners=[],
reason="Test", **extra)
def _part(layout_id, kind, name):
return LayoutPart(layout_id=layout_id, kind=kind, name=name,
position=(0.0, 0.0, 0.0))
def _bind(tro, slot, role, bmk="", constant="", status="zugeordnet"):
return Binding(tro=tro, slot=slot, role=role, bmk=bmk, constant=constant,
status=status)
def anlage() -> AnlageDocument:
"""SYS1 -> TRO01 (1Sep1Swi, Kreisel3) -> TRO02 (1Sep, Kreisel3) / TRO03 (Vario, VF_1)."""
tros = [
_tro("TRO01", "1Sep1Swi", ["0042"], ["0010-R"], predecessors=["SYS1"],
successors=["TRO02", "TRO03"]),
_tro("TRO02", "1Sep", ["0043"], ["0010-L"], predecessors=["TRO01"],
successors=["SYS2"]),
_tro("TRO03", "Vario", ["0062"], ["0015"], predecessors=["TRO01"]),
]
parts = [_part("0010", "Kreisel", "Kreisel3"), _part("0015", "Strecke", "VF_1")]
layout = TroDocument.from_analysis(Analysis(tros=tros), source="anlage.csv",
parts=parts)
return AnlageDocument(
layout_file="anlage_tro.json",
hw_file="Anlage_hw.json",
layout=layout,
bindings=[
_bind("TRO01", "separator1", "sensor", "BG0532", "cInBG0532"),
_bind("TRO01", "separator1", "stopper", "MB0533", "cOutMB0533"),
_bind("TRO01", "switch1", "valve", "MB0540", "cOutMB0540"),
# Widerspruch: Name ist da, aber nicht verlaesslich -> bleibt offen
_bind("TRO02", "separator1", "sensor", "BG0600", "cInBG0600",
status="widerspruch"),
_bind("TRO02", "separator1", "stopper", status="offen"),
_bind("TRO03", "drive", "senJam", "BG0900", "cInBG0900"),
],
conveyors=[
ConveyorBinding(eplan_name="K0102", layout_id="0010", layout_name="Kreisel3",
motor="MA0202", protection="FC0202"),
ConveyorBinding(eplan_name="K0106", layout_id="0015", layout_name="VF_1",
motor="MA0210", protection="FC0210"),
],
)
def _by_id(context, tro_id):
return next(t for t in context.tros if t.id == tro_id)
def _kontext(doc: AnlageDocument):
return build_context(doc.layout, "anlage.json", created="2026-01-01 00:00",
anlage=doc)
def test_separator_und_weiche_aus_den_zuordnungen():
tro = _by_id(_kontext(anlage()), "TRO01")
sep = tro.separators[0]
assert (sep.sensor.value, sep.sensor.todo) == ("cInBG0532", False)
assert (sep.stopper.value, sep.stopper.todo) == ("MB0533", False)
assert (tro.switches[0].valve.value, tro.switches[0].valve.todo) == ("MB0540", False)
# Rolle ohne Zuordnung bleibt offen
assert sep.part_sensor.todo
def test_offen_und_widerspruch_bleiben_todo():
sep = _by_id(_kontext(anlage()), "TRO02").separators[0]
assert (sep.sensor.value, sep.sensor.todo) == ("cInBG____", True)
assert (sep.stopper.value, sep.stopper.todo) == ("MB____", True)
def test_kreisel_freigabe_und_staubereich_aus_der_foerderernummer():
ctx = _kontext(anlage())
tro01 = _by_id(ctx, "TRO01")
assert tro01.carousel_run.value == "#fbCarousel0102.xOutConveyorFullSpeed"
assert not tro01.carousel_run.todo
# 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<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():
tro = _by_id(_kontext(anlage()), "TRO03")
assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA0210", False)
assert (tro.motor_protection.value, tro.motor_protection.todo) == ('"FC0210"', False)
assert tro.vario_sensors["stSenJam"].value == "cInBG0900"
assert tro.vario_sensors["stSenFinger"].todo
def test_widerspruch_im_antrieb_erbt_nicht_den_foerderermotor():
"""Zwei Vario-TROs auf einem Foerderer: merge_hw_layout fuehrt den Antrieb
als Widerspruch - dann nicht doch den Motor des Foerderers einsetzen."""
doc = anlage()
doc.bindings += [
_bind("TRO03", "drive", "motor", "MA0210", "cOutMA0210", status="widerspruch"),
_bind("TRO03", "drive", "motorProtection", "FC0210", "cInFC0210",
status="widerspruch"),
]
tro = _by_id(_kontext(doc), "TRO03")
assert (tro.vario_motor.value, tro.vario_motor.todo) == ("MA____", True)
assert tro.motor_protection.todo
def test_ohne_hardwareplanung_alles_offen():
doc = anlage()
ctx = build_context(doc.layout, "anlage_tro.json")
tro = _by_id(ctx, "TRO01")
assert tro.separators[0].sensor.todo and tro.carousel_run.todo
assert ctx.hw_source == ""
def _hw(*ohne: str) -> HardwareDocument:
namen = [("BG0532", "cInBG0532"), ("MB0533", "cOutMB0533"),
("MB0540", "cOutMB0540"), ("BG0900", "cInBG0900"),
("MA0210", ""), ("FC0210", "")]
return HardwareDocument(devices=[HwDevice(bmk=b, constant=c)
for b, c in namen if b not in ohne])
def _lauf(tmp_path, monkeypatch, *weitere: str, hw: HardwareDocument | None = None):
(tmp_path / "anlage_anlage.json").write_text(anlage().to_json(), encoding="utf-8")
(tmp_path / "Anlage_hw.json").write_text((hw or _hw()).to_json(), encoding="utf-8")
monkeypatch.setenv("SKEL_RESULTS", str(tmp_path))
rc = scl_skeleton.main(["--json", "anlage_anlage.json", *weitere])
return rc, tmp_path / "anlage_FB_Main.scl"
def test_scl_skeleton_liest_die_anlagen_json(tmp_path, monkeypatch, capsys):
rc, scl = _lauf(tmp_path, monkeypatch, "--templates")
assert rc == 0
text = scl.read_text(encoding="utf-8")
assert "// Hardwareplanung: Anlage_hw.json + anlage_tro.json" in text
assert '#TRO01.stInSeparator1.stSenInSep := "DB_Inputs".Sensors["cInBG0532"];\n' in text
assert 'xOutStopper1 => "MB0533",\n' in text
assert "xInCarouselRun1 := #fbCarousel0102.xOutConveyorFullSpeed," in text
assert "0 unbekannte Namen" in capsys.readouterr().out
def test_weniger_offene_stellen_als_mit_der_tro_json(tmp_path, monkeypatch):
rc, scl = _lauf(tmp_path, monkeypatch, "--templates")
mit = scl.read_text(encoding="utf-8").count(TODO)
(tmp_path / "anlage_tro.json").write_text(anlage().layout.to_json(), encoding="utf-8")
assert scl_skeleton.main(["--json", "anlage_tro.json", "--templates",
"--out", "ohne.scl"]) == 0
ohne = (tmp_path / "ohne.scl").read_text(encoding="utf-8").count(TODO)
assert rc == 0 and mit < ohne
def test_eplan_und_anlagen_json_schliessen_sich_aus(tmp_path, monkeypatch):
rc, scl = _lauf(tmp_path, monkeypatch, "--eplan", "x_eplan.json")
assert rc == 1
assert not scl.exists()
def test_unbekannter_name_warnt_und_bricht_mit_strict_ab(tmp_path, monkeypatch, capsys):
rc, scl = _lauf(tmp_path, monkeypatch, hw=_hw("MB0540"))
assert rc == 0 and scl.exists()
assert "WARNUNG: 1 eingesetzte Namen fehlen in Anlage_hw.json: MB0540" in \
capsys.readouterr().err
scl.unlink()
rc, scl = _lauf(tmp_path, monkeypatch, "--strict", hw=_hw("MB0540"))
assert rc == 1
assert not scl.exists()