187 lines
8.0 KiB
Python
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()
|