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>
970 lines
41 KiB
Python
970 lines
41 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3"
|
|
# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist.
|
|
"""
|
|
merge_hw_layout.py - Layout und Hardwareplanung zusammenfuehren -> <csv>_anlage.json.
|
|
|
|
Stufe 3 des Ablaufs in doc/Konzept_Layout_Hardware_SCL.md:
|
|
|
|
<csv>_tro.json (tro_flow --json) + <projekt>_hw.json (hw_import)
|
|
\\ /
|
|
merge_hw_layout -> <csv>_anlage.json + Bericht .md
|
|
|
|
Das CSV wird nicht gelesen: die TRO-JSON traegt die Slots der TROs und die
|
|
Lage der Layout-Bauteile (``parts``). Ablauf:
|
|
|
|
1. Lagebezug E-Zeichnung -> Layout (lib/dxf_registration.py) ueber die
|
|
Separator-Sensoren (VERW "IN SEP") gegen die Separatoren des Layouts;
|
|
abgelegt in SKEL_CFG/dxf_registration.json mit Pruefsumme der
|
|
Positionsdatei - ein neuer Stand gleichen Namens wird neu ermittelt.
|
|
2. Regeln aus SKEL_CFG/e_planning.ini (raeumlich mit Hoechst-/Sollabstand,
|
|
Nummernregel MB = BG + 1) ordnen E-Bauteile Layout-Bauteilen zu.
|
|
3. Jede Zuordnung geht an die Slot-Rolle des TRO, der das Bauteil traegt
|
|
(Schluessel TRO-ID + Slot + Rolle). Bauteile ohne Slot (Ausschleus-
|
|
elemente, Weichen) bleiben als ``ohne_slot`` sichtbar.
|
|
4. Foerderer: die Namen der E-Planung (K0102) und des Layouts (Kreisel3)
|
|
passen nicht zusammen. Alle Feldgeraete, deren Klartext einen Foerderer
|
|
nennt, stimmen fuer den naechsten Layout-Foerderer ab; die Mehrheit
|
|
entscheidet (cfg/parameter.ini [merge_hw_layout]). Daraus Antrieb, Motorschutz
|
|
und Geber je Layout-Foerderer und die Antriebs-Slots der Vario-TROs.
|
|
5. Handkorrekturen aus SKEL_CFG/hw_overrides.ini, zuletzt.
|
|
6. Pflichtrollen ohne BMK werden ``offen`` (spaeter TODO(E-Planung)), ein
|
|
BMK, das nicht im TIA-Import steht, ``widerspruch``.
|
|
|
|
Lieber eine Luecke als ein falscher Name: was keine Regel sicher trifft,
|
|
bleibt offen und steht im Bericht.
|
|
|
|
Die Zuordnungslogik (Regeln, raeumlich, Nummernregel) stand bis 2026-10-15
|
|
in e_planning.py; e_planning importiert sie jetzt von hier, damit
|
|
``scl_skeleton --eplan`` bis zur Umstellung auf die Anlagen-JSON weiterlaeuft.
|
|
|
|
Aufruf ueber bin/merge_hw_layout.bat bzw. bin/merge_hw_layout.sh.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import configparser
|
|
import fnmatch
|
|
import math
|
|
import sys
|
|
from collections import Counter
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
|
|
from dxf_registration import (
|
|
RegistrationError,
|
|
Transform,
|
|
derive_transform,
|
|
load_transform,
|
|
save_transform,
|
|
verify_transform,
|
|
)
|
|
from hw_import import HW_SCHEMA, EDevice, HardwareDocument, HwDevice
|
|
from model import (
|
|
AnlageDocument,
|
|
Binding,
|
|
ConveyorBinding,
|
|
LayoutPart,
|
|
Registration,
|
|
TroDocument,
|
|
)
|
|
from parameter import get_float, get_int
|
|
|
|
__all__ = [
|
|
"RULES_FILE",
|
|
"OVERRIDES_FILE",
|
|
"LAYOUT_KINDS",
|
|
"CONVEYOR_KINDS",
|
|
"LayoutPoint",
|
|
"Rule",
|
|
"Assignment",
|
|
"load_rules",
|
|
"assign",
|
|
"resolve_transform",
|
|
"points_from_parts",
|
|
"bind_slots",
|
|
"assign_conveyors",
|
|
"load_overrides",
|
|
"build_anlage",
|
|
]
|
|
|
|
RULES_FILE = "e_planning.ini"
|
|
OVERRIDES_FILE = "hw_overrides.ini"
|
|
|
|
# Zahlenwerte aus cfg/parameter.ini [merge_hw_layout] - Bedeutung und Einheit stehen dort.
|
|
_P = "merge_hw_layout"
|
|
DEFAULT_TOLERANCE_MM = get_float(_P, "toleranz_mm")
|
|
CONVEYOR_MAX_MM = get_float(_P, "conveyor_max_mm")
|
|
CONVEYOR_MIN_VOTES = get_int(_P, "conveyor_min_votes")
|
|
CONVEYOR_MIN_SHARE = get_float(_P, "conveyor_min_share")
|
|
REGISTRATION_MIN_SHARE = get_float(_P, "registration_min_share")
|
|
|
|
_CONFIG = ConfigDict(extra="forbid")
|
|
|
|
# Punktartige Layout-Bauteile (Element.kind bzw. Teil der TeileArt)
|
|
LAYOUT_KINDS = ("Separator", "Scanner", "Weiche", "Ausschleus", "Einschleus",
|
|
"Beladung", "Entladung")
|
|
# Foerderer, die fuer einen Foerderer der E-Planung in Frage kommen
|
|
CONVEYOR_KINDS = ("Kreisel", "Strecke", "Gefaellestrecke")
|
|
# Antriebs-Rollen eines Slots, die aus dem zugeordneten Foerderer folgen
|
|
_DRIVE_ROLES = {"motor": "motor", "motorProtection": "protection"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regeln und Zuordnung (bis 2026-10-15 in e_planning.py)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class LayoutPoint:
|
|
"""Ein punktartiges Bauteil des Layouts in Layout-Koordinaten."""
|
|
|
|
layout_id: str
|
|
kind: str
|
|
x: float
|
|
y: float
|
|
name: str = ""
|
|
|
|
|
|
@dataclass
|
|
class Rule:
|
|
"""Eine Zuordnungsregel aus e_planning.ini (Abschnitt "<Art>.<Rolle>")."""
|
|
|
|
role: str # "Separator.sensor"
|
|
layout: list[str] # Muster fuer die Layout-Art
|
|
verw: list[str] # Muster fuer VERW
|
|
max_mm: float | None = None # raeumlich: Hoechstabstand
|
|
number_from: str = "" # Nummernregel: Rolle des Partners ...
|
|
number_offset: int = 0 # ... und Versatz der Nummer
|
|
prefix: str = "" # BMK-Praefix, leer = egal
|
|
# Sollabstand: das Symbol liegt immer gleich weit neben dem Bauteil.
|
|
# Dann zaehlt die Abweichung vom Soll, nicht der kleinste Abstand.
|
|
target_mm: float | None = None
|
|
tolerance_mm: float = DEFAULT_TOLERANCE_MM
|
|
|
|
@property
|
|
def slot_role(self) -> str:
|
|
"""Rolle im Slot: "Separator.sensor" -> "sensor"."""
|
|
return self.role.split(".", 1)[-1]
|
|
|
|
def score(self, dist: float) -> float | None:
|
|
"""Rangfolge eines Kandidaten (kleiner = besser), None = nicht zulaessig."""
|
|
if self.max_mm is not None and dist > self.max_mm:
|
|
return None
|
|
if self.target_mm is None:
|
|
return dist
|
|
dev = abs(dist - self.target_mm)
|
|
return dev if dev <= self.tolerance_mm else None
|
|
|
|
|
|
class Assignment(BaseModel):
|
|
"""Ein E-Bauteil in einer Rolle an einem Layout-Bauteil."""
|
|
|
|
model_config = _CONFIG
|
|
|
|
layout_id: str
|
|
layout_kind: str
|
|
role: str
|
|
bmk: str
|
|
verw: str = ""
|
|
bezeichnung: str = ""
|
|
controller: str = ""
|
|
station: str = ""
|
|
# Abstand nach dem Lagebezug in mm (auch bei der Nummernregel zur Kontrolle)
|
|
distance: float
|
|
# "raeumlich" | "nummer"
|
|
rule: str
|
|
# Abstand des naechstbesten Kandidaten derselben Rolle (None: keiner)
|
|
runner_up: float | None = None
|
|
|
|
|
|
def _patterns(text: str) -> list[str]:
|
|
return [p.strip() for p in text.split(",") if p.strip()]
|
|
|
|
|
|
def _match(text: str, patterns: list[str]) -> bool:
|
|
return any(fnmatch.fnmatchcase(text.upper(), p.upper()) for p in patterns)
|
|
|
|
|
|
def load_rules(path: Path) -> tuple[list[Rule], list[str]]:
|
|
"""Regeln und Anker-Verwendungen aus e_planning.ini lesen.
|
|
|
|
Abschnitt [Registrierung] -> anker (VERW-Muster fuer den Lagebezug);
|
|
jeder Abschnitt "<Art>.<Rolle>" ist eine Regel, in Dateireihenfolge.
|
|
"""
|
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
|
interpolation=None)
|
|
parser.optionxform = str # Schluessel nicht kleinschreiben
|
|
with path.open(encoding="utf-8") as fh:
|
|
parser.read_file(fh)
|
|
anchors = _patterns(parser.get("Registrierung", "anker", fallback="IN SEP"))
|
|
rules = []
|
|
for section in parser.sections():
|
|
if "." not in section:
|
|
continue
|
|
s = parser[section]
|
|
max_mm = s.get("max_mm")
|
|
soll = s.get("soll_mm")
|
|
rules.append(Rule(
|
|
role=section,
|
|
layout=_patterns(s.get("layout", section.split(".", 1)[0])),
|
|
verw=_patterns(s.get("verw", "*")),
|
|
max_mm=float(max_mm) if max_mm else None,
|
|
number_from=s.get("nummer_von", "").strip(),
|
|
number_offset=int(s.get("nummer_versatz", "0")),
|
|
prefix=s.get("praefix", "").strip(),
|
|
target_mm=float(soll) if soll else None,
|
|
tolerance_mm=float(s.get("toleranz_mm", str(DEFAULT_TOLERANCE_MM))),
|
|
))
|
|
known = {r.role for r in rules}
|
|
for r in rules:
|
|
if r.number_from and r.number_from not in known:
|
|
raise ValueError(f"{path.name} [{r.role}]: nummer_von = {r.number_from} "
|
|
f"ist keine Regel")
|
|
return rules, anchors
|
|
|
|
|
|
@dataclass
|
|
class _State:
|
|
positions: dict[str, tuple[float, float]]
|
|
used: set[str] = field(default_factory=set)
|
|
result: list[Assignment] = field(default_factory=list)
|
|
# (layout_id, role) -> Assignment
|
|
by_slot: dict[tuple[str, str], Assignment] = field(default_factory=dict)
|
|
|
|
|
|
def _make(point: LayoutPoint, rule: Rule, dev: EDevice, dist: float, how: str,
|
|
runner_up: float | None) -> Assignment:
|
|
return Assignment(
|
|
layout_id=point.layout_id, layout_kind=point.kind, role=rule.role,
|
|
bmk=dev.bmk, verw=dev.verw, bezeichnung=dev.bezeichnung,
|
|
controller=dev.controller, station=dev.station,
|
|
distance=round(dist, 1), rule=how,
|
|
runner_up=None if runner_up is None else round(runner_up, 1),
|
|
)
|
|
|
|
|
|
def _candidates(rule: Rule, devices: list[EDevice], state: _State) -> list[EDevice]:
|
|
return [
|
|
d for d in devices
|
|
if d.bmk not in state.used and d.bmk in state.positions
|
|
and _match(d.verw, rule.verw)
|
|
and (not rule.prefix or d.prefix.upper() == rule.prefix.upper())
|
|
]
|
|
|
|
|
|
def _assign_spatial(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
|
|
state: _State) -> None:
|
|
"""Beste Bewertung zuerst (Rule.score); je Bauteil und E-Bauteil hoechstens einmal."""
|
|
cands = _candidates(rule, devices, state)
|
|
pairs = []
|
|
for p in points:
|
|
if (p.layout_id, rule.role) in state.by_slot:
|
|
continue
|
|
for d in cands:
|
|
dist = math.dist((p.x, p.y), state.positions[d.bmk])
|
|
score = rule.score(dist)
|
|
if score is not None:
|
|
pairs.append((score, dist, p.layout_id, d.bmk))
|
|
pairs.sort()
|
|
# je Bauteil die Kandidaten in Rangfolge - fuer "naechstbester Kandidat"
|
|
by_point: dict[str, list[tuple[str, float]]] = {}
|
|
for _, dist, pid, bmk in pairs:
|
|
by_point.setdefault(pid, []).append((bmk, dist))
|
|
point_of = {p.layout_id: p for p in points}
|
|
dev_of = {d.bmk: d for d in cands}
|
|
for _, dist, pid, bmk in pairs:
|
|
if (pid, rule.role) in state.by_slot or bmk in state.used:
|
|
continue
|
|
others = [x for b, x in by_point[pid] if b != bmk]
|
|
a = _make(point_of[pid], rule, dev_of[bmk], dist, "raeumlich",
|
|
others[0] if others else None)
|
|
state.used.add(bmk)
|
|
state.by_slot[(pid, rule.role)] = a
|
|
state.result.append(a)
|
|
|
|
|
|
def _assign_by_number(rule: Rule, points: list[LayoutPoint], devices: list[EDevice],
|
|
state: _State) -> None:
|
|
"""Nummer = Nummer des Partners (Rolle nummer_von) + Versatz."""
|
|
cands = {(d.number): d for d in _candidates(rule, devices, state)
|
|
if d.number is not None}
|
|
for p in points:
|
|
if (p.layout_id, rule.role) in state.by_slot:
|
|
continue
|
|
partner = state.by_slot.get((p.layout_id, rule.number_from))
|
|
if partner is None or partner.bmk == "":
|
|
continue
|
|
number = next((d.number for d in devices if d.bmk == partner.bmk), None)
|
|
if number is None:
|
|
continue
|
|
dev = cands.get(number + rule.number_offset)
|
|
if dev is None or dev.bmk in state.used:
|
|
continue
|
|
dist = math.dist((p.x, p.y), state.positions[dev.bmk])
|
|
# Auch die Nummernregel darf nicht quer ueber die Anlage greifen
|
|
if rule.max_mm is not None and dist > rule.max_mm:
|
|
continue
|
|
a = _make(p, rule, dev, dist, "nummer", None)
|
|
state.used.add(dev.bmk)
|
|
state.by_slot[(p.layout_id, rule.role)] = a
|
|
state.result.append(a)
|
|
|
|
|
|
def assign(points: list[LayoutPoint], devices: list[EDevice], rules: list[Rule],
|
|
transform: Transform) -> list[Assignment]:
|
|
"""Regeln der Reihe nach anwenden.
|
|
|
|
Eine Regel mit nummer_von versucht zuerst die Nummernregel und nimmt fuer
|
|
die verbleibenden Bauteile den Abstand (wenn max_mm/soll_mm gesetzt ist). So wird
|
|
das Ventil eines Separators ueber die Nummer gefunden, auch wenn das
|
|
Ventil des Nachbarn naeher liegt (Speicherlinien im 800-mm-Raster).
|
|
"""
|
|
state = _State(positions={
|
|
d.bmk: transform.to_csv(*d.pos) for d in devices
|
|
if d.pos is not None and not d.cabinet
|
|
})
|
|
for rule in rules:
|
|
targets = [p for p in points if _match(p.kind, rule.layout)]
|
|
if rule.number_from:
|
|
_assign_by_number(rule, targets, devices, state)
|
|
if rule.max_mm is not None or rule.target_mm is not None:
|
|
_assign_spatial(rule, targets, devices, state)
|
|
return sorted(state.result, key=lambda a: (a.layout_id, a.role))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lagebezug
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _anchor_points(points: list[LayoutPoint], devices: list[EDevice],
|
|
anchors: list[str]):
|
|
layout_pts = {"Separator": [(p.x, p.y) for p in points if p.kind == "Separator"]}
|
|
e_pts = {"Separator": [d.pos for d in devices
|
|
if d.pos is not None and not d.cabinet
|
|
and _match(d.verw, anchors)]}
|
|
return layout_pts, e_pts
|
|
|
|
|
|
def resolve_transform(points, devices, anchors, cfg_dir: Path, csv_name: str,
|
|
eplan_name: str, re_register: bool,
|
|
warnings: list[str], fingerprint: str = "") -> Transform:
|
|
"""Abgelegten Lagebezug nachrechnen oder neu ermitteln und ablegen.
|
|
|
|
Mit ``fingerprint`` (Pruefsumme der Positionsdatei) gilt ein abgelegter
|
|
Bezug nur fuer genau diesen Dateistand.
|
|
"""
|
|
layout_pts, e_pts = _anchor_points(points, devices, anchors)
|
|
if not re_register:
|
|
stored = load_transform(cfg_dir, csv_name, eplan_name, fingerprint)
|
|
if stored:
|
|
verify_transform(stored, layout_pts, e_pts)
|
|
return stored
|
|
transform = derive_transform(layout_pts, e_pts, csv_name, eplan_name)
|
|
transform.fingerprint = fingerprint
|
|
saved = save_transform(cfg_dir, transform)
|
|
warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}")
|
|
return transform
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slots
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def points_from_parts(parts: list[LayoutPart]) -> list[LayoutPoint]:
|
|
"""Punktartige Bauteile aus TroDocument.parts."""
|
|
points = []
|
|
for p in parts:
|
|
if p.position is None:
|
|
continue
|
|
kind = next((k for k in LAYOUT_KINDS if k.lower() in p.kind.lower()), "")
|
|
if kind:
|
|
points.append(LayoutPoint(p.layout_id, kind, p.position[0], p.position[1],
|
|
p.name))
|
|
return points
|
|
|
|
|
|
def _binding_from(a: Assignment, device: HwDevice | None, tro: str = "",
|
|
slot: str = "", role: str = "") -> Binding:
|
|
status, note = "zugeordnet", ""
|
|
if device is None or not device.in_tia:
|
|
status, note = "widerspruch", "BMK nicht im TIA-Import"
|
|
if not tro:
|
|
status = "ohne_slot" if status == "zugeordnet" else status
|
|
note = note or f"{a.layout_kind} {a.layout_id} haengt an keinem TRO-Slot"
|
|
return Binding(
|
|
tro=tro, slot=slot, role=role or a.role, layout_id=a.layout_id, bmk=a.bmk,
|
|
constant=device.constant if device else "",
|
|
address=device.address if device else "",
|
|
source="positionsdraw", rule=a.rule, distance=a.distance,
|
|
runner_up=a.runner_up, status=status, note=note,
|
|
)
|
|
|
|
|
|
def bind_slots(doc: TroDocument, assignments: list[Assignment],
|
|
rules: list[Rule], hw: HardwareDocument) -> list[Binding]:
|
|
"""Zuordnungen an die Slot-Rollen der TROs haengen.
|
|
|
|
Ein Bauteil kann in zwei Slots desselben TRO stehen (Speicher:
|
|
``separator1`` und ``line1``) - dann bekommen beide die Zuordnung.
|
|
"""
|
|
devices = hw.by_bmk()
|
|
slots_of: dict[str, list[tuple[str, str, list[str]]]] = {}
|
|
for tro in doc.tros:
|
|
for s in tro.slots:
|
|
if s.layout_id:
|
|
roles = [r.rstrip("?") for r in s.roles]
|
|
slots_of.setdefault(s.layout_id, []).append((tro.tro_id, s.slot, roles))
|
|
rule_of = {r.role: r for r in rules}
|
|
bindings: list[Binding] = []
|
|
for a in assignments:
|
|
role = rule_of[a.role].slot_role if a.role in rule_of else a.role.split(".")[-1]
|
|
targets = [(t, s) for t, s, roles in slots_of.get(a.layout_id, []) if role in roles]
|
|
if not targets:
|
|
bindings.append(_binding_from(a, devices.get(a.bmk)))
|
|
continue
|
|
for tro_id, slot in targets:
|
|
bindings.append(_binding_from(a, devices.get(a.bmk), tro_id, slot, role))
|
|
return bindings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Foerderer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def assign_conveyors(doc: TroDocument, hw: HardwareDocument, transform: Transform
|
|
) -> list[ConveyorBinding]:
|
|
"""Foerderer der E-Planung (K0102) per Mehrheit einem Layout-Foerderer zuordnen.
|
|
|
|
Jedes Feldgeraet, dessen Klartext einen Foerderer nennt, stimmt fuer den
|
|
Layout-Foerderer, dessen Huellbox ihm am naechsten liegt (hoechstens
|
|
CONVEYOR_MAX_MM). Zugeordnet wird bei mindestens CONVEYOR_MIN_VOTES
|
|
Stimmen und CONVEYOR_MIN_SHARE Anteil; je Layout-Foerderer hoechstens ein
|
|
Foerderer der E-Planung (der mit den meisten Stimmen).
|
|
"""
|
|
conveyors = [p for p in doc.parts if p.kind in CONVEYOR_KINDS and p.bbox and p.position]
|
|
drives = {dr.conveyor: dr for dr in hw.drives if dr.conveyor}
|
|
kind_of = _conveyor_kinds(hw)
|
|
votes: dict[str, Counter] = {}
|
|
totals: Counter = Counter()
|
|
for d in hw.field_devices():
|
|
if not d.conveyor:
|
|
continue
|
|
totals[d.conveyor] += 1
|
|
# Die Huellboxen ueberlappen (Speicherlinien reichen bis an den
|
|
# Kreisel); nennt der Klartext die Art, stimmen nur Foerderer dieser Art.
|
|
kinds = _LAYOUT_KINDS_FOR.get(kind_of.get(d.conveyor, ""), CONVEYOR_KINDS)
|
|
x, y = transform.to_csv(*d.pos)
|
|
best = min(((c.distance_to_box(x, y), c.layout_id) for c in conveyors
|
|
if c.kind in kinds), default=(None, ""))
|
|
if best[0] is not None and best[0] <= CONVEYOR_MAX_MM:
|
|
votes.setdefault(d.conveyor, Counter())[best[1]] += 1
|
|
names = sorted(set(totals) | set(drives))
|
|
name_of = {p.layout_id: p.name for p in doc.parts}
|
|
|
|
candidates = []
|
|
for name in names:
|
|
counter = votes.get(name, Counter())
|
|
layout_id, n = counter.most_common(1)[0] if counter else ("", 0)
|
|
candidates.append((n, name, layout_id))
|
|
taken: dict[str, str] = {}
|
|
result = []
|
|
for n, name, layout_id in sorted(candidates, key=lambda c: (-c[0], c[1])):
|
|
total = totals.get(name, 0)
|
|
dr = drives.get(name)
|
|
cb = ConveyorBinding(eplan_name=name, votes=n, total=total, rule="mehrheit",
|
|
motor=dr.motor if dr else "",
|
|
protection=dr.protection if dr else "",
|
|
encoder=dr.encoder if dr else "")
|
|
share = n / total if total else 0.0
|
|
if not layout_id:
|
|
cb.status = "offen"
|
|
cb.note = ("kein Feldgeraet nennt diesen Foerderer" if not total else
|
|
f"keine Stimme innerhalb {CONVEYOR_MAX_MM:.0f} mm eines Layout-Foerderers")
|
|
elif n < CONVEYOR_MIN_VOTES or share < CONVEYOR_MIN_SHARE:
|
|
cb.status = "offen"
|
|
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
|
cb.note = (f"unsicher: {n} von {total} Stimmen fuer {cb.layout_name or layout_id} "
|
|
f"(noetig {CONVEYOR_MIN_VOTES} und {CONVEYOR_MIN_SHARE:.0%})")
|
|
elif layout_id in taken:
|
|
cb.status = "widerspruch"
|
|
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
|
cb.note = f"{layout_id} ist schon {taken[layout_id]} zugeordnet (mehr Stimmen)"
|
|
else:
|
|
taken[layout_id] = name
|
|
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
|
others = votes.get(name, Counter())
|
|
rest = [f"{name_of.get(k, k)} {v}" for k, v in others.most_common() if k != layout_id]
|
|
if rest:
|
|
cb.note = "Gegenstimmen: " + ", ".join(rest)
|
|
result.append(cb)
|
|
return sorted(result, key=lambda c: c.eplan_name)
|
|
|
|
|
|
# Art des Foerderers im Klartext der E-Planung -> Arten im Layout
|
|
_LAYOUT_KINDS_FOR = {"Kreisel": ("Kreisel",), "Strecke": ("Strecke", "Gefaellestrecke")}
|
|
|
|
|
|
def _conveyor_kinds(hw: HardwareDocument) -> dict[str, str]:
|
|
"""Art je Foerderer der E-Planung: Mehrheit aller Klartexte, die ihn nennen.
|
|
|
|
"PRUEFUNG BT. POS.1 (KR-K0102 EINFAHRT PUFFER)" und "ANTRIEB
|
|
KREISEL-K0102" sagen Kreisel; ein Geraet ohne Artwort erbt die Art.
|
|
"""
|
|
counts: dict[str, Counter] = {}
|
|
for d in hw.devices:
|
|
if d.conveyor and d.conveyor_kind:
|
|
counts.setdefault(d.conveyor, Counter())[d.conveyor_kind] += 1
|
|
for dr in hw.drives:
|
|
if dr.conveyor and dr.conveyor_kind:
|
|
counts.setdefault(dr.conveyor, Counter())[dr.conveyor_kind] += 1
|
|
return {name: c.most_common(1)[0][0] for name, c in counts.items()}
|
|
|
|
|
|
def bind_drives(doc: TroDocument, conveyors: list[ConveyorBinding],
|
|
hw: HardwareDocument) -> list[Binding]:
|
|
"""Antriebs-Slots (Vario): Motor und Motorschutz des zugeordneten Foerderers."""
|
|
devices = hw.by_bmk()
|
|
by_layout = {c.layout_id: c for c in conveyors if c.status == "zugeordnet"}
|
|
bindings = []
|
|
for tro in doc.tros:
|
|
for s in tro.slots:
|
|
if not s.layout_id:
|
|
continue
|
|
conv = by_layout.get(s.layout_id.split("-", 1)[0])
|
|
if conv is None:
|
|
continue
|
|
roles = [r.rstrip("?") for r in s.roles]
|
|
for role, attr in _DRIVE_ROLES.items():
|
|
bmk = getattr(conv, attr)
|
|
if role not in roles or not bmk:
|
|
continue
|
|
dev = devices.get(bmk)
|
|
bindings.append(Binding(
|
|
tro=tro.tro_id, slot=s.slot, role=role, layout_id=s.layout_id,
|
|
bmk=bmk, constant=dev.constant if dev else "",
|
|
address=dev.address if dev else "", source="positionsdraw",
|
|
rule="mehrheit", status="zugeordnet" if dev and dev.in_tia else "widerspruch",
|
|
note=f"Antrieb von {conv.eplan_name}"))
|
|
return bindings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Handkorrekturen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class Overrides:
|
|
"""Handkorrekturen aus hw_overrides.ini fuer eine Anlage."""
|
|
|
|
# (tro, slot, role) -> BMK ("" = Zuordnung loeschen, Rolle offen)
|
|
bindings: dict[tuple[str, str, str], str] = field(default_factory=dict)
|
|
# Foerderer der E-Planung -> Layout-ID ("" = Zuordnung loeschen)
|
|
conveyors: dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
def load_overrides(path: Path, layout_source: str) -> Overrides:
|
|
"""Abschnitte ``[<csv>.bindings]`` und ``[<csv>.conveyors]`` lesen.
|
|
|
|
``TRO01.separator1.sensor = BG0532`` setzt ein BMK, ``= -`` loescht die
|
|
Zuordnung (Rolle bleibt offen); ``K0102 = 0010`` setzt einen Foerderer.
|
|
"""
|
|
result = Overrides()
|
|
if not path.is_file():
|
|
return result
|
|
parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"),
|
|
interpolation=None)
|
|
parser.optionxform = str
|
|
with path.open(encoding="utf-8") as fh:
|
|
parser.read_file(fh)
|
|
section = f"{layout_source}.bindings"
|
|
if parser.has_section(section):
|
|
for key, value in parser[section].items():
|
|
parts = key.split(".")
|
|
if len(parts) != 3:
|
|
raise ValueError(f"{path.name} [{section}] '{key}': erwartet "
|
|
f"<TRO-ID>.<Slot>.<Rolle>")
|
|
result.bindings[(parts[0], parts[1], parts[2].rstrip("?"))] = \
|
|
"" if value.strip() == "-" else value.strip()
|
|
section = f"{layout_source}.conveyors"
|
|
if parser.has_section(section):
|
|
for key, value in parser[section].items():
|
|
result.conveyors[key.strip()] = "" if value.strip() == "-" else value.strip()
|
|
return result
|
|
|
|
|
|
def _apply_overrides(doc: TroDocument, bindings: list[Binding],
|
|
conveyors: list[ConveyorBinding], overrides: Overrides,
|
|
hw: HardwareDocument, warnings: list[str]) -> list[Binding]:
|
|
devices = hw.by_bmk()
|
|
slots = {(t.tro_id, s.slot): s for t in doc.tros for s in t.slots}
|
|
for (tro, slot, role), bmk in overrides.bindings.items():
|
|
s = slots.get((tro, slot))
|
|
if s is None:
|
|
warnings.append(f"{OVERRIDES_FILE}: {tro}.{slot} gibt es in der TRO-JSON nicht "
|
|
f"- Korrektur nicht angewendet")
|
|
continue
|
|
bindings = [b for b in bindings
|
|
if not (b.tro == tro and b.slot == slot and b.role == role)]
|
|
if not bmk:
|
|
continue
|
|
# dasselbe BMK nicht zusaetzlich automatisch an anderer Stelle
|
|
bindings = [b for b in bindings if b.bmk != bmk or b.rule == "hand"]
|
|
dev = devices.get(bmk)
|
|
bindings.append(Binding(
|
|
tro=tro, slot=slot, role=role, layout_id=s.layout_id, bmk=bmk,
|
|
constant=dev.constant if dev else "", address=dev.address if dev else "",
|
|
source=OVERRIDES_FILE, rule="hand",
|
|
status="zugeordnet" if dev and dev.in_tia else "widerspruch",
|
|
note="" if dev and dev.in_tia else
|
|
("BMK nicht im TIA-Import" if dev else "BMK unbekannt in der Hardwareplanung")))
|
|
name_of = {p.layout_id: p.name for p in doc.parts}
|
|
for name, layout_id in overrides.conveyors.items():
|
|
cb = next((c for c in conveyors if c.eplan_name == name), None)
|
|
if cb is None:
|
|
cb = ConveyorBinding(eplan_name=name)
|
|
conveyors.append(cb)
|
|
if layout_id and doc.part(layout_id) is None:
|
|
warnings.append(f"{OVERRIDES_FILE}: Foerderer {name} -> {layout_id}: Layout-ID "
|
|
f"unbekannt - Korrektur nicht angewendet")
|
|
continue
|
|
cb.layout_id, cb.layout_name = layout_id, name_of.get(layout_id, "")
|
|
cb.rule = "hand"
|
|
cb.status = "zugeordnet" if layout_id else "offen"
|
|
cb.note = "von Hand"
|
|
return bindings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zusammenbau
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _open_and_conflicts(doc: TroDocument, bindings: list[Binding]) -> list[Binding]:
|
|
"""Pflichtrollen ohne BMK als ``offen``; doppelt vergebene BMK als Widerspruch."""
|
|
have = {(b.tro, b.slot, b.role) for b in bindings if b.tro}
|
|
for tro in doc.tros:
|
|
for s in tro.slots:
|
|
for role in s.roles:
|
|
if role.endswith("?") or (tro.tro_id, s.slot, role) in have:
|
|
continue
|
|
bindings.append(Binding(tro=tro.tro_id, slot=s.slot, role=role,
|
|
layout_id=s.layout_id, status="offen"))
|
|
# Ein BMK an zwei verschiedenen Bauteilen ist ein Widerspruch. Derselbe
|
|
# Separator in zwei Slots eines Speichers (separator1/line1) ist keiner.
|
|
places: dict[str, set[tuple[str, str]]] = {}
|
|
for b in bindings:
|
|
if b.bmk and b.status != "offen":
|
|
places.setdefault(b.bmk, set()).add((b.layout_id, b.role))
|
|
for b in bindings:
|
|
if b.bmk and len(places.get(b.bmk, ())) > 1 and b.status == "zugeordnet":
|
|
b.status = "widerspruch"
|
|
b.note = "BMK mehrfach vergeben"
|
|
order = {t.tro_id: i for i, t in enumerate(doc.tros)}
|
|
return sorted(bindings, key=lambda b: (b.tro == "", order.get(b.tro, 0), b.slot,
|
|
b.role, b.layout_id))
|
|
|
|
|
|
def build_anlage(doc: TroDocument, hw: HardwareDocument, rules: list[Rule],
|
|
anchors: list[str], cfg_dir: Path, layout_file: str, hw_file: str,
|
|
overrides: Overrides | None = None, re_register: bool = False,
|
|
transform: Transform | None = None) -> AnlageDocument:
|
|
"""Kompletter Lauf ohne Ausgabe: TRO-JSON + Hardware-JSON -> AnlageDocument.
|
|
|
|
``transform`` vorgeben (Tests) ueberspringt die Ermittlung des Lagebezugs.
|
|
"""
|
|
warnings: list[str] = []
|
|
if not doc.parts:
|
|
raise ValueError(f"{layout_file} enthaelt keine 'parts' (Lage der Bauteile) - "
|
|
f"mit tro_flow --json neu erzeugen")
|
|
points = points_from_parts(doc.parts)
|
|
devices: list[EDevice] = list(hw.devices)
|
|
|
|
assignments: list[Assignment] = []
|
|
conveyors: list[ConveyorBinding] = []
|
|
if hw.field_devices():
|
|
if transform is None:
|
|
transform = resolve_transform(points, devices, anchors, cfg_dir, doc.source,
|
|
hw.positions_file, re_register, warnings,
|
|
hw.positions_sha256)
|
|
share = transform.matched / transform.candidates if transform.candidates else 1.0
|
|
if share < REGISTRATION_MIN_SHARE:
|
|
warnings.append(
|
|
f"Lagebezug passt nur fuer {transform.matched} von {transform.candidates} "
|
|
f"Separatoren ({share:.0%}) - Layout und E-Zeichnung sind vermutlich "
|
|
f"verschiedene Planungsstaende; raeumliche Zuordnungen pruefen")
|
|
assignments = assign(points, devices, rules, transform)
|
|
conveyors = assign_conveyors(doc, hw, transform)
|
|
else:
|
|
warnings.append("Hardwareplanung ohne Lage (keine Positionsdatei) - keine "
|
|
"raeumliche Zuordnung, alle Rollen offen")
|
|
|
|
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 = _open_and_conflicts(doc, bindings)
|
|
|
|
used = {b.bmk for b in bindings if b.bmk}
|
|
unassigned = sorted(d.bmk for d in hw.field_devices() if d.bmk not in used)
|
|
registration = None
|
|
if transform is not None:
|
|
registration = Registration(
|
|
dx=round(transform.dx, 1), dy=round(transform.dy, 1),
|
|
rotation=round(transform.rotation, 4), source=transform.source,
|
|
matched=transform.matched, candidates=transform.candidates,
|
|
residual_max=round(transform.residual_max, 1),
|
|
description=transform.describe())
|
|
return AnlageDocument(
|
|
layout_file=layout_file, hw_file=hw_file, project=hw.project,
|
|
registration=registration, layout=doc, bindings=bindings,
|
|
conveyors=sorted(conveyors, key=lambda c: c.eplan_name),
|
|
unassigned=unassigned, warnings=warnings + hw.checks,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bericht
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def render_report(anlage: AnlageDocument, hw: HardwareDocument) -> str:
|
|
out: list[str] = []
|
|
add = out.append
|
|
devices = hw.by_bmk()
|
|
add(f"# Anlage: {anlage.layout.source} + {anlage.project}")
|
|
add("")
|
|
add(f"- Layout: `{anlage.layout_file}`")
|
|
add(f"- Hardwareplanung: `{anlage.hw_file}` (TIA `{hw.tia_file or '-'}`, "
|
|
f"Positionen `{hw.positions_file or '-'}`)")
|
|
add(f"- Erzeugt: {anlage.created}")
|
|
if anlage.registration:
|
|
add(f"- Lagebezug: {anlage.registration.description}")
|
|
add("")
|
|
|
|
add("## Slot-Rollen")
|
|
add("")
|
|
add("| Slot-Art | Rolle | zugeordnet | offen | Widerspruch |")
|
|
add("|---|---|---|---|---|")
|
|
stats: dict[tuple[str, str], Counter] = {}
|
|
for b in anlage.bindings:
|
|
if not b.tro:
|
|
continue
|
|
kind = b.slot.rstrip("0123456789")
|
|
stats.setdefault((kind, b.role), Counter())[b.status] += 1
|
|
for (kind, role), c in sorted(stats.items()):
|
|
add(f"| {kind} | {role} | {c['zugeordnet']} | {c['offen']} | {c['widerspruch']} |")
|
|
add("")
|
|
|
|
add("## Foerderer (E-Planung -> Layout)")
|
|
add("")
|
|
if anlage.conveyors:
|
|
add("| E-Planung | Layout | Stimmen | Motor | Motorschutz | Geber | Status | Hinweis |")
|
|
add("|---|---|---|---|---|---|---|---|")
|
|
for c in anlage.conveyors:
|
|
layout = f"{c.layout_name} ({c.layout_id})" if c.layout_id else "-"
|
|
add(f"| {c.eplan_name} | {layout} | {c.votes}/{c.total} | {c.motor or '-'} "
|
|
f"| {c.protection or '-'} | {c.encoder or '-'} | {c.status} | {c.note} |")
|
|
else:
|
|
add("- keine")
|
|
add("")
|
|
|
|
add("## Zuordnungen je TRO")
|
|
add("")
|
|
add("Abstand nach dem Lagebezug; \"2.\" = naechstbester Kandidat (knapp = pruefen).")
|
|
add("")
|
|
add("| TRO | Slot | Rolle | Layout | BMK | Konstante | Adresse | VERW | Abstand mm "
|
|
"| 2. | Regel | Status |")
|
|
add("|---|---|---|---|---|---|---|---|---|---|---|---|")
|
|
for b in anlage.bindings:
|
|
if not b.tro or b.status == "offen":
|
|
continue
|
|
d = devices.get(b.bmk)
|
|
dist = "" if b.distance is None else f"{b.distance:.0f}"
|
|
second = "" if b.runner_up is None else f"{b.runner_up:.0f}"
|
|
status = b.status + (f" ({b.note})" if b.note and b.status != "zugeordnet" else "")
|
|
add(f"| {b.tro} | {b.slot} | {b.role} | {b.layout_id} | {b.bmk} | {b.constant} "
|
|
f"| {b.address} | {d.verw if d else ''} | {dist} | {second} | {b.rule} | {status} |")
|
|
add("")
|
|
|
|
add("## Offene Pflichtrollen")
|
|
add("")
|
|
open_by: dict[str, list[str]] = {}
|
|
for b in anlage.bindings:
|
|
if b.status == "offen":
|
|
open_by.setdefault(b.tro, []).append(f"{b.slot}.{b.role}")
|
|
if open_by:
|
|
for tro, roles in open_by.items():
|
|
add(f"- **{tro}**: {', '.join(roles)}")
|
|
else:
|
|
add("- keine")
|
|
add("")
|
|
|
|
loose = [b for b in anlage.bindings if not b.tro]
|
|
add("## Zugeordnet, aber ohne TRO-Slot")
|
|
add("")
|
|
if loose:
|
|
add("Diese Bauteile haengen (noch) an keinem Slot der TRO-JSON, z. B. "
|
|
"Ausschleuselemente: welcher TRO-Ausgang zu welchem Element gehoert, "
|
|
"weiss tro_flow noch nicht.")
|
|
add("")
|
|
add("| Regel | Layout | BMK | Abstand mm | Status |")
|
|
add("|---|---|---|---|---|")
|
|
for b in loose:
|
|
dist = "" if b.distance is None else f"{b.distance:.0f}"
|
|
add(f"| {b.role} | {b.layout_id} | {b.bmk} | {dist} | {b.status} |")
|
|
else:
|
|
add("- keine")
|
|
add("")
|
|
|
|
add("## Feldgeraete ohne Zuordnung")
|
|
add("")
|
|
if anlage.unassigned:
|
|
add("| BMK | VERW | Bezeichnung | Station |")
|
|
add("|---|---|---|---|")
|
|
for bmk in anlage.unassigned:
|
|
d = devices[bmk]
|
|
add(f"| {bmk} | {d.verw} | {d.bezeichnung} | {d.controller}-{d.station} |")
|
|
else:
|
|
add("- keine")
|
|
add("")
|
|
if anlage.warnings:
|
|
add("## Hinweise")
|
|
add("")
|
|
for w in anlage.warnings:
|
|
add(f"- {w}")
|
|
add("")
|
|
return "\n".join(out)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _in_results(name: str, results: Path) -> Path:
|
|
path = Path(name)
|
|
if path.is_absolute() or path.parent != Path(".") or path.is_file():
|
|
return path
|
|
return results / name
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
from material_flow import env_dir
|
|
|
|
args = parse_args(argv)
|
|
results = env_dir("SKEL_RESULTS", "results")
|
|
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
|
tro_path = _in_results(args.tro, results)
|
|
hw_path = _in_results(args.hw, results)
|
|
rules_path = Path(args.rules) if args.rules else cfg_dir / RULES_FILE
|
|
overrides_path = Path(args.overrides) if args.overrides else cfg_dir / OVERRIDES_FILE
|
|
for label, path in (("TRO-JSON", tro_path), ("Hardware-JSON", hw_path),
|
|
("Regeln", rules_path)):
|
|
if not path.is_file():
|
|
print(f"FEHLER: {label} nicht gefunden: {path}", file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
doc = TroDocument.from_json(tro_path.read_text(encoding="utf-8"))
|
|
except ValidationError as exc:
|
|
print(f"FEHLER: {tro_path} ist keine gueltige TRO-JSON ({exc.error_count()} "
|
|
f"Abweichungen) - mit tro_flow --json neu erzeugen.", file=sys.stderr)
|
|
return 1
|
|
try:
|
|
hw = HardwareDocument.from_json(hw_path.read_text(encoding="utf-8"))
|
|
except ValidationError as exc:
|
|
print(f"FEHLER: {hw_path} passt nicht zum Schema '{HW_SCHEMA}' "
|
|
f"({exc.error_count()} Abweichungen) - mit hw_import neu erzeugen.",
|
|
file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
rules, anchors = load_rules(rules_path)
|
|
overrides = load_overrides(overrides_path, doc.source)
|
|
anlage = build_anlage(doc, hw, rules, anchors, cfg_dir, tro_path.name, hw_path.name,
|
|
overrides, args.re_register)
|
|
except RegistrationError as exc:
|
|
print(f"FEHLER: Lagebezug E-Zeichnung -> Layout nicht nachweisbar: {exc}",
|
|
file=sys.stderr)
|
|
print(f" Passen Layout und E-Zeichnung zusammen? Anker-VERW in "
|
|
f"{rules_path.name} [Registrierung] pruefen.", file=sys.stderr)
|
|
return 1
|
|
except (OSError, ValueError) as exc:
|
|
print(f"FEHLER: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
stem = Path(doc.source).stem or tro_path.stem.removesuffix("_tro")
|
|
results.mkdir(parents=True, exist_ok=True)
|
|
json_file = results / f"{stem}_anlage.json"
|
|
md_file = results / f"{stem}_anlage.md"
|
|
json_file.write_text(anlage.to_json(), encoding="utf-8")
|
|
md_file.write_text(render_report(anlage, hw), encoding="utf-8")
|
|
|
|
status = Counter(b.status for b in anlage.bindings)
|
|
print("")
|
|
print("================================================================")
|
|
print("LAYOUT + HARDWAREPLANUNG -> ANLAGE")
|
|
print("================================================================")
|
|
print(f"TRO-JSON = {tro_path}")
|
|
print(f"Hardware-JSON = {hw_path}")
|
|
print(f"Lagebezug = {anlage.registration.description if anlage.registration else '-'}")
|
|
print(f"Slot-Rollen = {status['zugeordnet']} zugeordnet, {status['offen']} offen, "
|
|
f"{status['widerspruch']} Widerspruch")
|
|
print(f"Ohne Slot = {status['ohne_slot']} Zuordnungen an Bauteilen ohne TRO-Slot")
|
|
conv_ok = [c for c in anlage.conveyors if c.status == "zugeordnet"]
|
|
print(f"Foerderer = {len(conv_ok)} von {len(anlage.conveyors)} zugeordnet"
|
|
+ (": " + ", ".join(f"{c.eplan_name}={c.layout_name or c.layout_id}"
|
|
for c in conv_ok) if conv_ok else ""))
|
|
print(f"Ohne Zuordnung = {len(anlage.unassigned)} Feldgeraete")
|
|
for w in anlage.warnings:
|
|
print(f" ! {w}")
|
|
print(f"JSON = {json_file}")
|
|
print(f"Bericht = {md_file}")
|
|
print("================================================================")
|
|
print("")
|
|
return 0
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
prog="merge_hw_layout",
|
|
description="Fuehrt die TRO-JSON (tro_flow --json) und die Hardware-JSON "
|
|
"(hw_import) zusammen: ordnet Sensoren, Ventile und Antriebe den "
|
|
"Slots der TROs zu und schreibt <csv>_anlage.json plus einen "
|
|
"Bericht <csv>_anlage.md.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="Beispiele:\n"
|
|
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json\n"
|
|
" merge_hw_layout.bat --tro mubea_tro.json --hw Mubea_hw.json --re-register\n"
|
|
"\n"
|
|
"Exit-Codes:\n"
|
|
" 0 ok (auch wenn nicht alles zugeordnet wurde - siehe Bericht)\n"
|
|
" 1 Eingabe-, Aufruf- oder Lagebezugsfehler\n",
|
|
)
|
|
parser.add_argument("--tro", required=True, metavar="NAME",
|
|
help="TRO-JSON <csv>_tro.json in %%SKEL_RESULTS%% (oder ein Pfad)")
|
|
parser.add_argument("--hw", required=True, metavar="NAME",
|
|
help="Hardware-JSON <projekt>_hw.json in %%SKEL_RESULTS%% "
|
|
"(oder ein Pfad)")
|
|
parser.add_argument("--rules", metavar="DATEI",
|
|
help=f"Regeldatei. Standard: %%SKEL_CFG%%\\{RULES_FILE}")
|
|
parser.add_argument("--overrides", metavar="DATEI",
|
|
help=f"Handkorrekturen. Standard: %%SKEL_CFG%%\\{OVERRIDES_FILE} "
|
|
f"(fehlt sie, keine)")
|
|
parser.add_argument("--re-register", action="store_true",
|
|
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
|
"einer abgelegt ist")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|