404d500271
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>
282 lines
12 KiB
Python
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()
|
|
|