#!/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()