Files
kabellaengen/lib/getpositions.py
T

655 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import configparser
import ezdxf
import os
import sys
import json
import re
from ezdxf.addons import iterdxf
import re
from pathlib import Path
"""
Dieses Programm:
- liest die dxf Datei und holt sich von den Layern der dxf Datei die Positionen
+ der Motoren, Sensoren und Aktoren
+ der Unterverteiler
+ der Polylinien der Kabelpritschen
- erzeugt daraus eine .json Datei im Work Ordner
"""
def write_results(jsnResults, outdir, filename):
""" write results to a json file
"""
print("writing results to a json file ...")
outfile = os.path.join(outdir, filename)
with open(outfile, 'w', encoding='utf-8') as fh:
fh.write(jsnResults)
print("done")
def merge_two_dicts(x, y):
z = x.copy()
z.update(y)
return z
def get_type_of_name(name):
SpecialKeys = ["MB", # Ventil
"MA", # Motor
"BG", # Stausensor
"BP", # Schalter Druckluft
"QM", # Ventile
"BX" # Scanner
]
KabelKey = ["WD", "WF"]
DropKeys = [
"FC", # Motorschutzschalter
"PF", # Leuchtmelder
"DI", # Feedback vom Gerät
"QA", # Hauptschütz
"SF" # Drucktaster
]
prefix = name[:2]
if prefix in SpecialKeys:
typ = "Sensor"
# Suche nach Kabel
elif prefix in KabelKey:
typ = "Kabel"
# suche nach Items die wir nicht weiter verfolgen
elif prefix in DropKeys:
typ = "Schaltschrankelement"
else:
typ = "unknown"
return typ
def get_type_of_name_cfg(name):
prefix = name[:2]
if config_BMK.has_option("Routing-Include", prefix):
return "Sensor"
elif config_BMK.has_option("Routing-Ignore", prefix):
return "Schaltschrankelement"
else:
return "unknown"
def get_attributes_of_insert(insert):
"""
Diese Funktion schaut nach den aktuell definierten Attributen in den Blöcken
Bei den Sensoren in den alten Layouts gibt zwei immer übereinanderliegende Blöcke mit den Attributen:
- A, B (z.B. MA0062), C, ARTINR (z.B. 790902001), BESCHR (E-Teile für SEW Motor ASE1..), MENGE, POSITION, ...
- IO (z.B. MA0062), ID , VERW (z.B. CV-M0062_0,75), BEZEICHNUNG (Motor MA0062), KENNZEICHNUNG (z.B.=A01+UH01-KF1DQ04), ...
Die Adresse für das Routing kommt aus "KENNZEICHNUNG", während die Sivas Nummer aus "ARTINR" geholt werden muss
Einmal wird B zur ID, beim anderen IO
Erdungssymbole erhalten die ID aus dem Eintrag unter "NAME"
Hier in Zukunft weniger Abfragen: IO und B und Reale_Position wird überflüssig wenn jeder Sensor nur noch ein Block mit allen Attributen!
"""
id = ""
ld = dict()
typ = 'unknown'
for attrib in insert.attribs:
attr_tag = attrib.dxf.tag
attr_text = attrib.dxf.text
if len(insert.attribs) == 0:
continue # Überspringe Blöcke ohne Attribute
#print(f"Attribut Name: {attrib.dxf.tag}, Wert: {attrib.dxf.text}")
ld[attr_tag] = attr_text
if attr_tag == "NAME": # die neueren Bläcke heissen nicht IO, sondern haben einen Namen
typ = get_type_of_name_cfg(attr_text)
id = attr_text
# pos = attrib.dxf.insert #Position aufzeichnen und bei Bedarf später mit REAL_POS überschreiben
# ld["pos"] = (round(pos.x, 1), round(pos.y, 1))
if attr_tag == "IO":
typ = get_type_of_name_cfg(attr_text)
id = attr_text
#print(f"-- coord io {id}--: {attrib.dxf.insert}") # position des Blocks
pos = attrib.dxf.insert #Position aufzeichnen und bei Bedarf später mit REAL_POS überschreiben
ld["pos"] = (round(pos.x, 1), round(pos.y, 1))
if attr_tag == "B":
# Suche nach Sensoren
typ = get_type_of_name_cfg(attr_text)
id = attr_text
pos = attrib.dxf.insert #Position aufzeichnen und bei Bedarf später mit REAL_POS überschreiben
ld["pos"] = (round(pos.x, 1), round(pos.y, 1))
if attr_tag == "REALE_POSITION" and attr_text == "x":
#typ = get_type_of_name(attr_text)
#print(f"-- coord real --: {attrib.dxf.insert}")
pos = attrib.dxf.insert #Position Ecke unten links von "x"-Marker auslesen
# Hoehe und Breite von "x" addieren, um Mittelpunkt zu finden
breite_marker = config.getfloat("GetPos-Geom-Sensor", "Breite")
hoehe_marker = config.getfloat("GetPos-Geom-Sensor", "Hoehe")
midx = pos[0] + breite_marker * 0.5
midy = pos[1] + hoehe_marker * 0.5
ld["pos"] = (round(midx, 1), round(midy, 1))
return (ld, id, typ)
def check_double_ids(ld, id, allSensors):
if id in allSensors and "NAME" in ld:
return True
if id in allSensors and "B" in ld and "B" in allSensors[id]:
return True
if id in allSensors and "IO" in ld and "IO" in allSensors[id]:
return True
return False
def extract_input_positions(insert_iterable) -> tuple:
allSensors = dict()
allCables = dict()
allunknowns = dict()
doubleIds = dict()
allSchaltschrank = dict()
for insert in insert_iterable:
if insert.dxftype() != 'INSERT':
continue
ld, id, typ = get_attributes_of_insert(insert)
if typ == "Sensor":
if id and "pos" in ld and isinstance(ld["pos"], tuple) and len(ld["pos"]) == 2:
if id in allSensors:
is_double = check_double_ids(ld, id, allSensors)
if not is_double:
allSensors[id] = merge_two_dicts(allSensors[id], ld) #Kombiniert alle infos aus dxf und "pos"
else:
# gib aus wo sich beide gleichlautenden Elemente auf der Zeichnung befinden
if id not in doubleIds:
doubleIds[id] = []
doubleIds[id].append(allSensors[id]["pos"]) # erste Position
doubleIds[id].append(ld["pos"]) # zweite Position
else:
allSensors[id] = ld
elif typ == "Kabel":
allCables[id] = ld
elif typ == "Schaltschrankelement":
allSchaltschrank[id] = ld
else:
allunknowns[id] = ld
return allSensors, doubleIds
def get_input_positions(msp) -> tuple:
return extract_input_positions(msp.query('INSERT'))
def get_input_positions_iter(dxf_path) -> tuple:
return extract_input_positions(iterdxf.modelspace(dxf_path))
def create_mappings(positions:dict) -> tuple:
unterverteiler_pfad = ""
dnamen = dict()
# sammle die Sensoren mit ihren zugehörigen Unterverteilern
sensor2unterverteiler = dict()
warnings = dict()
for sensorname,v in positions.items():
if "KENNZEICHNUNG" not in v:
warnings[sensorname] = "keine KENNZEICHNUNG vorhanden"
continue
unterverteiler_pfad = v["KENNZEICHNUNG"]
#print(unterverteiler_pfad)
# Pfad zur Karte splitten. Dieser hat z.B. den Inhalt "=AH01+UH02-KF1FDI7"
pattern = r"^=([A-Z]+\d+)([+\-])([A-Z]+\d+)([+\-])([A-Z]+\d+.)$"
match = re.match(pattern, unterverteiler_pfad)
if match:
anlage = match.group(1)
verteiler = match.group(3)
karte = match.group(5)
# match.group(1) # AH01
# match.group(2) # +
# match.group(3) # UH02
# match.group(4) # -
# match.group(5) # KF1FDI7
else:
warnings[sensorname] = f"Ungültiger Pfad in Kennzeichnung: {unterverteiler_pfad}"
continue
if verteiler not in dnamen:
dnamen[verteiler] = True
sensor2unterverteiler[sensorname] = verteiler
# jetzt zu jedem Unterverteiler die zugehörigen Sensoren merken
uv2sensor = dict()
for sensorname,verteiler in sensor2unterverteiler.items():
if verteiler not in uv2sensor:
uv2sensor[verteiler] = list()
uv2sensor[verteiler].append(sensorname)
return (uv2sensor, warnings)
def get_subdistributor_positions(msp, dist2sensors):
"""hole alle Positionen der Unterverteiler !!UV-Positionen bereits "Mitte-Mitte"!!
"""
ret = dict()
# Alle Texte auf Layer "xy"
all_distributors = dist2sensors.keys()
all_layers = config.items('GetPos-Layer_Distributors')
for (layer,v) in all_layers:
for distname in all_distributors:
selectstr = f'MTEXT[layer=="{layer}"]'
for text in msp.query(selectstr):
#print(f"Text auf Layer 'Busverteiler-Kennzeichnung': {text.dxf.text}")
match = re.search("-"+distname, text.dxf.text)
if match:
ret[distname] = (round(text.dxf.insert[0],1), round(text.dxf.insert[1],1)) #nur x und y Koordinate in Json schreiben
return ret
def get_subdistributor_positions_iter(dxf_path, dist2sensors):
"""Hole alle Positionen der Unterverteiler aus MTEXT-Objekten mithilfe von iterdxf."""
ret = {}
all_distributors = dist2sensors.keys()
all_layers = config.items('GetPos-Layer_Distributors')
for entity in iterdxf.modelspace(dxf_path):
if entity.dxftype() != "MTEXT":
continue
entity_text = entity.dxf.text
entity_layer = entity.dxf.layer
insert_point = entity.dxf.insert
for (layer_name, _) in all_layers:
if entity_layer != layer_name:
continue
for distname in all_distributors:
if f"-{distname}" in entity_text:
ret[distname] = (round(insert_point[0], 1), round(insert_point[1], 1))
return ret
def get_tunnel_positions(msp):
"""hole alle Positionen aller Tunnel Ein und Ausgänge
"""
allTunnels = dict()
tunnel_length = dict()
# Alle Text mit "Tunnel" als Inhalt auf Layer "xy"
all_layers = config.items('GetPos-Layer_Tunnel')
for (layer,v) in all_layers:
selectstr = f'MTEXT[layer=="{layer}"]'
for text in msp.query(selectstr):
txt = text.dxf.text
pattern = r"(TUNNEL\d+)-(\d+)"
match = re.search(pattern, txt)
if match:
pos = (round(text.dxf.insert[0],1), round(text.dxf.insert[1],1)) #nur x und y Koordinate in Json schreiben
tunnelname = match.group(1)
laenge = match.group(2)
tunnel_length[tunnelname] = laenge
if not tunnelname in allTunnels:
allTunnels[tunnelname] = list()
allTunnels[tunnelname].append(pos)
else:
allTunnels[tunnelname].append(pos)
if len(allTunnels.keys()) > 0:
allTunnels['length'] = tunnel_length
return allTunnels
def get_tunnel_positions_iter(dxf_path):
"""Hole alle Positionen aller Tunnel Ein- und Ausgänge mithilfe von iterdxf."""
allTunnels = dict()
tunnel_length = dict()
all_layers = config.items('GetPos-Layer_Tunnel')
for entity in iterdxf.modelspace(dxf_path):
if entity.dxftype() != "MTEXT":
continue
txt = entity.dxf.text
layer = entity.dxf.layer
insert = entity.dxf.insert
for (layer_name, _) in all_layers:
if layer != layer_name:
continue
pattern = r"(TUNNEL\d+)-(\d+)"
match = re.search(pattern, txt)
if match:
tunnelname = match.group(1)
laenge = match.group(2)
pos = (round(insert[0], 1), round(insert[1], 1))
if tunnelname not in allTunnels:
allTunnels[tunnelname] = []
allTunnels[tunnelname].append(pos)
tunnel_length[tunnelname] = laenge
if len(tunnel_length.keys()) > 0:
allTunnels['length'] = tunnel_length
return allTunnels
# helper function
def print_line(e):
print("LINE on layer: %s\n" % e.dxf.layer)
print("points: %s\n" % repr(e.dxf))
def print_polyline(e):
print("POLYLINE on layer: %s\n" % e.dxf.layer)
#print("points: %s\n" % repr(e.dxf))
#print("y point: %s\n" % e.dxf.y)
for x, y, start_width, end_width, bulge in e.get_points(): # Gibt Tuple (x, y, start_width, end_width, bulge)
print(f" Punkt: ({x}, {y}), Startbreite: ({start_width}, Endbreite: {end_width})")
if e.is_closed:
print("Diese Polyline ist geschlossen.")
def get_rack_positions(msp):
"""hole alle Positionen aller Kabelpritschen und nummeriere Racks"""
ret = dict()
rack_counter = 1 #Zaehler für Rack Nummerierung
all_layers = list(config.items('GetPos-Layer_Racks'))
for layer, _ in all_layers:
# Suche nach LWPOLYLINE
lw_query = f'LWPOLYLINE[layer=="{layer}"]'
for entity in msp.query(lw_query):
rack_key = f"Rack_{rack_counter}"
handle_lwpolyline(entity, rack_key, ret)
rack_counter += 1
# Suche nach klassischer POLYLINE
pl_query = f'POLYLINE[layer=="{layer}"]'
for entity in msp.query(pl_query):
rack_key = f"Rack_{rack_counter}"
handle_polyline(entity, rack_key, ret)
rack_counter += 1
return ret
def get_rack_positions_iter(dxf_path):
"""Hole alle Positionen aller Kabelpritschen (Racks) mithilfe von iterdxf."""
ret = dict()
rack_counter = 1 # Zähler für Rack-Nummerierung
all_layers = config.items('GetPos-Layer_Racks')
for entity in iterdxf.modelspace(dxf_path):
layer = entity.dxf.layer
if not any(layer == cfg_layer for cfg_layer, _ in all_layers):
continue
rack_key = f"Rack_{rack_counter}"
if entity.dxftype() == "LWPOLYLINE":
handle_lwpolyline(entity, rack_key, ret)
elif entity.dxftype() == "POLYLINE":
handle_polyline(entity, rack_key, ret)
else:
continue
rack_counter += 1
return ret
def handle_lwpolyline(entity, rack_key, ret):
"""Verarbeitet eine 2D LWPOLYLINE mit globalem Z-Wert (elevation)."""
z = getattr(entity.dxf, "elevation", 0.0) # hole "Erhebung" (gilt für gesamte Polyline!) aus Attributen
ret[rack_key] = []
for point in entity.vertices(): # returns (x, y, start_width, end_width, bulge)
x, y, *_ = point
ret[rack_key].append([round(x, 1), round(y, 1), round(z, 1)])
def handle_polyline(entity, rack_key, ret):
"""Verarbeitet eine klassische POLYLINE inklusive 3D-Polylinien mit individuellen Z-Werten."""
ret[rack_key] = []
for vertex in entity.vertices:
x = vertex.dxf.location.x
y = vertex.dxf.location.y
z = vertex.dxf.location.z
ret[rack_key].append([round(x, 1), round(y, 1), round(z, 1)])
def scan(dxf_source:ezdxf.document.Drawing):
layer_names_inside = dxf_source.layers.entries.keys()
alle_block_defs = set(dxf_source.blocks.block_names())
used_block_names = set(insert.dxf.name for insert in dxf_source.modelspace().query("INSERT"))
ret = dict()
ret['all_layers'] = layer_names_inside
ret['used_blocks'] = used_block_names
ret['all_blocks'] = alle_block_defs
return ret
def to_json(d, pretty: bool = True) -> str:
return json.dumps(d, indent=2 if pretty else None, ensure_ascii=False, default=str) #ensure_ascii false für darstellung von "ue"
def get_dxf_file(filepath):
"""hole das dxf file
"""
try:
print("reading file ..", end='')
doc = ezdxf.readfile(filepath)
print("done")
except IOError:
print(f"Not a DXF file or a generic I/O error.")
sys.exit(1)
except ezdxf.DXFStructureError:
print(f"Invalid or corrupted DXF file.")
sys.exit(2)
return doc
def check_file_in_work(work_dir:Path, filename:Path):
fexists = True
if not filename.exists(): # dann schau im Work Ordner nach
mypath = work_dir.joinpath(filename)
ex = mypath.exists()
if not mypath.exists():
fexists = False
else:
mypath = filename
return (mypath, fexists)
def check_existance(res_mappings, res_dist, res_pos):
ret = dict()
ret["missing_distributors"] = list()
ret["missing_sensors"] = list()
for dname in res_mappings.keys():
if dname not in res_dist:
ret["missing_distributors"].append(dname)
for sname,lofsensors in res_mappings.items():
for s in lofsensors:
if s not in res_pos:
ret['missing_sensors'].append(s)
return ret
def dxf_is_binary(dxf_path):
with open(dxf_path, 'rb') as f:
header = f.read(22)
return b'AutoCAD Binary DXF' in header
def validate_configs():
errors= []
print("\nValidating given configs: Checking for inconsistency.")
# 1. Alle Prefixes aus Routing_include müssen in Cable_Mapping stehen (nur BMK.cfg)
if config_BMK.has_section("Routing-Include") and config_BMK.has_section("Cable-Mapping"):
for prefix in config_BMK.options("Routing-Include"):
if prefix not in config_BMK["Cable-Mapping"]:
errors.append(f"No Cable-Mapping for Prefix '{prefix}' within 'Routing-Include'")
# 2. Jeder Eintrag in Cable-Mapping → Sektionen in kabel.cfg prüfen (Abgleich BMK.cfg und kabel.cfg)
if config_BMK.has_section("Cable-Mapping"):
for mapping_key, value in config_BMK.items("Cable-Mapping"):
sections = [s.strip() for s in value.split(",")]
for section in sections:
if not config_cables.has_section(section):
errors.append(f"Cable-Section '{section}' from Cable-Mapping ({mapping_key}) missing in kabel.cfg")
# 3. Länge in Length-Adjustments muss float >= 0 sein
if config_BMK.has_section("Length-Adjustments"):
for prefix, value in config_BMK.items("Length-Adjustments"):
try:
f = float(value)
if f < 0:
errors.append(f"Negative Value in Length-Adjustments for {prefix}: {value}")
except ValueError:
errors.append(f"Invalid Value in Length-Adjustments for {prefix}: {value}")
if errors:
print("Inconsistencies found:")
for e in errors:
print(f"- {e}")
print("\ncontinuing with routing process")
else:
print ("No inconsistencies found. Continuing with routing process.")
def check_environment_var(env_str:str) -> Path:
out_path = os.environ.get(env_str)
if out_path:
return Path(out_path)
else:
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
exit()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='fetches the x/y positions from a dxf file', prog='getpositions')
parser.add_argument('-f', '--filename', action='store', required=True, default="ST_6300_Steuerungstestlayout1_neueBloecke.dwg", help='which file should be fetched', metavar='myfile.dxf')
parser.add_argument('-s', '--sensors', action='store_true', help='fetch all position of sensors, motors, actors and subdistributors')
parser.add_argument('-r', '--rack', action='store_true', help='fetch all positions of all cable racks')
parser.add_argument('-w', '--write', action='store', help='write results into a json file')
parser.add_argument('-c', '--console', action='store_true', help='print results to output')
parser.add_argument('-e', '--errors', action='store', help='write an error file in case of double defined items in layout')
parser.add_argument('-n', '--scan', action='store_true', help='print all layer of racs, distributes and equiment not empty')
args = parser.parse_args()
out_dir = check_environment_var('PROJECT_DATA')
work_dir = check_environment_var('PROJECT_WORK')
config_dir = check_environment_var("PROJECT_CFG")
filename = Path(args.filename)
if not filename.suffix == ".dxf":
print("only available for .dxf files")
exit()
(dxf_path, dexists) = check_file_in_work(work_dir, filename)
if dexists == False:
print("no such file ")
parser.print_help()
exit()
if dxf_is_binary(dxf_path): # Wenn dxf eine binary ist, dann komplett parsen und modelspace anlegen
print("Given .dxf-file is binary dxf. Proceeding to read file. Watch RAM-usage.")
doc = get_dxf_file(dxf_path)
msp = doc.modelspace()
use_iter = False
else:
print("Given .dxf-file is ASCII-dxf. Proceeding to use iterative functions. Process may take longer.")
use_iter = True
res_sens = dict()
res_cables = dict()
res_dist = dict()
res_rac = dict()
res_mappings = dict()
if args.sensors or args.dists or args.rack:
# Allgemeine Config Laden
config = configparser.ConfigParser(allow_no_value=True, delimiters=("="))
config.optionxform = lambda option: option # preserve case for letters
config.read(os.path.join(config_dir, "allgemein.cfg"))
# Betriebsmittelkennzeichnungs-Config laden
config_BMK = configparser.ConfigParser(allow_no_value=True, delimiters=("="))
config_BMK.optionxform = lambda option: option # preserve case for letters
config_BMK.read(os.path.join(config_dir, "BMK.cfg"))
# Kabel-Config laden
config_cables = configparser.ConfigParser(allow_no_value=True, delimiters=("="))
config_cables.optionxform = lambda option: option
config_cables.read(os.path.join(config_dir, "kabel.cfg"))
validate_configs()
output_results = dict()
if args.sensors:
# Sensoren auslesen
if use_iter:
res_sens, res_double = get_input_positions_iter(dxf_path)
else:
res_sens, res_double = get_input_positions(msp)
if args.errors and len(res_double) > 0:
print("Duplicate blocks found. Writing errors-file.")
err_ids = list(res_double.keys())
write_results(to_json(list(err_ids)), work_dir, args.errors)
output_results['sensors'] = res_sens
#output_results['cables'] = res_cables
if args.console:
print(to_json(res_sens))
# Mapping zu Sensoren auslesen
(res_mappings, warnings) = create_mappings(res_sens)
output_results['mappings'] = res_mappings
if args.console:
print(to_json(res_mappings))
# Distributoren auslesen
if use_iter:
res_dist = get_subdistributor_positions_iter(dxf_path, res_mappings)
else:
res_dist = get_subdistributor_positions(msp, res_mappings)
output_results['distributors'] = res_dist
if args.console:
print(to_json(res_dist))
# Tunnel auslesen
if use_iter:
res_tunnel = get_tunnel_positions_iter(dxf_path)
else:
res_tunnel = get_tunnel_positions(msp)
output_results['tunnels'] = res_tunnel
if args.console:
print(to_json(res_tunnel))
if args.rack:
if use_iter:
res_rac = get_rack_positions_iter(dxf_path)
else:
res_rac = get_rack_positions(msp)
output_results['racks'] = res_rac
if args.console:
print(to_json(res_rac))
if args.write:
basename = os.path.splitext(args.write)[0]
res_not_found = check_existance(res_mappings, res_dist, res_sens)
res_not_found["missing_attributes"] = warnings
output_results["not_found"] = res_not_found
output_results["double_ids"] = res_double
write_results(to_json(output_results), work_dir, f"{basename}.json")
else:
parser.print_help()