Scripts/AutoCAD/IO/build_io_table.py
2025-12-06 19:16:58 +04:00

187 lines
8.0 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# (script content truncated in preview; full content below)
import argparse, re
from pathlib import Path
from typing import List, Dict
import pandas as pd
ORDER_VFD = ["I0","I1","I2","I3","IO0","IO1","SI0","SI1","SI2","SI3","SO0"]
ORDER_FIOM = [f"X{i}_{j}" for i in [3,2,1,0,7,6,5,4] for j in (0,1)]
ORDER_FIOH = [f"C{i}_{j}" for i in [7,5,3,1,8,6,4,2] for j in ("A","B")]
def load_canon(paths: List[Path]) -> Dict[str, str]:
canon = {}
for p in paths:
if not p or not Path(p).exists():
continue
c = pd.read_excel(p, dtype=str).rename(columns=lambda x: str(x).strip())
cols = [col for col in c.columns if col.lower() in ("assigned device","assigned_device","device","name")]
descs = [col for col in c.columns if col.lower() in ("description","desc")]
if not cols or not descs:
continue
c = c[[cols[0], descs[0]]].dropna()
c[cols[0]] = c[cols[0]].astype(str).str.strip().str.upper()
c[descs[0]] = c[descs[0]].astype(str).str.strip().str.upper()
canon.update(dict(zip(c[cols[0]], c[descs[0]])))
return canon
def strip_digits(tok: str) -> str:
if tok.startswith(("EPC","FPE")):
return tok
import re as _re
return _re.sub(r'\d+$','', tok)
def family_keys(dev: str):
s = dev.strip().upper()
parts = [p for p in s.split("_") if p]
if not parts: return []
head = "PDP" if s.startswith("PDP") else "CONV"
keys = []
if len(parts) >= 3:
t2, t1 = parts[-2], parts[-1]
t1s = t1 if t1.startswith(("EPC","FPE")) else strip_digits(t1)
keys += [f"{head}_{t2}_{t1}", f"{head}_{t2}_{t1s}"]
import re as _re
last = parts[-1]
if _re.fullmatch(r"(DISC|DSIC|TPE\d*|JPE\d*|ENW\d*|ENSH\d*|PRX1?|PX1|LRPE\d*|TS\d*|BDS\d*|EPC\d+|FPE\d+|CB\d+|PWM\d+|FIOH1|STO1)", last):
keys += [f"{head}_{last}", last]
if "BCN" in s:
if s.endswith("_A"): keys.append("CONV_BCN_A")
elif s.endswith("_R"): keys.append("CONV_BCN_R")
elif s.endswith("_G"): keys.append("CONV_BCN_G")
elif s.endswith("_B"): keys.append("CONV_BCN_B")
elif s.endswith("_W"): keys.append("CONV_BCN_W")
elif s.endswith("_H"): keys.append("CONV_BCN_H")
else: keys.append("CONV_BCN")
seen=set(); out=[]
for k in keys:
if k and k not in seen:
seen.add(k); out.append(k)
return out
def fallback_desc(dev: str) -> str:
import re as _re
d = dev.upper()
if d == "SPARE": return ""
if "VFD" in d and ("DISC" in d or "DSIC" in d): return "DISCONNECT AUX"
if _re.search(r"_TPE\d+|^TPE\d+", d): return "TRACKING PHOTOEYE"
if _re.search(r"_JPE\d+|^JPE\d+", d): return "JAM PHOTOEYE"
if _re.search(r"_EPC\d+|^EPC\d+", d): return "E-STOP PULLCORD"
if "ENSH" in d: return "SHAFT ENCODER"
if "ENW" in d: return "WHEEL ENCODER"
if "LRPE" in d and "_R" in d: return "LONG RANGE PHOTOEYE RCV"
if "LRPE" in d and "_S" in d: return "LONG RANGE PHOTOEYE SND"
if "LRPE" in d: return "LONG RANGE PHOTOEYE"
if any(x in d for x in ["PX1","PRX","PRX1"]): return "PROXIMITY SENSOR"
if any(x in d for x in ["_SOL","_SOV","_SV","_SOLV"]): return "SOLENOID VALVE"
if _re.search(r"_FIOH\d+|^FIOH\d+", d): return "I/O LINK HUB"
if "BCN" in d:
if d.endswith("_A"): return "AMBER BEACON LIGHT"
if d.endswith("_R"): return "RED BEACON LIGHT"
if d.endswith("_G"): return "GREEN BEACON LIGHT"
if d.endswith("_B"): return "BLUE BEACON LIGHT"
if d.endswith("_W"): return "WHITE BEACON LIGHT"
if d.endswith("_H"): return "HORN BEACON"
return "BEACON LIGHT"
if d.endswith("_FPE1"): return "FULL PHOTOEYE 100%"
if d.endswith("_FPE2"): return "FULL PHOTOEYE 50%"
if _re.search(r"_PS\d+$", d): return "PRESSURE SENSOR"
if _re.search(r"_BDS\d+_R$", d): return "BELT DISENGAGEMENT SENSOR RCV"
if _re.search(r"_BDS\d+_S$", d): return "BELT DISENGAGEMENT SENSOR SND"
if _re.search(r"_TS\d+_R$", d): return "TRASH SENSOR RCV"
if _re.search(r"_TS\d+_S$", d): return "TRASH SENSOR SND"
if "STO1" in d: return "SAFETY TORQUE OFF"
if d.startswith("PDP") and "_CB" in d: return "CIRCUIT BREAKER MONITORING"
if d.startswith("PDP") and "_PWM" in d: return "PHASE MONITOR"
return "DEVICE"
def desc_for(dev: str, canon: dict) -> str:
s = str(dev).strip().upper()
if not s: return ""
if s in canon: return canon[s]
for k in family_keys(s):
if k in canon: return canon[k]
return fallback_desc(s)
def choose_order(controller_name: str, available_columns):
s = str(controller_name).upper()
if "VFD" in s:
return [c for c in ORDER_VFD if c in available_columns]
if "FIOM" in s:
return [c for c in ORDER_FIOM if c in available_columns]
if "FIOH" in s:
return [c for c in ORDER_FIOH if c in available_columns]
return [c for c in available_columns if c in set(ORDER_FIOM+ORDER_FIOH+ORDER_VFD)]
def priority_for_controller(controller_name: str) -> int:
s = str(controller_name).upper()
if s.startswith("PDP") and "FIOM" in s: return 0
if s.startswith("PDP") and "FIOH" in s: return 1
return 2
def main():
import pandas as pd
import argparse, re
from pathlib import Path
ap = argparse.ArgumentParser(description="Build 5-column IO mapping Excel from source workbook.")
ap.add_argument("input", help="Input Excel workbook.")
ap.add_argument("output", help="Output Excel path.")
ap.add_argument("--sheet", default="Summary", help="Sheet to read (default Summary; falls back to first sheet).")
ap.add_argument("--canon", nargs="*", default=[], help="Optional canon Excel(s) with Assigned device/Description columns.")
args = ap.parse_args()
inp = Path(args.input)
xls = pd.ExcelFile(inp)
sheet = args.sheet if args.sheet in xls.sheet_names else xls.sheet_names[0]
df = pd.read_excel(inp, sheet_name=sheet, dtype=str).rename(columns=lambda c: str(c).strip())
if "P_TAG1" not in df.columns:
raise SystemExit("P_TAG1 column not found in the selected sheet.")
sig_cols_available = [c for c in df.columns if re.fullmatch(r"(I0|I1|I2|I3|IO0|IO1|SI0|SI1|SI2|SI3|SO0|X[0-7]_[01]|C[1-8]_[AB])", c)]
canon_map = load_canon([Path(p) for p in args.canon])
rows = []
for _, r in df.iterrows():
ctrl = str(r.get("P_TAG1","")).strip()
if not ctrl:
continue
order = choose_order(ctrl, sig_cols_available)
for sig in order:
val = r.get(sig)
dev = (str(val).strip() if pd.notna(val) else "")
if dev == "":
dev = "SPARE"
addr = f"{ctrl}_{sig}"
desc = "" if dev.upper()=="SPARE" else desc_for(dev, canon_map)
dU = dev.upper()
if re.search(r'_ESTOP1$', dU): desc = "ESTOP OK"
elif re.search(r'_SS\d+_SPB_LT$', dU): desc = "SS STATION START PUSHBUTTON LIGHT"
elif re.search(r'_SS\d+_SPB$', dU): desc = "SS STATION START PUSHBUTTON"
elif re.search(r'_SS\d+_STPB$', dU): desc = "SS STATION STOP PUSHBUTTON"
elif re.search(r'_S\d+_PB_LT$', dU): desc = "START PUSHBUTTON LIGHT"
elif re.search(r'_S\d+_PB$', dU): desc = "START PUSHBUTTON"
elif re.search(r'_JR\d+_PB_LT$', dU): desc = "JAM RESET PUSHBUTTON LIGHT"
elif re.search(r'_JR\d+_PB$', dU): desc = "JAM RESET PUSHBUTTON"
elif re.search(r'_EN\d*_PB_LT$', dU): desc = "ENABLE PUSHBUTTON LIGHT"
elif re.search(r'_EN\d*_PB$', dU): desc = "ENABLE PUSHBUTTON"
rows.append({
"Controller name": ctrl,
"Signal type": sig,
"Address name": addr,
"Assigned device": dev,
"Description": desc
})
out = pd.DataFrame(rows)
out["_p"] = out["Controller name"].map(priority_for_controller)
out = out.sort_values(["_p","Controller name"], kind="mergesort").drop(columns=["_p"]).reset_index(drop=True)
out.to_excel(Path(args.output), index=False)
if __name__ == "__main__":
main()