TRO-JSON v4: Slots je TRO und Staubereiche (jamAreas)
- model: Slot (separator/switch/scanner/drive + Rollen), JamArea (J<Separator>, Eingang eines TRO mit allen Vorgaengern), derive_slots, derive_jam_areas; Schema sps_skel-tro-v4, v3-Dateien werden weiter gelesen - scl_context: ein JamEntr je eigenem Staubereich statt je Vorgaenger (wie UH01-UH05), Ausgaenge nennen den Staubereich des Nachfolgers - Golden Files neu erzeugt, Tests fuer Slots/Staubereiche, Doku nachgezogen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dd2d98f2ee
commit
0115ef68b7
+145
-5
@@ -21,6 +21,11 @@ Klassen:
|
||||
Connection eine gerichtete Verbindung zwischen zwei TROs/Stationen
|
||||
TroCandidate eine im Review gemeldete Stelle, an der ein TRO erwartet
|
||||
wird, die aber (noch) keiner ist
|
||||
Slot eine Stelle eines TRO, an die Hardware gehoert (Separator,
|
||||
Weiche, Scanner, Antrieb) mit den Rollen, die die
|
||||
Hardwareplanung dort fuellen muss
|
||||
JamArea ein Staubereich: Eingang genau eines TRO (je Separator
|
||||
laut Katalog), mit allen Vorgaengern, die dort einfahren
|
||||
Analysis Arbeitsergebnis von tro_flow.analyse_tros
|
||||
TroDocument die TRO-JSON (<csv>_tro.json): von tro_flow --json
|
||||
geschrieben, von scl_skeleton gelesen und geprueft
|
||||
@@ -47,22 +52,28 @@ from pydantic import (
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from tro_catalog import TroDefinition, get_tro
|
||||
from tro_catalog import TroDefinition, TroItem, get_tro
|
||||
|
||||
__all__ = [
|
||||
"SCHEMA",
|
||||
"TroBase",
|
||||
"Slot",
|
||||
"JamArea",
|
||||
"DerivedTro",
|
||||
"Connection",
|
||||
"TroCandidate",
|
||||
"Analysis",
|
||||
"TroDocument",
|
||||
"derive_slots",
|
||||
"derive_jam_areas",
|
||||
]
|
||||
|
||||
# Kennung der TRO-JSON. v1 war die Ausgabe von tro_extract (aus der Zeichnung),
|
||||
# v2 die von tro_flow --json (aus dem CSV, ueber dieses Modell), v3 wie v2 mit
|
||||
# `candidates` statt `openPoints`.
|
||||
SCHEMA = "sps_skel-tro-v3"
|
||||
# `candidates` statt `openPoints`, v4 wie v3 mit `slots` je TRO und `jamAreas`.
|
||||
# Eine v3-Datei liest sich weiter (die neuen Felder fehlen dann und werden beim
|
||||
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived).
|
||||
SCHEMA = "sps_skel-tro-v4"
|
||||
|
||||
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
|
||||
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
|
||||
@@ -191,6 +202,50 @@ class TroBase(_Positioned):
|
||||
return data
|
||||
|
||||
|
||||
class Slot(BaseModel):
|
||||
"""
|
||||
Eine Stelle eines TRO, an die Hardware gehoert (Konzept Umbau 1).
|
||||
|
||||
``slot`` ist der stabile Schluessel innerhalb des TRO (``separator1``,
|
||||
``switch1``, ``scanner1``, ``drive``); zusammen mit der TRO-ID adressiert er
|
||||
die Zuordnung der Hardwareplanung. ``layout_id`` ist die TeileId des
|
||||
Bauteils im aktuellen Export - nur zur Nachverfolgung, nicht stabil ("",
|
||||
wenn das Bauteil im Layout nicht bekannt ist). Rollen mit ``?`` sind
|
||||
optional: fehlt das Geraet, wird der zugehoerige xPlugged*-Schalter FALSE.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
slot: str
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
roles: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class JamArea(_Positioned):
|
||||
"""
|
||||
Ein Staubereich: die Warteschlange vor einem Separator, Eingang genau eines
|
||||
TRO (``tro``, Eingang Nummer ``entry``).
|
||||
|
||||
Kennung ``J<Separator>`` (Mubea: Sep1024 -> J1024). Mehrere Vorgaenger
|
||||
koennen in denselben Staubereich einfahren (Zusammenfuehrung); deren
|
||||
Ausgang zeigt dann auf diesen Staubereich. Der SCL-Name
|
||||
``"DB_JamArea<NNNN>".stJam<NNNN>_<k>`` (Standard UH01-UH05) braucht die
|
||||
Foerderernummer aus der Hardwareplanung und steht deshalb nicht hier.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
jam_id: str = Field(alias="id")
|
||||
tro: str
|
||||
entry: int = 1
|
||||
separator: str = ""
|
||||
# tragender Foerderer (erster Host des TRO im Layout)
|
||||
host: str = ""
|
||||
predecessors: list[str] = Field(default_factory=list)
|
||||
note: str = ""
|
||||
position: tuple[float, float, float] | None = None
|
||||
|
||||
|
||||
class DerivedTro(TroBase):
|
||||
"""
|
||||
Ein von tro_flow aus dem CSV abgeleiteter TRO (oder eine Systemgrenze).
|
||||
@@ -207,6 +262,8 @@ class DerivedTro(TroBase):
|
||||
# Mittelpunkt der Bauteile, aus denen dieser TRO gebildet wurde
|
||||
position_from: str = Field(default="", alias="positionFrom")
|
||||
position_points: int = Field(default=0, alias="positionPoints")
|
||||
# Stellen fuer die Hardwareplanung (derive_slots); leer bei Systemgrenzen
|
||||
slots: list[Slot] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
@@ -288,16 +345,23 @@ class TroDocument(BaseModel):
|
||||
boundaries: list[DerivedTro] = Field(default_factory=list)
|
||||
connections: list[Connection] = Field(default_factory=list)
|
||||
candidates: list[TroCandidate] = Field(default_factory=list)
|
||||
jam_areas: list[JamArea] = Field(default_factory=list, alias="jamAreas")
|
||||
findings: list[str] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_analysis(cls, analysis: Analysis, source: str, note: str = "") -> TroDocument:
|
||||
"""Dokument aus dem Ergebnis von tro_flow.analyse_tros bilden."""
|
||||
"""Dokument aus dem Ergebnis von tro_flow.analyse_tros bilden.
|
||||
|
||||
Slots und Staubereiche werden hier abgeleitet, damit die JSON sie
|
||||
traegt und nicht erst das Rendern sie kennt.
|
||||
"""
|
||||
tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros]
|
||||
return cls(
|
||||
source=source,
|
||||
note=note,
|
||||
tros=analysis.tros,
|
||||
tros=tros,
|
||||
boundaries=analysis.boundaries,
|
||||
jam_areas=derive_jam_areas(tros),
|
||||
connections=[
|
||||
Connection(src=src, dst=dst, kind=analysis.edge_kinds.get((src, dst), ""))
|
||||
for src, dst in analysis.edges
|
||||
@@ -310,6 +374,10 @@ class TroDocument(BaseModel):
|
||||
"""JSON-Text einlesen und pruefen (pydantic.ValidationError bei Fehlern)."""
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
def jam_areas_or_derived(self) -> list[JamArea]:
|
||||
"""jamAreas aus der Datei; bei einer v3-Datei (ohne jamAreas) abgeleitet."""
|
||||
return self.jam_areas or derive_jam_areas(self.tros)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Als JSON-Text mit den Schluesseln der TRO-JSON (id, type, fbType ...)."""
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
@@ -319,3 +387,75 @@ class TroDocument(BaseModel):
|
||||
if tro.tro_id == tro_id:
|
||||
return tro
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ableitungen aus Katalogtyp und Topologie
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Rollen je Slot-Art (doc/Konzept_Layout_Hardware_SCL.md, Umbau 1); "?" = optional
|
||||
_SEPARATOR_ROLES = ("sensor", "stopper", "jamSensor?", "partSensor?")
|
||||
_SWITCH_ROLES = ("valve", "exitSensor?")
|
||||
_SCANNER_ROLES = ("reader",)
|
||||
_DRIVE_ROLES = ("motor", "motorProtection", "senCarrInPos", "senFinger",
|
||||
"senLastPos", "senJam")
|
||||
|
||||
|
||||
def _at(values: list[str], index: int) -> str:
|
||||
return values[index] if index < len(values) else ""
|
||||
|
||||
|
||||
def derive_slots(tro: DerivedTro) -> list[Slot]:
|
||||
"""Slots eines TRO: Anzahl aus dem Katalog, Layout-ID aus dem TRO.
|
||||
|
||||
Weichen tragen noch keine Layout-ID - tro_flow speichert das Weichen-
|
||||
bzw. Ausschleuselement heute nicht am TRO.
|
||||
"""
|
||||
items = tro.items
|
||||
slots: list[Slot] = []
|
||||
for i in range(items.get(TroItem.SEPARATOR, 0)):
|
||||
slots.append(Slot(slot=f"separator{i + 1}", layout_id=_at(tro.separators, i),
|
||||
roles=list(_SEPARATOR_ROLES)))
|
||||
for i in range(items.get(TroItem.SWITCH, 0)):
|
||||
slots.append(Slot(slot=f"switch{i + 1}", roles=list(_SWITCH_ROLES)))
|
||||
for i in range(items.get(TroItem.SCANNER, 0)):
|
||||
slots.append(Slot(slot=f"scanner{i + 1}", layout_id=_at(tro.scanners, i),
|
||||
roles=list(_SCANNER_ROLES)))
|
||||
if items.get(TroItem.VARIO_DRIVE):
|
||||
slots.append(Slot(slot="drive", layout_id=_at(tro.hosts, 0),
|
||||
roles=list(_DRIVE_ROLES)))
|
||||
return slots
|
||||
|
||||
|
||||
def derive_jam_areas(tros: list[DerivedTro]) -> list[JamArea]:
|
||||
"""Staubereiche: je Separator laut Katalog ein Eingang des TRO.
|
||||
|
||||
Kennung J<Separator>; fehlt der Separator im Layout, J<TRO-ID>_<n>. Alle
|
||||
Vorgaenger fahren in den ersten Eingang ein - bei zwei Eingaengen
|
||||
(2Sep-Typen) ist die Aufteilung aus dem Layout nicht ableitbar und als
|
||||
Hinweis vermerkt.
|
||||
"""
|
||||
areas: list[JamArea] = []
|
||||
for tro in tros:
|
||||
n_entries = max(1, tro.items.get(TroItem.SEPARATOR, 1))
|
||||
if n_entries > 1 and tro.predecessors:
|
||||
note = "Aufteilung der Vorgaenger auf die Eingaenge offen"
|
||||
elif tro.items.get(TroItem.STORAGE_LINE):
|
||||
# Die Separatoren eines Speichers sitzen auf den Lagerlinien; der
|
||||
# Eingang liegt davor und hat im Layout keinen eigenen Separator.
|
||||
note = "Eingang des Speichers - Kennung vorlaeufig nach erstem Linien-Separator"
|
||||
else:
|
||||
note = ""
|
||||
for n in range(1, n_entries + 1):
|
||||
sep = _at(tro.separators, n - 1)
|
||||
areas.append(JamArea(
|
||||
jam_id=f"J{sep}" if sep else f"J{tro.tro_id}_{n}",
|
||||
tro=tro.tro_id,
|
||||
entry=n,
|
||||
separator=sep,
|
||||
host=_at(tro.hosts, 0),
|
||||
predecessors=list(tro.predecessors) if n == 1 else [],
|
||||
note=note,
|
||||
position=tro.position if n == 1 else None,
|
||||
))
|
||||
return areas
|
||||
|
||||
+50
-13
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from model import TroBase, TroDocument
|
||||
from model import JamArea, TroBase, TroDocument
|
||||
from tro_catalog import TroItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -132,17 +132,29 @@ class SclScanner(BaseModel):
|
||||
|
||||
|
||||
class SclJam(BaseModel):
|
||||
"""Ein Staubereich am Ein- oder Ausgang (JamEntr<n> / JamExit<n>)."""
|
||||
"""Ein Staubereich am Ein- oder Ausgang (JamEntr<n> / JamExit<n>).
|
||||
|
||||
Ein Eingang ist ein Staubereich des TRO selbst (TroDocument.jamAreas), ein
|
||||
Ausgang der Eingangs-Staubereich des Nachfolgers.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
n: int
|
||||
# Nachbar als Text fuer den Kommentar, SYSn als Station markiert
|
||||
# Kennung aus der TRO-JSON (J<Separator>); "" am Ausgang zu einer Station
|
||||
jam_id: str = ""
|
||||
# Eingang: die Vorgaenger, Ausgang: der Nachfolger - als Text fuer den
|
||||
# Kommentar, SYSn als Station markiert
|
||||
neighbour: str
|
||||
db: SclName
|
||||
# Stausensor des Ausgangs (stInSenJam<n>); None an Eingaengen
|
||||
sensor: SclName | None = None
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Fuer den Kommentar: 'Staubereich J0003' (ohne Kennung nur 'Staubereich')."""
|
||||
return f"Staubereich {self.jam_id}" if self.jam_id else "Staubereich"
|
||||
|
||||
|
||||
class SclTro(BaseModel):
|
||||
"""Alles, was eine REGION fuer einen TRO braucht."""
|
||||
@@ -314,14 +326,38 @@ def _separator(i: int, layout: str, eplan: EplanDocument | None) -> SclSeparator
|
||||
)
|
||||
|
||||
|
||||
def _entries(tro: TroBase, eigene: list[JamArea],
|
||||
alle: dict[str, TroBase], jam_db: str) -> list[SclJam]:
|
||||
"""Ein JamEntr je eigenem Staubereich - nicht je Vorgaenger: mehrere
|
||||
Vorgaenger fahren in denselben Staubereich ein (Referenzen UH01-UH05)."""
|
||||
eingaenge = []
|
||||
for area in eigene:
|
||||
von = ", ".join(_nachbar(v, alle) for v in area.predecessors) or "-"
|
||||
if area.note:
|
||||
von = f"{von} ({area.note})"
|
||||
eingaenge.append(SclJam(n=area.entry, jam_id=area.jam_id,
|
||||
neighbour=von, db=_offen(jam_db)))
|
||||
return eingaenge
|
||||
|
||||
|
||||
def _exit_area(tro_id: str, nach: str,
|
||||
bereiche: dict[str, list[JamArea]]) -> str:
|
||||
"""Kennung des Staubereichs, in den tro_id beim Nachfolger einfaehrt."""
|
||||
ziel = bereiche.get(nach, [])
|
||||
passend = [a for a in ziel if tro_id in a.predecessors] or ziel
|
||||
return passend[0].jam_id if passend else ""
|
||||
|
||||
|
||||
def _tro_context(tro: TroBase, alle: dict[str, TroBase],
|
||||
eplan: EplanDocument | None = None) -> SclTro:
|
||||
eplan: EplanDocument | None = None,
|
||||
bereiche: dict[str, list[JamArea]] | None = None) -> SclTro:
|
||||
items = tro.items
|
||||
n_sep = items.get(TroItem.SEPARATOR, 1)
|
||||
n_swi = items.get(TroItem.SWITCH, 0)
|
||||
n_scan = items.get(TroItem.SCANNER, 0)
|
||||
tro_id = tro.tro_id
|
||||
jam_db = '"DB_JamArea____".stJam____'
|
||||
bereiche = bereiche or {}
|
||||
|
||||
separators = [
|
||||
_separator(i, tro.separators[i - 1] if i <= len(tro.separators) else "?", eplan)
|
||||
@@ -371,15 +407,13 @@ def _tro_context(tro: TroBase, alle: dict[str, TroBase],
|
||||
for i in range(1, n_scan + 1)
|
||||
],
|
||||
storage_lines=items.get(TroItem.STORAGE_LINE, 0),
|
||||
# Staubereiche: je ankommender/abgehender Verbindung einer. Welche
|
||||
# DB-Struktur dahintersteht, ist eine Planungsentscheidung - hier steht
|
||||
# nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren.
|
||||
jam_entries=[
|
||||
SclJam(n=n, neighbour=_nachbar(vor, alle), db=_offen(jam_db))
|
||||
for n, vor in enumerate(tro.predecessors, start=1)
|
||||
],
|
||||
# Staubereiche aus der TRO-JSON (jamAreas): Eingaenge sind die eigenen,
|
||||
# Ausgaenge die Eingaenge der Nachfolger. Der DB-Name braucht die
|
||||
# Foerderernummer aus der Hardwareplanung und bleibt offen.
|
||||
jam_entries=_entries(tro, bereiche.get(tro_id, []), alle, jam_db),
|
||||
jam_exits=[
|
||||
SclJam(n=n, neighbour=_nachbar(nach, alle), db=_offen(jam_db),
|
||||
SclJam(n=n, jam_id=_exit_area(tro_id, nach, bereiche),
|
||||
neighbour=_nachbar(nach, alle), db=_offen(jam_db),
|
||||
sensor=_offen("cInBG____"))
|
||||
for n, nach in enumerate(tro.successors, start=2)
|
||||
],
|
||||
@@ -406,12 +440,15 @@ def build_context(document: TroDocument, source: str, start_id: str = "",
|
||||
alle Namen offen und die Ausgabe ist die bisherige.
|
||||
"""
|
||||
alle = {t.tro_id: t for t in document.tros}
|
||||
bereiche: dict[str, list[JamArea]] = {}
|
||||
for area in document.jam_areas_or_derived():
|
||||
bereiche.setdefault(area.tro, []).append(area)
|
||||
geordnet, startgrund = flow_order(document.tros, start_id)
|
||||
return SclFile(
|
||||
source=source,
|
||||
created=created or datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
start_reason=startgrund,
|
||||
tros=[_tro_context(t, alle, eplan) for t in geordnet],
|
||||
tros=[_tro_context(t, alle, eplan, bereiche) for t in geordnet],
|
||||
eplan_source=(f"{eplan.eplan_file} (Zuordnung vom {eplan.created})"
|
||||
if eplan is not None else ""),
|
||||
)
|
||||
|
||||
+2
-2
@@ -221,11 +221,11 @@ def emit_call(add, tro: SclTro) -> None:
|
||||
|
||||
for jam in tro.jam_entries:
|
||||
zeilen.append((f"arInOutJamEntr{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"Staubereich von {jam.neighbour}")))
|
||||
offen(jam.db, f"{jam.title} von {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamEntr{jam.n} := {jam.db.value}.stData", ""))
|
||||
for jam in tro.jam_exits:
|
||||
zeilen.append((f"arInOutJamExit{jam.n} := {jam.db.value}.arCarrier",
|
||||
offen(jam.db, f"Staubereich nach {jam.neighbour}")))
|
||||
offen(jam.db, f"{jam.title} vor {jam.neighbour}")))
|
||||
zeilen.append((f"stInOutJamExit{jam.n} := {jam.db.value}.stData", ""))
|
||||
|
||||
for s in tro.separators:
|
||||
|
||||
@@ -42,11 +42,11 @@ REGION {{ t.region_title }}
|
||||
{% set _ = z.append(("stInOutMachineState := " ~ t.machine_state.value, m.note(t.machine_state, "Schaltschrank"))) %}
|
||||
{% set _ = z.append(("stInOutCognexInterface := #stCognexInterfaceDummy", "")) %}
|
||||
{% for j in t.jam_entries %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, "Staubereich von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("arInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " von " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamEntr" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% for j in t.jam_exits %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, "Staubereich nach " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("arInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".arCarrier", m.note(j.db, j.title ~ " vor " ~ j.neighbour))) %}
|
||||
{% set _ = z.append(("stInOutJamExit" ~ j.n ~ " := " ~ j.db.value ~ ".stData", m.note(j.db))) %}
|
||||
{% endfor %}
|
||||
{% set _ = z.append(('stInOutHMI := "DB_Interface_HMI".stTRO.' ~ t.id, "")) %}
|
||||
|
||||
Reference in New Issue
Block a user