hw_import + merge_hw_layout: Hardwareplanung einlesen und mit dem Layout zusammenfuehren
Stufen 2 und 3 aus doc/Konzept_Layout_Hardware_SCL.md: - lib/hw_import.py: TIA-Import (*_TIA.xlsx, ohne openpyxl gelesen) + Positionsdatei (*_positionsconv.json) -> <projekt>_hw.json: je BMK Adresse, Index-Konstante, VERW, Station, Lage, Foerderer (K0102, #106 -> K0106); Antriebe MA/FC/GF; Gegenpruefung beider Quellen. - lib/merge_hw_layout.py: <csv>_tro.json + <projekt>_hw.json -> <csv>_anlage.json (model.AnlageDocument): bindings je TRO-ID + Slot + Rolle mit Konstante und Adresse, Foerderer per Mehrheit (Mubea: K0102 = Kreisel3, K0104 = Kreisel2, K0105 = Kreisel1), Handkorrekturen cfg/hw_overrides.ini. Zuordnungslogik aus e_planning.py hierher verschoben; e_planning re-exportiert sie, Ergebnis unveraendert. - TRO-JSON traegt optional `parts` (Lage/Huellbox, tro_flow --json), PinStore bekommt Slots line<n>. - Gespeicherter Lagebezug mit SHA-256 der Positionsdatei; Unterordner in Eingabenamen relativ zu SKEL_DATA. - Doku: Python_Scripts.md 5d/5e, Konzept, E-Planung_Bruecke (Abgleich mubea_layout.dxf / CSV / E-Planung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c4122b4497
commit
df24cf5adb
+224
-4
@@ -26,9 +26,17 @@ Klassen:
|
||||
Hardwareplanung dort fuellen muss
|
||||
JamArea ein Staubereich: Eingang genau eines TRO (je Separator
|
||||
laut Katalog), mit allen Vorgaengern, die dort einfahren
|
||||
LayoutPart ein Bauteil des Layouts mit Lage und Huellbox - das Ziel
|
||||
der raeumlichen Zuordnung in merge_hw_layout (die TRO-JSON traegt
|
||||
es, damit merge_hw_layout das CSV nicht lesen muss)
|
||||
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
|
||||
Binding eine Zuordnung Hardware -> TRO-Slot-Rolle (merge_hw_layout)
|
||||
ConveyorBinding ein Foerderer der Hardwareplanung (K0102) an einem
|
||||
Layout-Foerderer, mit Antrieb, Motorschutz, Geber
|
||||
AnlageDocument die Anlagen-JSON (<csv>_anlage.json): Layout unveraendert
|
||||
plus bindings[] - von merge_hw_layout geschrieben
|
||||
|
||||
In der JSON stehen die Schluessel wie bisher in der TRO-JSON (`id`, `type`,
|
||||
`fbType`, `candidates`, ...); im Python-Code heissen die Felder weiter
|
||||
@@ -59,22 +67,44 @@ __all__ = [
|
||||
"TroBase",
|
||||
"Slot",
|
||||
"JamArea",
|
||||
"LayoutPart",
|
||||
"DerivedTro",
|
||||
"Connection",
|
||||
"TroCandidate",
|
||||
"Analysis",
|
||||
"TroDocument",
|
||||
"Registration",
|
||||
"Binding",
|
||||
"ConveyorBinding",
|
||||
"AnlageDocument",
|
||||
"ANLAGE_SCHEMA",
|
||||
"BINDING_STATUS",
|
||||
"derive_slots",
|
||||
"derive_jam_areas",
|
||||
"storage_lines",
|
||||
]
|
||||
|
||||
# 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`, 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).
|
||||
# Rendern abgeleitet, siehe TroDocument.jam_areas_or_derived). Seit 2026-10-15
|
||||
# traegt v4 optional `parts` (Lage der Layout-Bauteile fuer merge_hw_layout); eine
|
||||
# v4-Datei ohne `parts` liest sich weiter, nur merge_hw_layout braucht sie.
|
||||
SCHEMA = "sps_skel-tro-v4"
|
||||
|
||||
# Kennung der Anlagen-JSON (<csv>_anlage.json, merge_hw_layout)
|
||||
ANLAGE_SCHEMA = "sps_skel-anlage-v1"
|
||||
|
||||
# Zustand einer Zuordnung (doc/Konzept_Layout_Hardware_SCL.md, Umbau 3):
|
||||
# zugeordnet ein BMK steht fest
|
||||
# offen Pflichtrolle eines Slots ohne BMK -> TODO(E-Planung)
|
||||
# widerspruch BMK steht fest, passt aber nicht (fehlt im TIA-Import,
|
||||
# doppelt vergeben, Handkorrektur auf unbekanntes BMK)
|
||||
# ohne_slot E-Bauteil einem Layout-Bauteil zugeordnet, das (noch) an
|
||||
# keinem TRO-Slot haengt (z. B. Ausschleuselemente)
|
||||
BINDING_STATUS = ("zugeordnet", "offen", "widerspruch", "ohne_slot")
|
||||
|
||||
# Gemeinsame Einstellungen: Pruefung bei Erzeugung und Zuweisung, unbekannte
|
||||
# Schluessel sind ein Fehler (ein umbenannter JSON-Schluessel faellt auf statt
|
||||
# still auf einen Standardwert zurueckzufallen), Aliase und Feldnamen gelten
|
||||
@@ -246,6 +276,34 @@ class JamArea(_Positioned):
|
||||
position: tuple[float, float, float] | None = None
|
||||
|
||||
|
||||
class LayoutPart(_Positioned):
|
||||
"""
|
||||
Ein Bauteil des Layouts (eine CSV-Zeile mit Lage), z. B. Separator,
|
||||
Ausschleuselement, Kreisel.
|
||||
|
||||
Ziel der raeumlichen Zuordnung in merge_hw_layout: die Slots nennen nur die
|
||||
``layout_id``, die Lage steht hier. ``bbox`` ist die achsparallele
|
||||
Ausdehnung (mm) um ``position`` aus der CSV-Spalte "Boundingbox" -
|
||||
gebraucht, um Geraete einem langen Foerderer (Kreisel) zuzuordnen.
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
layout_id: str = Field(alias="layoutId")
|
||||
kind: str
|
||||
name: str = ""
|
||||
position: tuple[float, float, float] | None = None
|
||||
bbox: tuple[float, float, float] | None = None
|
||||
|
||||
def distance_to_box(self, x: float, y: float) -> float | None:
|
||||
"""Abstand eines Punkts zur Huellbox (0 innerhalb); None ohne Lage/Box."""
|
||||
if not self.position or not self.bbox:
|
||||
return None
|
||||
dx = max(abs(x - self.position[0]) - self.bbox[0] / 2, 0.0)
|
||||
dy = max(abs(y - self.position[1]) - self.bbox[1] / 2, 0.0)
|
||||
return (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
|
||||
class DerivedTro(TroBase):
|
||||
"""
|
||||
Ein von tro_flow aus dem CSV abgeleiteter TRO (oder eine Systemgrenze).
|
||||
@@ -346,14 +404,18 @@ class TroDocument(BaseModel):
|
||||
connections: list[Connection] = Field(default_factory=list)
|
||||
candidates: list[TroCandidate] = Field(default_factory=list)
|
||||
jam_areas: list[JamArea] = Field(default_factory=list, alias="jamAreas")
|
||||
# Layout-Bauteile mit Lage (fuer merge_hw_layout); leer in aelteren Dateien
|
||||
parts: list[LayoutPart] = Field(default_factory=list)
|
||||
findings: list[str] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_analysis(cls, analysis: Analysis, source: str, note: str = "") -> TroDocument:
|
||||
def from_analysis(cls, analysis: Analysis, source: str, note: str = "",
|
||||
parts: list[LayoutPart] | None = None) -> TroDocument:
|
||||
"""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.
|
||||
traegt und nicht erst das Rendern sie kennt. ``parts`` (Lage der
|
||||
Layout-Bauteile) gibt tro_flow aus dem CSV mit.
|
||||
"""
|
||||
tros = [t.model_copy(update={"slots": derive_slots(t)}) for t in analysis.tros]
|
||||
return cls(
|
||||
@@ -362,6 +424,7 @@ class TroDocument(BaseModel):
|
||||
tros=tros,
|
||||
boundaries=analysis.boundaries,
|
||||
jam_areas=derive_jam_areas(tros),
|
||||
parts=list(parts or []),
|
||||
connections=[
|
||||
Connection(src=src, dst=dst, kind=analysis.edge_kinds.get((src, dst), ""))
|
||||
for src, dst in analysis.edges
|
||||
@@ -369,6 +432,11 @@ class TroDocument(BaseModel):
|
||||
findings=analysis.findings,
|
||||
)
|
||||
|
||||
def part(self, layout_id: str) -> LayoutPart | None:
|
||||
"""Layout-Bauteil zur TeileId; ein Bahn-Suffix ("0010-R") zaehlt nicht."""
|
||||
base = layout_id.split("-", 1)[0]
|
||||
return next((p for p in self.parts if p.layout_id == base), None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> TroDocument:
|
||||
"""JSON-Text einlesen und pruefen (pydantic.ValidationError bei Fehlern)."""
|
||||
@@ -389,6 +457,138 @@ class TroDocument(BaseModel):
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Anlagen-JSON: Layout + Zuordnungen der Hardwareplanung (merge_hw_layout)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Registration(BaseModel):
|
||||
"""Lagebezug E-Zeichnung -> Layout, mit dem die Zuordnung lief (Nachweis).
|
||||
|
||||
Kopie der Kennwerte aus dxf_registration.Transform - model.py importiert
|
||||
dxf_registration bewusst nicht (Schichten, siehe Kopf).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
dx: float
|
||||
dy: float
|
||||
rotation: float = 0.0
|
||||
source: str = ""
|
||||
matched: int = 0
|
||||
candidates: int = 0
|
||||
residual_max: float = Field(default=0.0, alias="residualMax")
|
||||
description: str = ""
|
||||
|
||||
|
||||
class Binding(BaseModel):
|
||||
"""
|
||||
Eine Zuordnung: Rolle ``role`` im Slot ``slot`` des TRO ``tro`` <- BMK.
|
||||
|
||||
Stabiler Schluessel ist TRO-ID + Slot + Rolle (TRO-IDs aus der ID-Datei);
|
||||
``layout_id`` dient nur zur Nachverfolgung im aktuellen Layoutstand. Bei
|
||||
``status == "ohne_slot"`` sind ``tro``/``slot`` leer und ``role`` ist die
|
||||
Regel (``Ausschleus.stau``). ``constant``/``address`` kommen aus dem
|
||||
TIA-Import und sind das, was im SCL steht (``cInBG0532``, ``%E053.2``).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
tro: str = ""
|
||||
slot: str = ""
|
||||
role: str
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
bmk: str = ""
|
||||
constant: str = ""
|
||||
address: str = ""
|
||||
# positionsdraw | hw_overrides.ini
|
||||
source: str = ""
|
||||
# raeumlich | nummer | hand
|
||||
rule: str = ""
|
||||
distance: float | None = None
|
||||
runner_up: float | None = Field(default=None, alias="runnerUp")
|
||||
status: str = "zugeordnet"
|
||||
note: str = ""
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _known_status(cls, value: str) -> str:
|
||||
if value not in BINDING_STATUS:
|
||||
raise ValueError(f"status '{value}' unbekannt, erlaubt: {', '.join(BINDING_STATUS)}")
|
||||
return value
|
||||
|
||||
|
||||
class ConveyorBinding(BaseModel):
|
||||
"""
|
||||
Ein Foerderer der Hardwareplanung (``K0102``, ``K0106``) an einem
|
||||
Layout-Foerderer (``layout_id`` "0010" = Kreisel3).
|
||||
|
||||
Die Namenswelten passen nicht zusammen (Layout: Kreisel1..3, E-Planung:
|
||||
K0102/K0104/K0105), deshalb raeumlich: alle Feldgeraete, deren
|
||||
Bezeichnung den Foerderer nennt, stimmen fuer den naechsten
|
||||
Layout-Foerderer ab (``votes`` von ``total``).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
eplan_name: str = Field(alias="eplanName")
|
||||
layout_id: str = Field(default="", alias="layoutId")
|
||||
layout_name: str = Field(default="", alias="layoutName")
|
||||
votes: int = 0
|
||||
total: int = 0
|
||||
motor: str = ""
|
||||
protection: str = ""
|
||||
encoder: str = ""
|
||||
# mehrheit | hand
|
||||
rule: str = ""
|
||||
status: str = "zugeordnet"
|
||||
note: str = ""
|
||||
|
||||
|
||||
class AnlageDocument(BaseModel):
|
||||
"""
|
||||
Die Anlagen-JSON ``<csv>_anlage.json``: das Layout (TRO-JSON) unveraendert
|
||||
unter ``layout``, daneben die Zuordnungen der Hardwareplanung.
|
||||
|
||||
Geschrieben von merge_hw_layout, kuenftig die eine Eingabe von scl_skeleton
|
||||
(doc/Konzept_Layout_Hardware_SCL.md, Umbau 4).
|
||||
"""
|
||||
|
||||
model_config = _CONFIG
|
||||
|
||||
schema_id: str = Field(default=ANLAGE_SCHEMA, alias="schema")
|
||||
created: str = Field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
layout_file: str = Field(default="", alias="layoutFile")
|
||||
hw_file: str = Field(default="", alias="hwFile")
|
||||
project: str = ""
|
||||
registration: Registration | None = None
|
||||
layout: TroDocument
|
||||
bindings: list[Binding] = Field(default_factory=list)
|
||||
conveyors: list[ConveyorBinding] = Field(default_factory=list)
|
||||
# Feldgeraete mit Lage, die keine Zuordnung bekommen haben (BMK)
|
||||
unassigned: list[str] = Field(default_factory=list)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
def binding(self, tro_id: str, slot: str, role: str) -> Binding | None:
|
||||
"""Zuordnung einer Slot-Rolle (Rolle ohne "?"), None wenn keine."""
|
||||
role = role.rstrip("?")
|
||||
return next((b for b in self.bindings
|
||||
if b.tro == tro_id and b.slot == slot and b.role == role), None)
|
||||
|
||||
def conveyor_for(self, layout_id: str) -> ConveyorBinding | None:
|
||||
"""Foerderer der Hardwareplanung an einem Layout-Foerderer ("0010-R" geht)."""
|
||||
base = layout_id.split("-", 1)[0]
|
||||
return next((c for c in self.conveyors
|
||||
if c.layout_id == base and c.status == "zugeordnet"), None)
|
||||
|
||||
def to_json(self) -> str:
|
||||
return self.model_dump_json(by_alias=True, indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, text: str) -> AnlageDocument:
|
||||
return cls.model_validate_json(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ableitungen aus Katalogtyp und Topologie
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -399,23 +599,43 @@ _SWITCH_ROLES = ("valve", "exitSensor?")
|
||||
_SCANNER_ROLES = ("reader",)
|
||||
_DRIVE_ROLES = ("motor", "motorProtection", "senCarrInPos", "senFinger",
|
||||
"senLastPos", "senJam")
|
||||
_STORAGE_LINE_ROLES = ("sensor", "stopper", "jamSensor?")
|
||||
|
||||
|
||||
def _at(values: list[str], index: int) -> str:
|
||||
return values[index] if index < len(values) else ""
|
||||
|
||||
|
||||
def storage_lines(tro: TroBase) -> list[str]:
|
||||
"""Separatoren der Lagerlinien eines Speicher-TRO.
|
||||
|
||||
tro_flow legt sie als [steuernde ..., Linien ...] ab; die Linien sind die
|
||||
letzten len(hosts) (eine je Speicherbahn). Fehlen Hosts, zaehlt jeder
|
||||
Separator als Linie - dieselbe Regel wie scl_context._store.
|
||||
"""
|
||||
hosts = list(getattr(tro, "hosts", []) or [])
|
||||
seps = list(tro.separators)
|
||||
count = len(hosts) if 0 < len(hosts) <= len(seps) else len(seps)
|
||||
return seps[len(seps) - count:]
|
||||
|
||||
|
||||
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.
|
||||
bzw. Ausschleuselement heute nicht am TRO. Ein Speicher bekommt je
|
||||
Lagerlinie einen Slot ``line<n>``; der erste Separator-Slot und
|
||||
``line1`` koennen dasselbe Bauteil sein (wie im PinStore-Template).
|
||||
"""
|
||||
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)))
|
||||
if items.get(TroItem.STORAGE_LINE):
|
||||
for n, sep in enumerate(storage_lines(tro), start=1):
|
||||
slots.append(Slot(slot=f"line{n}", layout_id=sep,
|
||||
roles=list(_STORAGE_LINE_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)):
|
||||
|
||||
Reference in New Issue
Block a user