svg Files des .dot Files erzeugt und mit eingecheckt
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
@echo off
|
||||
REM Erzeugt den TRO-Topologie-Graphen (DOT) aus den FB_Main.scl-Dateien
|
||||
REM und rendert ihn anschliessend mit Graphviz als SVG.
|
||||
REM
|
||||
REM Voraussetzung: Graphviz (dot.exe) muss installiert sein, z.B. per
|
||||
REM winget install --id Graphviz.Graphviz -e
|
||||
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "REPO_DIR=%SCRIPT_DIR%.."
|
||||
set "DOT_FILE=%REPO_DIR%\doc\TRO_Graph_UH01-UH05.dot"
|
||||
set "SVG_FILE=%REPO_DIR%\doc\TRO_Graph_UH01-UH05.svg"
|
||||
|
||||
echo Erzeuge %DOT_FILE% ...
|
||||
python "%SCRIPT_DIR%generate_tro_graph.py" "%DOT_FILE%"
|
||||
if errorlevel 1 (
|
||||
echo Fehler bei der Erzeugung der DOT-Datei.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
where dot >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo dot.exe wurde nicht gefunden. Bitte Graphviz installieren:
|
||||
echo winget install --id Graphviz.Graphviz -e
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Rendere %SVG_FILE% ...
|
||||
dot -Tsvg "%DOT_FILE%" -o "%SVG_FILE%"
|
||||
if errorlevel 1 (
|
||||
echo Fehler beim Rendern der SVG-Datei.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Fertig: %SVG_FILE%
|
||||
endlocal
|
||||
@@ -0,0 +1,325 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Extrahiert den TRO-Topologie-Graphen aller 5 UH-Steuerungen aus FB_Main.scl
|
||||
und erzeugt eine Graphviz-DOT-Datei.
|
||||
|
||||
Prinzip: Jeder TRO-Baustein bekommt JamArea-Slots als Parameter
|
||||
(arInOutJamEntr*, arInOutJamExit*, arInOutJamBypass, arInOutJamFinger).
|
||||
Zwei TROs sind verbunden, wenn der Exit-Slot des einen dieselbe JamArea
|
||||
referenziert wie der Entry-Slot des anderen.
|
||||
|
||||
Aufruf:
|
||||
python bin/generate_tro_graph.py [ausgabe.dot]
|
||||
|
||||
Ohne Argument wird nach doc/TRO_Graph_UH01-UH05.dot geschrieben.
|
||||
Zum Rendern als SVG anschliessend:
|
||||
dot -Tsvg doc/TRO_Graph_UH01-UH05.dot -o doc/TRO_Graph_UH01-UH05.svg
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
UHS = ["UH01", "UH02", "UH03", "UH04", "UH05"]
|
||||
|
||||
decl_re = re.compile(r'^\s*(\w+)\s*(?:\{[^}]*\})?\s*:\s*"([\w.]+)"\s*;')
|
||||
jamparam_re = re.compile(
|
||||
r'arInOut(Jam\w+|StorageLines|CarrierStopper|CarrierExit)'
|
||||
r'\s*:=\s*"([^"]+)"\.(\w+)')
|
||||
call_start_re = re.compile(r'#(\w+)\s*\(')
|
||||
region_re = re.compile(r'^\s*REGION\b\s*(.*)$')
|
||||
endregion_re = re.compile(r'^\s*END_REGION')
|
||||
|
||||
|
||||
def parse_uh(uh):
|
||||
path = REPO / f"=A01+{uh}-KF00" / "Programmbausteine" / "100_Main" / "FB_Main.scl"
|
||||
text = path.read_text(encoding="utf-8-sig")
|
||||
text = re.sub(r'\(\*.*?\*\)', '', text, flags=re.S) # Blockkommentare
|
||||
lines = [re.sub(r'//.*$', '', ln) for ln in text.splitlines()]
|
||||
|
||||
# 1) Instanz-Deklarationen -> FB-Typ
|
||||
types = {}
|
||||
for ln in lines:
|
||||
m = decl_re.match(ln)
|
||||
if m:
|
||||
types[m.group(1)] = m.group(2)
|
||||
|
||||
# 2) Aufrufe finden inkl. REGION-Kontext
|
||||
nodes = {} # inst -> dict(type, region, jams=[(role, jamref)])
|
||||
region_stack = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
ln = lines[i]
|
||||
if endregion_re.match(ln):
|
||||
if region_stack:
|
||||
region_stack.pop()
|
||||
i += 1
|
||||
continue
|
||||
m = region_re.match(ln)
|
||||
if m and "END_REGION" not in ln.split("REGION")[0]:
|
||||
region_stack.append(m.group(1).strip())
|
||||
i += 1
|
||||
continue
|
||||
m = call_start_re.search(ln)
|
||||
if m and types.get(m.group(1)):
|
||||
inst = m.group(1)
|
||||
# Parameterblock bis zur schliessenden Klammer einsammeln
|
||||
buf = ln[m.end() - 1:]
|
||||
depth = 0
|
||||
j = i
|
||||
done = False
|
||||
while j < len(lines) and not done:
|
||||
seg = lines[j] if j > i else buf
|
||||
for ch in seg:
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
done = True
|
||||
break
|
||||
j += 1
|
||||
block = "\n".join([buf] + lines[i + 1:j])
|
||||
jams = []
|
||||
for jm in jamparam_re.finditer(block):
|
||||
role, db, member = jm.group(1), jm.group(2), jm.group(3)
|
||||
jams.append((role, f"{db}.{member}"))
|
||||
if jams:
|
||||
# aussagekraeftigste REGION waehlen (generische wie "CALL",
|
||||
# "TRO 101 CALL", "Enable und RequestRun" ueberspringen)
|
||||
region = ""
|
||||
for r in reversed(region_stack):
|
||||
if not re.match(r'^(.*\bCALL\b.*|Enable und RequestRun)$',
|
||||
r, re.I):
|
||||
region = r
|
||||
break
|
||||
node = nodes.setdefault(inst, {
|
||||
"type": types[inst],
|
||||
"region": region,
|
||||
"jams": []})
|
||||
for j2 in jams:
|
||||
if j2 not in node["jams"]:
|
||||
node["jams"].append(j2)
|
||||
i = j
|
||||
continue
|
||||
i += 1
|
||||
return nodes
|
||||
|
||||
|
||||
def role_dir(role):
|
||||
if role.startswith("JamEntr") or role in ("StorageLines",
|
||||
"CarrierStopper"):
|
||||
return "in"
|
||||
if (role.startswith(("JamExit", "JamBypass", "JamFinger"))
|
||||
or role == "CarrierExit"):
|
||||
return "out"
|
||||
return "?"
|
||||
|
||||
|
||||
def short_jam(jamref):
|
||||
# DB_JamArea0101.stJam0101_1 -> JA0101_1
|
||||
m = re.match(r"DB_JamArea(\w+)\.stJam\w+_(\w+)", jamref)
|
||||
if m:
|
||||
return f"JA{m.group(1)}_{m.group(2)}"
|
||||
return jamref.replace("DB_", "").replace(".arLine", "")
|
||||
|
||||
|
||||
def main():
|
||||
all_nodes = {}
|
||||
report = []
|
||||
for uh in UHS:
|
||||
nodes = parse_uh(uh)
|
||||
all_nodes[uh] = nodes
|
||||
report.append(f"{uh}: {len(nodes)} TRO-Knoten")
|
||||
|
||||
# Kanten je UH ueber gemeinsame JamAreas
|
||||
edges = defaultdict(list) # uh -> [(from, to, jam, tag)]
|
||||
externals = defaultdict(list) # uh -> [(node, dir, jam, role)]
|
||||
for uh, nodes in all_nodes.items():
|
||||
jam_users = defaultdict(lambda: {"in": [], "out": []})
|
||||
for inst, nd in nodes.items():
|
||||
for role, jam in nd["jams"]:
|
||||
d = role_dir(role)
|
||||
if d in ("in", "out"):
|
||||
jam_users[jam][d].append((inst, role))
|
||||
for jam, users in sorted(jam_users.items()):
|
||||
for (src, srole) in users["out"]:
|
||||
tag = {"JamExit3": "Dir2", "JamExit4": "Dir3",
|
||||
"JamBypass": "Bypass", "JamFinger": "Finger"
|
||||
}.get(srole, "")
|
||||
if users["in"]:
|
||||
for (dst, drole) in users["in"]:
|
||||
if dst != src:
|
||||
edges[uh].append((src, dst, jam, tag))
|
||||
else:
|
||||
externals[uh].append((src, "out", jam, srole))
|
||||
for (dst, drole) in users["in"]:
|
||||
if not users["out"]:
|
||||
externals[uh].append((dst, "in", jam, drole))
|
||||
|
||||
for uh in UHS:
|
||||
report.append(f"{uh}: {len(edges[uh])} Kanten, "
|
||||
f"{len(externals[uh])} offene Enden")
|
||||
print("\n".join(report))
|
||||
|
||||
# Knoten ohne jede Kante?
|
||||
for uh, nodes in all_nodes.items():
|
||||
connected = set()
|
||||
for (s, d, _, _) in edges[uh]:
|
||||
connected.add(s)
|
||||
connected.add(d)
|
||||
for (n, _, _, _) in externals[uh]:
|
||||
connected.add(n)
|
||||
iso = set(nodes) - connected
|
||||
if iso:
|
||||
print(f"{uh} isoliert: {sorted(iso)}")
|
||||
|
||||
write_dot(all_nodes, edges, externals)
|
||||
|
||||
|
||||
COLORS = {
|
||||
"FB_ILS_MTRO_1Sep": ("#d4e8ff", "box"),
|
||||
"FB_ILS_MTRO_1Sep_SSCC": ("#ffd4d4", "box"),
|
||||
"FB_ILS_MTRO_1Sep1Swi": ("#ffe4a0", "diamond"),
|
||||
"FB_ILS_MTRO_1Sep2Swi": ("#ffc060", "diamond"),
|
||||
"FB_ILS_MTRO_2Sep1Swi": ("#ffe4a0", "diamond"),
|
||||
"FB_ILS_MTRO_Vario": ("#e8d4f0", "parallelogram"),
|
||||
"FB_ILS_MTRO_Vario_workStation": ("#c0f0f0", "parallelogram"),
|
||||
"FB_EmptyCarrBuffer": ("#d4f0d4", "box3d"),
|
||||
}
|
||||
|
||||
|
||||
def node_style(fbtype):
|
||||
for k in sorted(COLORS, key=len, reverse=True):
|
||||
if fbtype.startswith(k):
|
||||
return COLORS[k]
|
||||
return ("#f0f0f0", "box")
|
||||
|
||||
|
||||
def esc(s):
|
||||
return s.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
HEADER = '''\
|
||||
// ============================================================================
|
||||
// TRO-Topologie-Graph der Gesamtanlage HundM_Fortna (UH01 - UH05)
|
||||
// ============================================================================
|
||||
//
|
||||
// Format: Graphviz DOT (https://graphviz.org)
|
||||
// Rendern z.B. mit: dot -Tsvg TRO_Graph_UH01-UH05.dot -o graph.svg
|
||||
// oder online: https://dreampuf.github.io/GraphvizOnline
|
||||
//
|
||||
// Quelle: Automatisch extrahiert aus den FB_Main.scl der 5 Steuerungen
|
||||
// (=A01+UH01-KF00 ... =A01+UH05-KF00, Ordner 100_Main).
|
||||
// Erzeugt von bin/generate_tro_graph.py.
|
||||
//
|
||||
// Methode: Jeder TRO-Baustein (FB_ILS_MTRO_*, FB_EmptyCarrBuffer,
|
||||
// FB_LoadingBoom_INBOUND) referenziert JamAreas als Parameter:
|
||||
// arInOutJamEntr* / arInOutStorageLines / arInOutCarrierStopper = Eingang
|
||||
// arInOutJamExit* / arInOutJamBypass / arInOutJamFinger /
|
||||
// arInOutCarrierExit = Ausgang
|
||||
// Zwei TROs sind verbunden, wenn der Ausgangs-Slot des einen
|
||||
// dieselbe JamArea referenziert wie der Eingangs-Slot des anderen.
|
||||
// Auskommentierter Code wurde ignoriert.
|
||||
//
|
||||
// Knoten: Ein Knoten je TRO-Instanz, Label: Name, [FB-Typ], REGION-Kommentar
|
||||
// aus FB_Main. Farbe/Form nach FB-Typ:
|
||||
// hellblau/Box = 1Sep (Standard-Separator)
|
||||
// rot/Box = 1Sep_SSCC (SSCC-Scanner + WCS-Telegramm)
|
||||
// gelb/Raute = 1Sep1Swi, 1Sep2Swi, 2Sep1Swi (Weichen)
|
||||
// violett/Parallelogr.= Vario (Kettenfoerderer)
|
||||
// tuerkis/Parallelogr.= Vario_workStation (manueller Arbeitsplatz)
|
||||
// gruen/3D-Box = EmptyCarrBuffer (Leertablar-Puffer)
|
||||
// grau = Sonstige (z.B. LoadingBoom, PinStore)
|
||||
//
|
||||
// Kanten: Label = JamArea-Slot (JAxxxx_y = "DB_JamAreaxxxx".stJamxxxx_y).
|
||||
// Zusatz "Dir2"/"Dir3" = Weichen-Ausgang (arInOutJamExit3/4),
|
||||
// Richtungsentscheidung kommt aus FC_Direction.
|
||||
// Gestrichelt "Bypass" = Bypass-Spur (PinStore),
|
||||
// gestrichelt "Finger" = Finger-Uebergabe eines Vario-Foerderers.
|
||||
//
|
||||
// EXTERN: Graue Kanten zu EXTERN-Knoten = JamAreas, die nur auf einer
|
||||
// Seite belegt sind (Anlagengrenze, Uebergabe an WES/WCS, AMR
|
||||
// oder andere SPS). CPU-CPU-Uebergaben zwischen den Steuerungen
|
||||
// sind an den Labels "IO_Device_UHxx_TO_UHyy..." erkennbar
|
||||
// (PROFINET-Direktkopplung, kein gemeinsamer JamArea-DB).
|
||||
//
|
||||
// Hinweis: Kanten existieren nur innerhalb einer Steuerung, da JamArea-DBs
|
||||
// nicht SPS-uebergreifend geteilt werden. Der physische Fluss
|
||||
// zwischen den UHs laeuft ueber die EXTERN-Uebergaben.
|
||||
//
|
||||
'''
|
||||
|
||||
|
||||
def write_dot(all_nodes, edges, externals):
|
||||
body = []
|
||||
B = body.append
|
||||
B(HEADER.rstrip())
|
||||
total_n = sum(len(v) for v in all_nodes.values())
|
||||
total_e = sum(len(set(v)) for v in edges.values())
|
||||
B(f"// Umfang: {total_n} TRO-Knoten, {total_e} Verbindungen "
|
||||
f"(Stand der Extraktion siehe Git-Historie)")
|
||||
for uh in UHS:
|
||||
B(f"// {uh}: {len(all_nodes[uh])} TROs, "
|
||||
f"{len(set(edges[uh]))} Verbindungen")
|
||||
B("")
|
||||
B("digraph TRO_Topologie {")
|
||||
B(' rankdir = "LR";')
|
||||
B(' fontname = "Arial";')
|
||||
B(' node [fontname="Arial", fontsize=10];')
|
||||
B(' edge [fontname="Arial", fontsize=8];')
|
||||
B(' label = "TRO-Topologie HundM_Fortna (UH01-UH05)";')
|
||||
B(' labelloc = "t";')
|
||||
B("")
|
||||
for uh in UHS:
|
||||
nodes = all_nodes[uh]
|
||||
B(f' subgraph "cluster_{uh}" {{')
|
||||
B(f' label = "{uh} (=A01+{uh}-KF00)";')
|
||||
B(' style = "rounded";')
|
||||
B(' color = "#888888";')
|
||||
for inst in sorted(nodes):
|
||||
nd = nodes[inst]
|
||||
color, shape = node_style(nd["type"])
|
||||
t = nd["type"].replace("FB_ILS_MTRO_", "").replace("FB_", "")
|
||||
reg = esc(nd["region"])
|
||||
B(f' "{uh}.{inst}" [label="{inst}\\n[{t}]\\n{reg}", '
|
||||
f'shape={shape}, style=filled, fillcolor="{color}"];')
|
||||
# Externe Enden als ein Sammel-Knoten je Richtung
|
||||
ext = externals[uh]
|
||||
if any(d == "in" for (_, d, _, _) in ext):
|
||||
B(f' "{uh}.EXTERN_IN" [label="EXTERN\\n(Zulauf)", '
|
||||
f'shape=ellipse, style="filled,dashed", fillcolor="#f8f8f8"];')
|
||||
if any(d == "out" for (_, d, _, _) in ext):
|
||||
B(f' "{uh}.EXTERN_OUT" [label="EXTERN\\n(Ablauf)", '
|
||||
f'shape=ellipse, style="filled,dashed", fillcolor="#f8f8f8"];')
|
||||
seen = set()
|
||||
for (s, d, jam, tag) in edges[uh]:
|
||||
key = (s, d, jam, tag)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
lbl = short_jam(jam) + (f" {tag}" if tag else "")
|
||||
style = ', style=dashed' if tag in ("Bypass", "Finger") else ""
|
||||
B(f' "{uh}.{s}" -> "{uh}.{d}" [label="{lbl}"{style}];')
|
||||
seen = set()
|
||||
for (n, d, jam, role) in ext:
|
||||
key = (n, d, jam)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if d == "in":
|
||||
B(f' "{uh}.EXTERN_IN" -> "{uh}.{n}" '
|
||||
f'[label="{short_jam(jam)}", color="#999999"];')
|
||||
else:
|
||||
B(f' "{uh}.{n}" -> "{uh}.EXTERN_OUT" '
|
||||
f'[label="{short_jam(jam)}", color="#999999"];')
|
||||
B(" }")
|
||||
B("")
|
||||
B("}")
|
||||
dest = Path(sys.argv[1]) if len(sys.argv) > 1 else REPO / "doc" / "TRO_Graph_UH01-UH05.dot"
|
||||
dest.write_text("\n".join(body) + "\n", encoding="utf-8", newline="\n")
|
||||
print("DOT geschrieben:", dest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user