From 6e36186012b2a6ede3d081dbf8f742880b76b35d Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 14 May 2025 18:04:17 +0200 Subject: [PATCH] Creado nuevo grupo de scripts IO_adaptation --- .../script_groups/IO_adaptation/IO Adapted.md | 125 +++++ .../IO_adaptation/Inputs PLCTags.xlsx | Bin 0 -> 8115 bytes .../script_groups/IO_adaptation/PLC Tags.csv | 5 + .../IO_adaptation/TagTable Properties.csv | 2 + .../IO_adaptation/description.json | 6 + backend/script_groups/IO_adaptation/readme.md | 77 +++ .../IO_adaptation/scripts_description.json | 8 + backend/script_groups/IO_adaptation/x1.py | 472 ++++++++++++++++ .../IO_adaptation/x1_agregatetags_to_md.py | 193 +++++++ backend/script_groups/IO_adaptation/x2.py | 511 ++++++++++++++++++ .../ObtainIOFromProjectTia/Inputs PLCTags.csv | 76 +++ .../Inputs PLCTags.xlsx | Bin 0 -> 8115 bytes .../ObtainIOFromProjectTia/log_x3.txt | 11 +- .../ObtainIOFromProjectTia/readme.md | 39 +- .../ObtainIOFromProjectTia/x3.py | 79 ++- .../XML Parser to SCL/x0_main.py | 4 +- data/log.txt | 45 +- 17 files changed, 1606 insertions(+), 47 deletions(-) create mode 100644 backend/script_groups/IO_adaptation/IO Adapted.md create mode 100644 backend/script_groups/IO_adaptation/Inputs PLCTags.xlsx create mode 100644 backend/script_groups/IO_adaptation/PLC Tags.csv create mode 100644 backend/script_groups/IO_adaptation/TagTable Properties.csv create mode 100644 backend/script_groups/IO_adaptation/description.json create mode 100644 backend/script_groups/IO_adaptation/readme.md create mode 100644 backend/script_groups/IO_adaptation/scripts_description.json create mode 100644 backend/script_groups/IO_adaptation/x1.py create mode 100644 backend/script_groups/IO_adaptation/x1_agregatetags_to_md.py create mode 100644 backend/script_groups/IO_adaptation/x2.py create mode 100644 backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.csv create mode 100644 backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.xlsx diff --git a/backend/script_groups/IO_adaptation/IO Adapted.md b/backend/script_groups/IO_adaptation/IO Adapted.md new file mode 100644 index 0000000..aeee4da --- /dev/null +++ b/backend/script_groups/IO_adaptation/IO Adapted.md @@ -0,0 +1,125 @@ +# Adaptación I/O entre Hardware PLC y Software Master + +## Tabla de Adaptación + +| IO | Master Tag | PLC Description | Master Description | Certeza | Alternative | +| --- | --- | --- | --- | --- | --- | +| I0.0 | DI_AuxVoltage_On | AUXILIARY CIRC. ON AUSILIARI INSERITI | Electrical Panel Restored | Alto | | +| I0.1 | DI_PB_HornReset | SIREN RESET RESET SIRENA | PB Horn Reset | Alto | | +| I0.2 | DI_AlarmReset | RESET RESET | PB Machine Reset | Alto | | +| I0.3 | DI_PB_Machine_Stop | MACHINE STOP ARRESTO MACCHINA | PB Machine Stop | Alto | | +| I0.4 | DI_PB_Machine_Start | MARCIA MACCHINA MACHINE START | PB Machine Start | Alto | | +| I0.5 | DI_Emergency_Pilz_On | PRESENZA TENSIONE VOLTAGE PRESENCE | Pilz Emergency | Medio | DI_UPSsupply, DI_AuxVoltage_On | +| I0.6 | DI_LSN301L | SONDA LIVELLO MINIMO DEAREATORE 1 | LSN301_L - Deaireator Tank Minimun Level | Alto | | +| I0.7 | DI_Min_Syrup_Level | SONDA LIVELLO MINIMO SCIRO - PPC SYRUP MINIMUM LEVEL PROBE | - Syrup Tank Minimun Level | Alto | | +| I1.0 | DI_LSM302L | SONDA LIVELLO MINIMO SERB.STOCCAGGIO - STORE TANK MIN LEVEL PROBE | LSM302_L - Product Tank Minimun Level | Alto | | +| I1.5 | DI_RMM301_Closed | VALVOLA CHIUSA VM1 - CLOSED VALVE VM1 | RMM301 - Feedback OFF (VM1WATER) | Alto | | +| I1.6 | DI_RMP302_Closed | CLOSED VALVE VM2 - VALVOLA CHIUSA VM2 | RMP302 - Feedback OFF (VM2 SYRUP) | Alto | | +| I1.7 | DI_RMM303_Closed | VALVOLA CHIUSA VM3 - CLOSED VALVE VM3 | RMM303 - Feedback OFF (VM3 CO2) | Alto | | +| I2.0 | DI_PPN301_Ovrld | WATER PUMP OVERLOAD - TERMICO POMPA ACQUA | PPN301 - Deaireator Pump Overload | Alto | | +| I2.1 | PPN301_SoftStart_Averia | AVARIA POMPA ACQUA - WATER PUMP FAULT | PPN301_SoftStart_Averia | Alto | | +| I2.2 | DI_PPP302_Ovrld | SYRUP PUMP OVERLOAD - TERMICO POMPA SCIROPPC | PPP302 - Syrup Pump Overload | Alto | | +| I2.3 | DI_PPP302_Contactor | AVARIA POMPA SCIROPPO - SYRUP PUMP FAULT | PPP302 - Syrup Pump Feedback | Medio | DI_SyrRoom_SyrPump_Running | +| I2.4 | DI_PPM303_Ovrld | OVERPRESS.PUMP OVERLOAD - TERMICO POMPA SOVRAPRES. | PPM303 - Product Pump Overload | Alto | | +| I2.5 | DI_PPM306_Ovrld | OVERPRESS.PUMP FAULT - AVARIA POMPA SOVRAPRES. | PPM306 - Recirculating Pump Overload | Medio | DI_PPM303_Ovrld | +| I3.5 | DI_UPSAlarm | ALLARME UPS - UPS ALARM | UPS Alarm | Alto | | +| I3.6 | DI_UPSsupply | ALIMENTAZIONE DA BATTERIE - BATTERY POWER SUPPLY | UPS supply OK | Alto | | +| I3.7 | DI_UPSBatteryReady | BATTERIA TAMPONE PRONTA - BUFFER BATTERY READY | UPS Battery ready | Alto | | +| I4.3 | DI_CIP_CIP_Enable | ALARM ENABLING - ABILITAZIONE ALLARME | From CIP Enable | Bajo | DI_Flr_CIP/RinseFiller, DI_CIP_TankFilling | +| I4.4 | DI_MaxTempAlarm | ABILITAZIONE ALLARME - ALARM ENABLING | Electrical Cabinet High Temperature | Bajo | DI_CIP_Fault, DI_Flr_CIP_CleaningAlarm | +| I5.0 | DI_SyrRoom_SyrPump_Running | POMPA SALA SCIROPPI INMARCIA - SYRUPS ROOM PUMP RUN | From Syrup Room - Syrup Pump Running | Alto | | +| I7.1 | DI_Air_InletPress_OK | AIR PRESSURE GAUGE - PRESSOSTATO ARIA | Air Pressure Switch | Alto | | +| I7.2 | DI_HVP301_Sensor | SENSORE VALVOLA SCARICO SCIROPPO - SYRUP DISCHARGE VALVE SENSOR | GCP301 - Manual Syrup Valve Closed (NO) | Alto | | +| I7.3 | DI_FSS301 | FLOW GAUGE FLUSSOSTATO | FSS301 - Local Cip Return Flow Switch | Alto | | +| Q0.0 | DO_HMIPowerSupply | RIPRISTINO ALIMENTAZIONE HMI - HMI POWER SUPPLY RESTORE | Cut Power to PC | Alto | | +| Q1.0 | DO_SyrRoom_SyrupRequest | RICHIESTA SCIROPPO - SYRUP REQUEST | SYRUP ROOM - Syrup Request | Alto | | +| Q1.1 | DO_SyRm_WaterRequest | WATER REQUEST - RICHIESTA ACQUA | To syrup Room Water Request | Alto | | +| Q7.0 | DO_Horn | ALLARME ACUSTICO - ACOUSTIC ALARM | DO_Horn | Alto | | +| Q7.1 | DO_PB_Green_Lamp | MACHINE START - MARCIA MACCHINA | PB Machine Start Lamp | Alto | | +| Q7.2 | DO_Red_Lamp | MACHINE ALARM - ALLARME MACCHINA | DO_Red_Lamp | Alto | | +| Q7.3 | DO_Yellow_Lamp | ROTAT. LAMP - ROT ALLARM | DO_Yellow_Lamp | Alto | | +| Q7.4 | DO_PPN301_Run | COMANDO POMPA ACQUA - WATER PUMP CONTROL | DO_PPN301_SoftStartPower | Alto | | +| Q7.5 | DO_PPP302_Run | SYRUP PUMP CONTROL - COMANDO POMPA SCIROPPO | DO_PPP302_Run | Alto | | +| Q7.6 | DO_PPM303_Run | COMANDO POMPA SOVRAPRESSIONE - OVERPRESSURE PUMP CONTROL | DO_PPM303_Run | Alto | | +| A16.0 | DO_AVN348 | SFIATO SATURATORE | MIX - Deaireator Inlet | Medio | DO_AVN390, DO_AVM346 | +| A16.1 | DO_AVN350 | DEAREAZIONE ACQUA | nan | Bajo | DO_AVN325, DO_AVN349, DO_AVM339 | +| A16.2 | DO_AVM382 | RICIRCOLO PRODOTTO | Mix - Product Recirculation though chiller | Alto | | +| A16.3 | DO_AVN373 | SCARICO DEAREATORE | MIX - Deaireator 2 Drain | Alto | | +| A16.4 | DO_AVN374 | SCARICO SATURATORE | MIX - Deaireators Connection Drain | Alto | | +| A16.5 | DO_EV67_SyrupLineDrain | SCARICO SCIROPPO | MIX - N10_O101_ | Medio | DO_EV71_FillerPrPipeDrai, DO_EV19_2 | +| A16.6 | DO_AVN329 | DIVOSFERA SATURATORE | MIX - Deaireator 2 Tank Spray Ball | Alto | | +| A16.7 | DO_RVN301_Level | TROPPOPIENO DEAREATORE | MIX - Deaireator Level Control | Alto | | +| A17.0 | DO_AVP317_1 | TROPPO PIENO SATURATORE | MIX - CIP To Syrup | Bajo | DO_AVM353, DO_AVM369 | +| A17.1 | DO_AVS336 | SFIATO ARIA POMPA PRODOTTO | MIX - CIP Recirculation | Bajo | DO_AVM342, DO_AVM380 | +| A17.2 | DO_AVP363 | SCARICO SERPENTINA | MIX - Syrup Line In H2O | Bajo | DO_AVS338, DO_AVM312_Deair_Reflux | +| A17.3 | DO_EV03_SyrupLvlCtrl | RICICLO SCIROPPO START-UP | MIX - N10_O06_ | Bajo | DO_AVM380, DO_EV04_SyrupFillUp | +| A17.5 | DO_AVM341 | INTERCETTAZIONE INIETT.CO2 | MIX - CO2 Inlet | Alto | | +| A17.6 | DO_AVN377 | DIVOSFERADEAREATORE | nan | Bajo | DO_AVM327, DO_AVM329 | +| A17.7 | DO_AVS331 | DEAREAZ. ACOUA IN RISCACOUO | MIX - CIP Venturi | Bajo | DO_AVS332, DO_EV66_FillerRinseWater | +| A18.0 | DO_AVS331 | ASPIRAZIONE VENTURI | MIX - CIP Venturi | Alto | | +| A18.1 | DO_AVS332 | LAVAGGIOVENTURI | MIX - CIP Wash Venturi | Alto | | +| A18.2 | DO_AVS333 | INGRESSO SANIFICANTE 1 | MIX - CIP Caustic | Alto | | +| A18.3 | DO_AVS334 | INGRESSO SANIFICANTE 2 | MIX - CIP Acid | Alto | | +| A18.4 | DO_AVS335 | INGRESSO SANIFICANTE 3 | MIX - CIP Peracetic Acid | Alto | | +| A18.5 | DO_AVS336 | RICIRCOLO SANIFICANTE | MIX - CIP Recirculation | Alto | | +| A18.6 | DO_AVS337 | SCARICO SANIFICANTE 1 | MIX - CIP Drain | Alto | | +| A18.7 | DO_AVS338 | SCARICO SCAMBIATORE | MIX - CIP Heater | Alto | | +| A19.1 | DO_EV71_FiRinseSprayBall | DIVOSFERA SCIROPPO | MIX - N10_O105_ | Bajo | DO_EV68_FillerRinseWater, DO_AVN329 | +| A19.2 | DO_EV71_FillerPrPipeDrai | SCARICO TUBO SCIROPPO | MIX - N10_O104_ | Alto | | +| A19.3 | DO_EV72_FlrRinseTankDrai | SCARICO SATURATORE | MIX - N10_O106_ | Alto | | +| A20.0 | DO_AVN347 | GALLEGGIANTE DEAREATORE 1 | MIX - Deaireator Tank Start CO2 Injection 1 | Bajo | DO_AVM339, DO_AVM340 | +| A20.1 | DO_AVM340 | INVASAMENTODEAREATORE1 | MIX - Still Water By-Pass Product Intercept | Bajo | DO_AVM339, DO_AVN347 | +| A20.2 | DO_EV03_SyrupLvlCtrl | GALLEGGIANTE SCIROPPO | MIX - N10_O06_ | Bajo | DO_EV04_SyrupFillUp, DO_RVP303 | +| A20.3 | DO_EV04_SyrupFillUp | INVASAMENTO SCIROPPO | MIX - N10_O07_ | Alto | | +| PEW100 | P_AI_LTM302 | LIVELLO SERBATOIO DI STOCCAGGIO - STORAGE TANK LEVEL | LTM302 - Product Tank Level | Alto | | +| PEW102 | P_AI_PTM304 | SENSORE PRESSIONE SERB.DI STOCCAGGIO - STORAGE TANK PRESSURE SENSOR | PTM304 - Product Tank Pressure | Alto | | +| PEW104 | P_AI_PTF203 | CONTR.PORTATA CO2 PER DEAREAZIONE - AIR VACUUM CO2 FLOW CONTROL | PTF203 - Differential Pressure | Alto | | +| PEW106 | P_AI_PTM308 | CONTROLLO PRESSIONE SERBATOIO CO2 - CO2 TANK PRESSURE CONTROL | PTM308 - PCM306 Infeed Pressure | Alto | | +| PEW108 | P_AI_TTM306 | PRODUCT TEMPERATURE SENSOR - SENSORE TEMPERATURA PRODOTTO | TTM306 - Chiller Temperature | Alto | | +| PEW112 | P_AI_TTN321 | TEMP. H2O DEAREATORE - H2O DEAREATOR TEMP. | TTN321 - Deaireator Temperature | Alto | | +| PEW114 | P_AI_RVN304 | NORGREN PV VLAVE ANALOG - OUTPUT USCITA ANALOGICA VALVOLA NORGREN PV | RVN304 - Deaireation Valve | Alto | | +| PAW100 | P_AO_RMM301 | VALVOLA MOTORIZZATA ACQUA - WATER MOTOR VALVE | RMM301 - Water Flow Control | Alto | | +| PAW102 | P_AO_RMP302 | VALVOLA MOTORIZZATA SCIROPPO - SYRUP MOTOR VALVE | RMP302 - Syrup Flow Control | Alto | | +| PAW104 | P_AO_RMM303 | VALVOLA MOTORIZZATA CO2 - CO2 MOTOR VALVE | RMM303 - Gas Flow Control | Alto | | +| PAW108 | P_AO_PCM306 | AIR VACUUM CO2 FLOW CONTROL - CONTR.PORTATA CO2 PER DEAREAZIONE | PCM306 - Gas Injection Pressure Control | Alto | | +| PAW110 | P_AO_RVM319 | PRODUCT TEMPERATURE REGULATION - REGOLAZIONE TEMPERATURA PRODOTTO | RVM319 - Chiller Temperature control | Alto | | +| PAW112 | P_AO_RVS318 | SANIT. TEMP. CONTROL C- ONTROLLO TEMPERATURA SANIFICANTE | RVS318 - Local Cip Heating Valve | Alto | | +| PAW114 | P_AO_RVN304 | USCITA ANALOGICA VALVOLA NORGREN - SP NORGREN SP VALVE ANALOG OUTPUT | RVN304 - Deaireation Valve | Alto | | +| PAW122 | P_AO_RVM301 | CONTROLLO PRESSIONE SERBATOIO CO2 - CO2 TANK PRESSURE CONTROL | RVM301 - Product Tank Pressure Valve | Alto | | +| EW 3080..3084 | P_FTN301_Flow | Volume Flow (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 3100..3104 | P_FTN301_Totalizer | Totalizer Value / Control (PROFIBUS) | MIX - Profibus Variables | Alto | | +| AW 3100..3100 | P_FTN301_Tot_Ctrl | Totalizer Value / Control (PROFIBUS) | MIX - | Alto | | +| EW 2030..2034 | P_FTP302_Flow | Mass Flow (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 2045..2049 | P_FTP302_Density | Density (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 2050..2054 | P_FTP302_Brix | Concentration (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 2055..2059 | P_FTP302_Temp | Temperature (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 2070..2074 | P_FTP302_Totalizer | Totalizer Value / Control (PROFIBUS) | MIX - Profibus Variables | Alto | | +| AW 2070..2070 | P_FTP302_Tot_Ctrl | Totalizer Value / Control (PROFIBUS) | MIX - | Alto | | +| EW 3200..3204 | P_FTM303_Flow | Mass Flow (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 3215..3219 | P_FTM303_Density | Density (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 3225..3229 | P_FTM303_Temperature | Temperature (PROFIBUS) | MIX - Profibus Variables | Alto | | +| EW 3240..3244 | P_FTM303_Totalizer | Totalizer Value / Control (PROFIBUS) | MIX - Profibus Variables | Alto | | +| AW 3240..3240 | P_FTM303_Tot_Ctrl | Totalizer Value / Control (PROFIBUS) | MIX - | Alto | | +| EW 15000..15031 | P_PDS_CO2 | IN128 mPDS5>PLC_4_1 (PROFIBUS) | nan | Medio | P_gMaselli_ProductCO2, P_AI_ProductCO2 | +| EW 15032..15063 | P_PDS_Product_Brix | IN128 mPDS5>PLC_4_2 (PROFIBUS) | nan | Medio | P_gMaselli_ProductBrix, P_FTP302_Brix | +| EW 15064..15095 | P_PDS_Temperature | IN128 mPDS5>PLC_4_3 (PROFIBUS) | nan | Medio | P_gMaselli_ProductTemp, P_FTM303_Temperature | +| EW 15096..15127 | P_PDS_Density | IN128 mPDS5>PLC_4_4 (PROFIBUS) | nan | Medio | P_gMaselli_ProductNumber, P_FTM303_Density | +| AW 15000..15031 | P_PDS_Recipe_Number | OUT128 PLC>mPDS5_4_1 (PROFIBUS) | PDS Recipe Number | Medio | P_gMaselli_RecipeSetNum | +| AW 15032..15063 | P_PDS_Freeze_To_PDS | OUT128 PLC>mPDS5_4_2 (PROFIBUS) | nan | Medio | MaselliHold, P_gMaselli_RecipeSetNumStr | +| AW 15064..15095 | P_PDS_Stop_to_PDS | OUT128 PLC>mPDS5_4_3 (PROFIBUS) | nan | Medio | MaselliSpare | +| AW 15096..15127 | DO_FillerNextRecipe | OUT128 PLC>mPDS5_4_4 (PROFIBUS) | MIX - | Bajo | P_gMaselli_RecipeSetNum | + +## Excepciones y Problemas + +| IO | Problema Detectado | +| --- | --- | +| I4.3 | Descripción ambigua "ALARM ENABLING" podría corresponder a varias entradas | +| I4.4 | Descripción ambigua "ABILITAZIONE ALLARME" podría corresponder a varias entradas | +| A16.1 | No se encuentra una correspondencia clara para "DEAREAZIONE ACQUA" | +| A17.0 | No hay correspondencia clara para "TROPPO PIENO SATURATORE" | +| A17.1 | No hay correspondencia clara para "SFIATO ARIA POMPA PRODOTTO" | +| A17.6 | No hay correspondencia clara para "DIVOSFERADEAREATORE" | +| A19.1 | Descripción "DIVOSFERA SCIROPPO" sin correspondencia exacta | +| A20.0 | "GALLEGGIANTE DEAREATORE 1" sin correspondencia exacta | +| A20.1 | "INVASAMENTODEAREATORE1" sin correspondencia clara | +| A20.2 | "GALLEGGIANTE SCIROPPO" sin correspondencia exacta | diff --git a/backend/script_groups/IO_adaptation/Inputs PLCTags.xlsx b/backend/script_groups/IO_adaptation/Inputs PLCTags.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..63139e2a2f7e8d4bce6b5f8059e239fcd8d1d79d GIT binary patch literal 8115 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/ +├── _CAx_Export.aml +├── / +│ ├── ProgramBlocks_XML/ +│ │ └── ... (archivos XML de bloques) +│ ├── ProgramBlocks_SCL/ +│ │ └── ... (archivos SCL de bloques) +│ ├── ProgramBlocks_CR/ +│ │ └── ... (archivos XML de referencias cruzadas de bloques) +│ ├── PlcTags/ +│ │ └── ... (archivos XML de tablas de tags) +│ ├── PlcTags_CR/ +│ │ └── ... (archivos XML de referencias cruzadas de tablas de tags) +│ ├── PlcDataTypes_CR/ +│ │ └── ... (archivos XML de referencias cruzadas de UDTs) +│ ├── SystemBlocks_CR/ +│ │ └── ... +│ └── SoftwareUnits_CR/ +│ └── ... +│ └── Documentation/ +│ └── Source +│ └── ... (archivos md de bloques de programa) +│ └── JSON +│ └── ... (archivos JSON temporales) +│ └── xref_calls_tree.md +│ └── xref_db_usage_summary.md +│ └── xref_plc_tags_summary.md +│ └── full_project_representation.md +│ └── _CAx_Export_Hardware_Tree.md + +├── / +│ ├── ProgramBlocks_XML/ +│ │ └── ... +│ └── ... +└── ... \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/scripts_description.json b/backend/script_groups/IO_adaptation/scripts_description.json new file mode 100644 index 0000000..e0d5ffa --- /dev/null +++ b/backend/script_groups/IO_adaptation/scripts_description.json @@ -0,0 +1,8 @@ +{ + "x1_agregatetags_to_md.py": { + "display_name": "Convertir Excel Tags a Markdown", + "short_description": "This script converts Excel files containing tags into Markdown tables", + "long_description": "Tener en cuenta que el nombre de los archivo excel determina si en la tabla se asignan los tag a las entradas o las salidas.", + "hidden": false + } +} \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/x1.py b/backend/script_groups/IO_adaptation/x1.py new file mode 100644 index 0000000..7a681e5 --- /dev/null +++ b/backend/script_groups/IO_adaptation/x1.py @@ -0,0 +1,472 @@ +import pandas as pd +import re +import os +import shutil +import openpyxl +import sys +from datetime import datetime + +def read_markdown_table(file_path): + """Leer tabla en formato Markdown de Obsidian y convertirla a DataFrame.""" + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + # Dividir el contenido en líneas + lines = content.strip().split('\n') + + # Encontrar el inicio de la tabla (la primera línea que comienza con '|') + table_start = None + for i, line in enumerate(lines): + if line.strip().startswith('|'): + table_start = i + break + + if table_start is None: + print("No se encontró ninguna tabla en el archivo") + return pd.DataFrame() + + # Encontrar todas las líneas de la tabla + table_lines = [] + for i in range(table_start, len(lines)): + line = lines[i].strip() + if line.startswith('|'): + table_lines.append(line) + elif not line: # Línea vacía podría indicar el final de la tabla + # Si la siguiente línea no comienza con '|', consideramos que es el final de la tabla + if i + 1 < len(lines) and not lines[i + 1].strip().startswith('|'): + break + else: + break # Si no comienza con '|' y no está vacía, es el final de la tabla + + if len(table_lines) < 3: # Necesitamos al menos encabezado, separador y una fila de datos + print("La tabla no tiene suficientes filas") + return pd.DataFrame() + + # Procesar encabezados + header_line = table_lines[0] + separator_line = table_lines[1] + + # Verificar que la segunda línea sea realmente un separador + is_separator = all(cell.strip().startswith(':') or cell.strip().startswith('-') + for cell in separator_line.split('|')[1:-1] if cell.strip()) + + if not is_separator: + print("Advertencia: La segunda línea no parece ser un separador. Se asume que es parte de los datos.") + separator_idx = None + else: + separator_idx = 1 + + # Extraer encabezados + header_cells = header_line.split('|') + # Eliminar elementos vacíos al principio y al final + if not header_cells[0].strip(): + header_cells = header_cells[1:] + if not header_cells[-1].strip(): + header_cells = header_cells[:-1] + + headers = [h.strip() for h in header_cells] + print(f"Encabezados detectados: {headers}") + + # Procesar filas de datos + data_start_idx = 2 if separator_idx == 1 else 1 + data = [] + + for line in table_lines[data_start_idx:]: + # Dividir la línea por el carácter pipe + cells = line.split('|') + + # Eliminar elementos vacíos al principio y al final + if not cells[0].strip(): + cells = cells[1:] + if not cells[-1].strip(): + cells = cells[:-1] + + # Limpiar valores + row_values = [cell.strip() for cell in cells] + + # Asegurar que la fila tenga el mismo número de columnas que los encabezados + if len(row_values) != len(headers): + print(f"Advertencia: Fila con {len(row_values)} valores, pero se esperaban {len(headers)}. Ajustando...") + + # Intentar ajustar la fila para que coincida con el número de columnas + if len(row_values) < len(headers): + row_values.extend([''] * (len(headers) - len(row_values))) + else: + row_values = row_values[:len(headers)] + + data.append(row_values) + + # Convertir a DataFrame + df = pd.DataFrame(data, columns=headers) + + return df + +def create_log_file(log_path): + """Crear un archivo de log con timestamp.""" + log_dir = os.path.dirname(log_path) + if log_dir and not os.path.exists(log_dir): + os.makedirs(log_dir) + + with open(log_path, 'w', encoding='utf-8') as log_file: + log_file.write(f"Log de actualización de PLCTags - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + log_file.write("=" * 80 + "\n\n") + + return log_path + +def log_message(log_path, message): + """Añadir mensaje al log.""" + with open(log_path, 'a', encoding='utf-8') as log_file: + log_file.write(message + "\n") + print(message) + +def transform_io_address(address): + """ + Transform IO addresses according to the required format: + - Ixx.x → %Exx.x + - Exx.x → %Exx.x + - Qxx.x → %Axx.x + - Axx.x → %Axx.x + - PEWxx → %EWxx + - PAWxx → %AWxx + """ + if not address or not isinstance(address, str): + return address + + address = address.strip() + + # Patterns for boolean addresses + if re.match(r'^I(\d+)\.(\d+)$', address): + return re.sub(r'^I(\d+)\.(\d+)$', r'%E\1.\2', address) + elif re.match(r'^E(\d+)\.(\d+)$', address): + return re.sub(r'^E(\d+)\.(\d+)$', r'%E\1.\2', address) + elif re.match(r'^Q(\d+)\.(\d+)$', address): + return re.sub(r'^Q(\d+)\.(\d+)$', r'%A\1.\2', address) + elif re.match(r'^A(\d+)\.(\d+)$', address): + return re.sub(r'^A(\d+)\.(\d+)$', r'%A\1.\2', address) + + # Patterns for word addresses + elif re.match(r'^PEW(\d+)$', address): + return re.sub(r'^PEW(\d+)$', r'%EW\1', address) + elif re.match(r'^PAW(\d+)$', address): + return re.sub(r'^PAW(\d+)$', r'%AW\1', address) + + # If already in correct format or unknown format, return as is + return address + +def update_excel_with_adaptation(excel_path, adaptation_path, output_path=None, log_path=None): + """ + Actualiza el archivo Excel con la información de adaptación. + - Modifica la columna "Logical Address" según las reglas: + 1. Si el tag se encuentra en la tabla de adaptación, convierte el formato de IO. + 2. Si no se encuentra y tiene formato %E, %A, %EW, %AW, asigna una dirección %M. + + Args: + excel_path: Ruta al archivo Excel de tags PLC + adaptation_path: Ruta al archivo de adaptación en Markdown + output_path: Ruta para guardar el Excel actualizado (si es None, sobrescribe el original) + log_path: Ruta para el archivo de log + """ + # Si no se especifica ruta de salida, sobrescribir el archivo original + if output_path is None: + output_path = excel_path + + # Si no se especifica ruta de log, crear una por defecto + if log_path is None: + log_dir = os.path.dirname(output_path) + log_filename = f"update_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + log_path = os.path.join(log_dir, log_filename) + + # Crear archivo de log + create_log_file(log_path) + log_message(log_path, f"Archivo Excel de entrada: {excel_path}") + log_message(log_path, f"Archivo de adaptación: {adaptation_path}") + log_message(log_path, f"Archivo Excel de salida: {output_path}") + log_message(log_path, "-" * 80) + + # Leer el archivo de adaptación + adaptation_df = read_markdown_table(adaptation_path) + + # Identificar automáticamente la columna Master TAG + master_tag_col = None + for col in adaptation_df.columns: + # Buscar columnas con "master" y "tag" en el nombre (insensible a mayúsculas) + if "master" in col.lower() and "tag" in col.lower(): + master_tag_col = col + break + + # Si no encontramos la columna por nombre exacto, intentar con coincidencias parciales + if not master_tag_col: + for col in adaptation_df.columns: + if any(keyword in col.lower() for keyword in ["master", "tag", "name"]): + master_tag_col = col + break + + # Si aún no hemos encontrado, verificar el contenido de las columnas + if not master_tag_col and not adaptation_df.empty: + # Buscar columnas que contengan valores que parezcan tags (con formato DI_xxx, DO_xxx, etc.) + for col in adaptation_df.columns: + # Tomar algunas muestras + samples = adaptation_df[col].dropna().astype(str).head(5).tolist() + # Comprobar si alguna muestra coincide con el patrón de Master TAG + tag_pattern = r'^[A-Z]{2,3}_[A-Za-z0-9_]+$' + if any(re.match(tag_pattern, s) for s in samples): + master_tag_col = col + break + + if not master_tag_col: + error_msg = "Error: No se encontró la columna 'Master Tag' o similar en el archivo de adaptación" + log_message(log_path, error_msg) + log_message(log_path, f"Columnas disponibles: {adaptation_df.columns.tolist()}") + return False + + log_message(log_path, f"Usando columna '{master_tag_col}' para el mapeo de tags") + + # Aseguramos que no tenemos filas completamente vacías o solo con Master TAG vacío + adaptation_df = adaptation_df[adaptation_df[master_tag_col].notna() & + (adaptation_df[master_tag_col] != '')] + + # Identificar automáticamente la columna IO + io_col = None + for col in adaptation_df.columns: + # Buscar coincidencias exactas + if col.lower() == "io": + io_col = col + break + + # Buscar coincidencias parciales + if any(keyword in col.lower() for keyword in ["io", "i/o", "address", "logical"]): + io_col = col + break + + # Si aún no encontramos, verificar el contenido de las columnas + if not io_col and not adaptation_df.empty: + # Buscar columnas que contengan valores que parezcan direcciones IO (I0.0, Q1.2, etc.) + for col in adaptation_df.columns: + # Tomar algunas muestras + samples = adaptation_df[col].dropna().astype(str).head(5).tolist() + + # Definir patrones para direcciones IO + io_patterns = [ + r'^[IQM][0-9]+\.[0-9]+$', # Ejemplo: I0.0, Q1.2 + r'^PE[WBD][0-9]+$', # Ejemplo: PEW100 + r'^PA[WBD][0-9]+$' # Ejemplo: PAW100 + ] + + # Verificar si alguna muestra coincide con algún patrón + matches = False + for pattern in io_patterns: + if any(re.match(pattern, s) for s in samples): + matches = True + break + + if matches: + io_col = col + break + + if not io_col: + error_msg = "Error: No se encontró la columna 'IO' o similar en el archivo de adaptación" + log_message(log_path, error_msg) + log_message(log_path, f"Columnas disponibles: {adaptation_df.columns.tolist()}") + return False + + log_message(log_path, f"Usando columna '{io_col}' para los valores de IO") + + # Eliminar el archivo de salida si ya existe + if os.path.exists(output_path): + try: + os.remove(output_path) + log_message(log_path, f"Archivo de salida existente eliminado: {output_path}") + except Exception as e: + log_message(log_path, f"Error al eliminar archivo existente: {e}") + return False + + # Crear una copia exacta del archivo Excel original + try: + shutil.copy2(excel_path, output_path) + log_message(log_path, f"Archivo Excel copiado: {excel_path} -> {output_path}") + except Exception as e: + log_message(log_path, f"Error al copiar el archivo Excel: {e}") + return False + + # Abrir el archivo Excel copiado usando openpyxl para preservar estructura + try: + workbook = openpyxl.load_workbook(output_path) + log_message(log_path, f"Archivo Excel abierto correctamente: {output_path}") + log_message(log_path, f"Hojas disponibles: {workbook.sheetnames}") + except Exception as e: + log_message(log_path, f"Error al abrir el archivo Excel: {e}") + return False + + # Crear un diccionario de actualización desde el archivo de adaptación + update_dict = {} + unmatched_count = 0 + + for idx, row in adaptation_df.iterrows(): + master_tag = row[master_tag_col] + io_value = row[io_col] + + # Convertir a string y limpiar espacios + master_tag_str = str(master_tag).strip() if not pd.isna(master_tag) else "" + io_value_str = str(io_value).strip() if not pd.isna(io_value) else "" + + if master_tag_str and io_value_str: + update_dict[master_tag_str] = transform_io_address(io_value_str) + else: + unmatched_count += 1 + + if unmatched_count > 0: + log_message(log_path, f"Advertencia: {unmatched_count} filas en el archivo de adaptación tenían valores vacíos de Master TAG o IO") + + log_message(log_path, f"Tags encontrados en el archivo de adaptación: {len(update_dict)}") + + # Inicializar contador para direcciones %M + memory_byte_counter = 3600 + memory_bit_counter = 0 + + # Contador de coincidencias + matched_tags = [] + unmatched_adaptation_tags = set(update_dict.keys()) + converted_to_memory = [] + + # Procesar cada hoja + for sheet_name in workbook.sheetnames: + sheet = workbook[sheet_name] + log_message(log_path, f"\nProcesando hoja: {sheet_name}") + + # Encontrar la columna "Name" y "Logical Address" + name_col_idx = None + logical_addr_col_idx = None + data_type_col_idx = None + + for col_idx, cell in enumerate(sheet[1], 1): # Asumiendo que la primera fila contiene encabezados + cell_value = str(cell.value).lower() if cell.value else "" + + if "name" in cell_value: + name_col_idx = col_idx + log_message(log_path, f"Columna 'Name' encontrada en posición {col_idx}") + + if "logical address" in cell_value: + logical_addr_col_idx = col_idx + log_message(log_path, f"Columna 'Logical Address' encontrada en posición {col_idx}") + + if "data type" in cell_value: + data_type_col_idx = col_idx + log_message(log_path, f"Columna 'Data Type' encontrada en posición {col_idx}") + + if name_col_idx is None or logical_addr_col_idx is None: + log_message(log_path, f"No se encontraron las columnas necesarias en la hoja {sheet_name}, omitiendo...") + continue + + # Actualizar los valores de "Logical Address" + updates_in_sheet = 0 + memory_address_conversions = 0 + + for row_idx, row in enumerate(sheet.iter_rows(min_row=2), 2): # Comenzando desde la fila 2 + name_cell = row[name_col_idx - 1] # Ajuste para índice base 0 + logical_addr_cell = row[logical_addr_col_idx - 1] # Ajuste para índice base 0 + data_type_cell = row[data_type_col_idx - 1] if data_type_col_idx else None # Puede ser None + + tag_name = str(name_cell.value).strip() if name_cell.value else "" + current_address = str(logical_addr_cell.value).strip() if logical_addr_cell.value else "" + data_type = str(data_type_cell.value).strip().lower() if data_type_cell and data_type_cell.value else "" + + if not tag_name or not current_address: + continue + + # Caso 1: Tag encontrado en el diccionario de adaptación + if tag_name in update_dict: + old_value = logical_addr_cell.value + new_value = update_dict[tag_name] + + logical_addr_cell.value = new_value + updates_in_sheet += 1 + matched_tags.append(tag_name) + + if tag_name in unmatched_adaptation_tags: + unmatched_adaptation_tags.remove(tag_name) + + log_message(log_path, f" Actualizado: {tag_name} | Viejo valor: {old_value} | Nuevo valor: {new_value}") + + # Caso 2: Tag no encontrado en adaptación pero con formato %E, %A, %EW, %AW + elif (current_address.startswith('%E') or + current_address.startswith('%A') or + current_address.startswith('%EW') or + current_address.startswith('%AW')): + + old_value = logical_addr_cell.value + + # Determinar si es booleano o word + is_boolean = ('bool' in data_type) or ('.') in current_address + + if is_boolean: + # Para boolean, usamos formato %M byte.bit + new_value = f"%M{memory_byte_counter}.{memory_bit_counter}" + memory_bit_counter += 1 + + # Si llegamos a bit 8, pasamos al siguiente byte + if memory_bit_counter > 7: + memory_bit_counter = 0 + memory_byte_counter += 1 + else: + # Para word, usamos %MW y aumentamos en incrementos de 2 + new_value = f"%MW{memory_byte_counter}" + memory_byte_counter += 2 + + logical_addr_cell.value = new_value + memory_address_conversions += 1 + converted_to_memory.append(tag_name) + + log_message(log_path, f" Convertido a memoria: {tag_name} | Viejo valor: {old_value} | Nuevo valor: {new_value}") + + log_message(log_path, f"Total de actualizaciones en la hoja {sheet_name}: {updates_in_sheet}") + log_message(log_path, f"Total de conversiones a memoria en la hoja {sheet_name}: {memory_address_conversions}") + + # Guardar cambios + try: + workbook.save(output_path) + log_message(log_path, f"\nArchivo Excel actualizado guardado: {output_path}") + except Exception as e: + log_message(log_path, f"Error al guardar el archivo Excel: {e}") + return False + + # Mostrar resumen + unique_matched_tags = set(matched_tags) + log_message(log_path, "\n" + "=" * 30 + " RESUMEN " + "=" * 30) + log_message(log_path, f"Total de tags en archivo de adaptación: {len(update_dict)}") + log_message(log_path, f"Total de tags actualizados (coincidencias): {len(unique_matched_tags)}") + log_message(log_path, f"Total de tags convertidos a memoria: {len(converted_to_memory)}") + + # Mostrar tags del archivo de adaptación sin coincidencias + if unmatched_adaptation_tags: + log_message(log_path, f"\nTags sin coincidencias ({len(unmatched_adaptation_tags)}):") + for tag in sorted(unmatched_adaptation_tags): + log_message(log_path, f" - {tag} -> {update_dict[tag]}") + + return True + +if __name__ == "__main__": + # Rutas de archivos predeterminadas + adaptation_table = r"C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\SAE196 - IO Adapted.md" + tag_from_master_table = r"C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\PLCTags.xlsx" + output_table = r"C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\TAGsIO\PLCTags_Updated.xlsx" # Crear un nuevo archivo para no sobrescribir el original + log_path = r"update_log.txt" # Ruta para el archivo de log + + # Permitir pasar rutas como argumentos desde la línea de comandos + if len(sys.argv) > 1: + tag_from_master_table = sys.argv[1] + if len(sys.argv) > 2: + adaptation_table = sys.argv[2] + if len(sys.argv) > 3: + output_table = sys.argv[3] + if len(sys.argv) > 4: + log_path = sys.argv[4] + + # Ejecutar la actualización + result = update_excel_with_adaptation(tag_from_master_table, adaptation_table, output_table, log_path) + + if result: + print("\nProceso completado exitosamente.") + print(f"Se ha generado un archivo log en: {log_path}") + else: + print("\nHubo errores durante el proceso.") + print(f"Consulte el archivo log para más detalles: {log_path}") diff --git a/backend/script_groups/IO_adaptation/x1_agregatetags_to_md.py b/backend/script_groups/IO_adaptation/x1_agregatetags_to_md.py new file mode 100644 index 0000000..d0b6861 --- /dev/null +++ b/backend/script_groups/IO_adaptation/x1_agregatetags_to_md.py @@ -0,0 +1,193 @@ +""" +convert Excel Tags to md : This script converts Excel files containing tags into Markdown tables. +""" + +# Standard library imports +import os +import sys +import glob + +# Third-party imports +try: + import pandas as pd +except ImportError: + print( + "Error: La librería 'pandas' no está instalada. Por favor, instálala con 'pip install pandas openpyxl'." + ) + sys.exit(1) + +# Determine script_root and add to sys.path for custom module import +# This structure assumes x1_agregatetags_to_md.py is located at: +# d:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x1_agregatetags_to_md.py +# leading to script_root = d:\Proyectos\Scripts\ParamManagerScripts +try: + current_script_path = os.path.abspath(__file__) + script_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_script_path))) + ) + if script_root not in sys.path: + sys.path.append(script_root) + + from backend.script_utils import load_configuration + +except ImportError: + print( + "Error: No se pudo importar 'load_configuration' desde 'backend.script_utils'." + ) + sys.exit(1) +except NameError: # __file__ is not defined + print( + "Error: __file__ no está definido. Este script podría no estar ejecutándose en un entorno Python estándar." + ) + sys.exit(1) + + +def convert_excel_to_markdown_tables(): + """ + Busca archivos Excel en el working_directory, los convierte a tablas Markdown + y los consolida en un único archivo .md. + """ + try: + configs = load_configuration() + working_directory = configs.get("working_directory") + if not working_directory: + print("Error: 'working_directory' no se encontró en la configuración.") + return + if not os.path.isdir(working_directory): + print( + f"Error: El directorio de trabajo '{working_directory}' no existe o no es un directorio." + ) + return + except Exception as e: + print(f"Error al cargar la configuración: {e}") + return + + working_directory_abs = os.path.abspath(working_directory) + print(f"Usando directorio de trabajo: {working_directory_abs}") + + excel_files = glob.glob(os.path.join(working_directory_abs, "*.xlsx")) + + if not excel_files: + print(f"No se encontraron archivos Excel (.xlsx) en {working_directory_abs}.") + return + + output_md_filename = "IO Tags consolidated.md" + output_md_filepath_abs = os.path.join(working_directory_abs, output_md_filename) + + markdown_content = [] + + # Definición de las columnas y sus anchos para el formato Markdown (basado en el ejemplo) + # Nombres de las columnas en la tabla Markdown + md_header_names = ["Master Tag", "Type", "Data Type", "Description"] + # Anchos de las celdas de contenido (espacio disponible para el texto) + col_widths = {"Master Tag": 32, "Type": 6, "Data Type": 9, "Description": 71} + + # Crear el encabezado y separador Markdown una vez, ya que se repite para cada tabla + header_parts = [f" {name:<{col_widths[name]}} " for name in md_header_names] + markdown_table_header = f"|{'|'.join(header_parts)}|" + + separator_parts = [f" {'-'*col_widths[name]} " for name in md_header_names] + markdown_table_separator = f"|{'|'.join(separator_parts)}|" + + for excel_file_path in excel_files: + excel_filename = os.path.basename(excel_file_path) + print(f"Procesando archivo Excel: {excel_filename}...") + + # Determinar si el nombre del archivo sugiere un tipo específico ("Input" o "Output") + filename_suggested_type = None + if "input" in excel_filename.lower(): + filename_suggested_type = "Input" + elif ( + "output" in excel_filename.lower() + ): # elif para dar precedencia a "input" si ambos estuvieran + filename_suggested_type = "Output" + + try: + table_title = os.path.splitext(excel_filename)[0] + markdown_content.append(f"## {table_title}\n") + + markdown_content.append(markdown_table_header) + markdown_content.append(markdown_table_separator) + + df = pd.read_excel(excel_file_path, sheet_name=0) + + # Columnas esperadas en el archivo Excel + excel_col_master_tag = "Name" + excel_col_original_type = "Path" # Columna del Excel que originalmente contiene el tipo (ej. "Inputs") + excel_col_data_type = "Data Type" + excel_col_comment = "Comment" # Columna para la descripción + + excel_required_cols = [ + excel_col_master_tag, + excel_col_original_type, # Requerida como fuente base o fallback para el tipo + excel_col_data_type, + excel_col_comment, + ] + missing_cols = [col for col in excel_required_cols if col not in df.columns] + if missing_cols: + msg = f"*Archivo '{excel_filename}' omitido debido a columnas faltantes: {', '.join(missing_cols)}*\n" + print(f"Advertencia: {msg.strip()}") + markdown_content.append(msg) + continue + + for _, row in df.iterrows(): + master_tag = str(row.get(excel_col_master_tag, "")) + + # Determinar el valor final para la columna 'Type' en Markdown + # Por defecto, tomar de la columna 'Path' (excel_col_original_type) del Excel + tag_type_for_md = str(row.get(excel_col_original_type, "N/A")) + + # Si el nombre del archivo sugiere un tipo ("Input" o "Output"), este tiene precedencia + if filename_suggested_type: + tag_type_for_md = filename_suggested_type + + data_type = str(row.get(excel_col_data_type, "")) + comment_text = str(row.get(excel_col_comment, "")) + description = ( + f'"{comment_text}"' # Descripción tomada de la columna "Comment" + ) + + master_tag_cell = f"{master_tag:<{col_widths['Master Tag']}.{col_widths['Master Tag']}}" + type_cell = ( + f"{tag_type_for_md:<{col_widths['Type']}.{col_widths['Type']}}" + ) + data_type_cell = ( + f"{data_type:<{col_widths['Data Type']}.{col_widths['Data Type']}}" + ) + description_cell = f"{description:<{col_widths['Description']}.{col_widths['Description']}}" + + md_row = f"| {master_tag_cell} | {type_cell} | {data_type_cell} | {description_cell} |" + markdown_content.append(md_row) + + markdown_content.append("\n") # Espacio después de cada tabla + + except FileNotFoundError: + msg = f"*Error procesando '{excel_filename}': Archivo no encontrado.*\n" + print(f"Error: {msg.strip()}") + markdown_content.append(msg) + except pd.errors.EmptyDataError: + msg = f"*Archivo '{excel_filename}' omitido por estar vacío.*\n" + print(f"Advertencia: {msg.strip()}") + markdown_content.append(msg) + except Exception as e: + msg = f"*Error procesando '{excel_filename}': {e}*\n" + print(f"Error: {msg.strip()}") + markdown_content.append(msg) + + if markdown_content: + try: + with open(output_md_filepath_abs, "w", encoding="utf-8") as f: + f.write("\n".join(markdown_content)) + print( + f"¡Éxito! Archivos Excel convertidos a Markdown en: {output_md_filepath_abs}" + ) + except IOError as e: + print( + f"Error al escribir el archivo Markdown '{output_md_filepath_abs}': {e}" + ) + else: + print("No se generó contenido para el archivo Markdown.") + + +if __name__ == "__main__": + convert_excel_to_markdown_tables() diff --git a/backend/script_groups/IO_adaptation/x2.py b/backend/script_groups/IO_adaptation/x2.py new file mode 100644 index 0000000..0eb22bf --- /dev/null +++ b/backend/script_groups/IO_adaptation/x2.py @@ -0,0 +1,511 @@ +""" +convert Markdown tables from adapted IO to Excel for import into TIA Portal +""" + +import pandas as pd +import openpyxl +import re +import os +import sys +import tkinter as tk +from tkinter import filedialog, messagebox +from datetime import datetime + +# Determine script_root and add to sys.path for custom module import +try: + current_script_path = os.path.abspath(__file__) + script_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_script_path))) + ) + if script_root not in sys.path: + sys.path.append(script_root) + + from backend.script_utils import load_configuration + +except ImportError: + print( + "Error: No se pudo importar 'load_configuration' desde 'backend.script_utils'." + ) + sys.exit(1) +except NameError: # __file__ is not defined + print( + "Error: __file__ no está definido. Este script podría no estar ejecutándose en un entorno Python estándar." + ) + sys.exit(1) + + +def read_markdown_table(file_path): + """Leer tabla en formato Markdown y convertirla a DataFrame.""" + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + # Dividir el contenido en líneas + lines = content.strip().split('\n') + + # Encontrar el inicio de la tabla (primera línea que comienza con '|') + table_start = None + for i, line in enumerate(lines): + if line.strip().startswith('|'): + table_start = i + break + + if table_start is None: + print("No se encontró ninguna tabla en el archivo") + return pd.DataFrame() + + # Encontrar todas las líneas de la tabla + table_lines = [] + for i in range(table_start, len(lines)): + line = lines[i].strip() + if line.startswith('|'): + table_lines.append(line) + elif not line: # Línea vacía podría indicar el final de la tabla + if i + 1 < len(lines) and not lines[i + 1].strip().startswith('|'): + break + else: + break # Si no comienza con '|' y no está vacía, es el final de la tabla + + if len(table_lines) < 3: # Necesitamos al menos encabezado, separador y una fila de datos + print("La tabla no tiene suficientes filas") + return pd.DataFrame() + + # Procesar encabezados + header_line = table_lines[0] + separator_line = table_lines[1] + + # Verificar que la segunda línea sea realmente un separador + is_separator = all(cell.strip().startswith(':') or cell.strip().startswith('-') + for cell in separator_line.split('|')[1:-1] if cell.strip()) + + if not is_separator: + print("Advertencia: La segunda línea no parece ser un separador. Se asume que es parte de los datos.") + separator_idx = None + else: + separator_idx = 1 + + # Extraer encabezados + header_cells = header_line.split('|') + # Eliminar elementos vacíos al principio y al final + if not header_cells[0].strip(): + header_cells = header_cells[1:] + if not header_cells[-1].strip(): + header_cells = header_cells[:-1] + + headers = [h.strip() for h in header_cells] + print(f"Encabezados detectados: {headers}") + + # Procesar filas de datos + data_start_idx = 2 if separator_idx == 1 else 1 + data = [] + + for line in table_lines[data_start_idx:]: + # Dividir la línea por el carácter pipe + cells = line.split('|') + + # Eliminar elementos vacíos al principio y al final + if not cells[0].strip(): + cells = cells[1:] + if not cells[-1].strip(): + cells = cells[:-1] + + # Limpiar valores + row_values = [cell.strip() for cell in cells] + + # Asegurar que la fila tenga el mismo número de columnas que los encabezados + if len(row_values) != len(headers): + print(f"Advertencia: Fila con {len(row_values)} valores, pero se esperaban {len(headers)}. Ajustando...") + + # Intentar ajustar la fila para que coincida con el número de columnas + if len(row_values) < len(headers): + row_values.extend([''] * (len(headers) - len(row_values))) + else: + row_values = row_values[:len(headers)] + + data.append(row_values) + + # Convertir a DataFrame + df = pd.DataFrame(data, columns=headers) + + return df + +def create_log_file(output_dir): + """Crear un archivo de log con timestamp.""" + log_filename = f"update_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + log_path = os.path.join(output_dir, log_filename) + + with open(log_path, 'w', encoding='utf-8') as log_file: + log_file.write(f"Log de actualización de PLCTags - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + log_file.write("=" * 80 + "\n\n") + + return log_path + +def log_message(log_path, message): + """Añadir mensaje al log.""" + with open(log_path, 'a', encoding='utf-8') as log_file: + log_file.write(message + "\n") + print(message) + +def transform_io_address(address): + """ + Transform IO addresses according to the required format: + - Ixx.x → %Exx.x + - Exx.x → %Exx.x + - Qxx.x → %Axx.x + - Axx.x → %Axx.x + - PEWxx → %EWxx + - PAWxx → %AWxx + - EW xx..xx → %EWxx (ranges for Profibus) + - AW xx..xx → %AWxx (ranges for Profibus) + """ + if not address or not isinstance(address, str): + return address + + address = address.strip() + + # Handle Profibus ranges (extract the first number before the range) + profibus_match = re.match(r'^(EW|AW)\s+(\d+)\.\..*$', address) + if profibus_match: + prefix, number = profibus_match.groups() + if prefix == 'EW': + return f"%EW{number}" + elif prefix == 'AW': + return f"%AW{number}" + + # Patterns for boolean addresses + if re.match(r'^I(\d+)\.(\d+)$', address): + return re.sub(r'^I(\d+)\.(\d+)$', r'%E\1.\2', address) + elif re.match(r'^E(\d+)\.(\d+)$', address): + return re.sub(r'^E(\d+)\.(\d+)$', r'%E\1.\2', address) + elif re.match(r'^Q(\d+)\.(\d+)$', address): + return re.sub(r'^Q(\d+)\.(\d+)$', r'%A\1.\2', address) + elif re.match(r'^A(\d+)\.(\d+)$', address): + return re.sub(r'^A(\d+)\.(\d+)$', r'%A\1.\2', address) + + # Patterns for word addresses + elif re.match(r'^PEW(\d+)$', address): + return re.sub(r'^PEW(\d+)$', r'%EW\1', address) + elif re.match(r'^PAW(\d+)$', address): + return re.sub(r'^PAW(\d+)$', r'%AW\1', address) + + # If already in correct format or unknown format, return as is + return address + +def is_input_tag(tag_name): + """Determinar si un tag es de entrada basado en su nombre.""" + input_prefixes = ['DI_', 'AI_', 'P_AI_', 'P_FT', 'P_CT', 'P_PT', 'P_TT', 'P_g', 'P_PDS_'] + for prefix in input_prefixes: + if tag_name.startswith(prefix): + # Excepciones para P_g que pueden ser outputs + if tag_name.startswith('P_g') and ('_VFC_ControlWord' in tag_name or '_Refvalue' in tag_name): + return False + return True + return False + +def is_output_tag(tag_name): + """Determinar si un tag es de salida basado en su nombre.""" + output_prefixes = ['DO_', 'AO_', 'P_AO_', 'P_g', 'MaselliHold', 'MaselliSpare'] + for prefix in output_prefixes: + if tag_name.startswith(prefix): + return True + # Si comienza con P_g, revisar si tiene '_VFC_ControlWord' o 'Refvalue' que son outputs + if tag_name.startswith('P_g') and ('_VFC_ControlWord' in tag_name or '_VFC_Refvalue' in tag_name): + return True + # Si comienza con P_PDS_, revisar si son outputs específicos + if tag_name.startswith('P_PDS_') and ('_Recipe_Number' in tag_name or '_Freeze_To_PDS' in tag_name or '_Stop_to_PDS' in tag_name): + return True + return False + +def is_bit_type(data_type): + """Determinar si el tipo de dato es un bit (Bool).""" + return data_type.lower() == 'bool' + +def update_plc_tags(excel_path, md_path, output_path, log_path): + """ + Actualiza el archivo Excel con la información del archivo Markdown. + + Args: + excel_path: Ruta al archivo Excel exportado de TIA Portal + md_path: Ruta al archivo Markdown con la adaptación IO + output_path: Ruta para guardar el Excel actualizado + log_path: Ruta para el archivo de log + """ + log_message(log_path, f"Iniciando proceso de actualización") + log_message(log_path, f"Archivo Excel de entrada: {excel_path}") + log_message(log_path, f"Archivo Markdown de entrada: {md_path}") + log_message(log_path, f"Archivo Excel de salida: {output_path}") + log_message(log_path, "-" * 80) + + # Leer el archivo Markdown + md_df = read_markdown_table(md_path) + + # Identificar las columnas relevantes en el archivo Markdown + io_col = None + master_tag_col = None + + for col in md_df.columns: + col_lower = col.lower() + if col_lower == 'io' or 'address' in col_lower: + io_col = col + elif 'master' in col_lower and 'tag' in col_lower: + master_tag_col = col + + if not io_col or not master_tag_col: + log_message(log_path, "ERROR: No se pudieron identificar las columnas necesarias en el archivo Markdown") + return False + + log_message(log_path, f"Columna IO: {io_col}") + log_message(log_path, f"Columna Master Tag: {master_tag_col}") + + # Crear un diccionario de mapeo IO desde el Markdown + io_mapping = {} + for _, row in md_df.iterrows(): + master_tag = str(row[master_tag_col]).strip() + io_value = str(row[io_col]).strip() + + if master_tag and io_value and master_tag != 'nan' and io_value != 'nan': + io_mapping[master_tag] = transform_io_address(io_value) + + log_message(log_path, f"Tags mapeados en el archivo Markdown: {len(io_mapping)}") + + # Cargar el archivo Excel + try: + # Usar openpyxl para mantener la estructura del Excel + workbook = openpyxl.load_workbook(excel_path) + log_message(log_path, f"Archivo Excel cargado: {excel_path}") + log_message(log_path, f"Hojas disponibles: {workbook.sheetnames}") + except Exception as e: + log_message(log_path, f"ERROR: No se pudo cargar el archivo Excel: {e}") + return False + + # Inicializar contadores para direcciones de memoria + input_mem_byte = 3600 + input_mem_bit = 0 + output_mem_byte = 3800 + output_mem_bit = 0 + + # Estadísticas + total_tags = 0 + updated_tags = 0 + relocated_to_inputs = 0 + relocated_to_outputs = 0 + relocated_to_not_in_hardware_inputs = 0 + relocated_to_not_in_hardware_outputs = 0 + assigned_memory_addresses = 0 + + # Procesamos la hoja principal (asumimos que es la primera) + if len(workbook.sheetnames) > 0: + sheet = workbook[workbook.sheetnames[0]] + + # Encontrar las columnas relevantes + name_col = None + path_col = None + data_type_col = None + logical_address_col = None + + for col_idx, cell in enumerate(sheet[1], 1): + cell_value = str(cell.value).strip() if cell.value else "" + if cell_value.lower() == "name": + name_col = col_idx + elif cell_value.lower() == "path": + path_col = col_idx + elif cell_value.lower() == "data type": + data_type_col = col_idx + elif cell_value.lower() == "logical address": + logical_address_col = col_idx + + if not all([name_col, path_col, data_type_col, logical_address_col]): + log_message(log_path, "ERROR: No se encontraron todas las columnas necesarias en el Excel") + return False + + # Convertir a índices base 0 para openpyxl + name_col -= 1 + path_col -= 1 + data_type_col -= 1 + logical_address_col -= 1 + + # Recorrer todas las filas (excluyendo la primera que es el encabezado) + for row_idx, row in enumerate(sheet.iter_rows(min_row=2), 2): + name_cell = row[name_col] + path_cell = row[path_col] + data_type_cell = row[data_type_col] + logical_address_cell = row[logical_address_col] + + tag_name = str(name_cell.value).strip() if name_cell.value else "" + path = str(path_cell.value).strip() if path_cell.value else "" + data_type = str(data_type_cell.value).strip() if data_type_cell.value else "" + + # Verificar si el tag debe ser procesado (está en los paths relevantes) + relevant_paths = [ + "Inputs", + "Outputs", + "IO Not in Hardware\\InputsMaster", + "IO Not in Hardware\\OutputsMaster" + ] + + if path in relevant_paths: + total_tags += 1 + + # Verificar si el tag está en el mapeo de IO + if tag_name in io_mapping: + old_address = logical_address_cell.value + new_address = io_mapping[tag_name] + logical_address_cell.value = new_address + + # Actualizar el path según el tipo de señal + if new_address.startswith("%E"): + path_cell.value = "Inputs" + relocated_to_inputs += 1 + elif new_address.startswith("%A"): + path_cell.value = "Outputs" + relocated_to_outputs += 1 + + updated_tags += 1 + log_message(log_path, f"Actualizado: {tag_name} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}") + + # Si no está en el mapeo, asignar dirección de memoria según el tipo + else: + is_input = is_input_tag(tag_name) + is_output = is_output_tag(tag_name) + is_bit = is_bit_type(data_type) + + # Para entradas + if is_input and not is_output: + path_cell.value = "IO Not in Hardware\\InputsMaster" + relocated_to_not_in_hardware_inputs += 1 + + if is_bit: + new_address = f"%M{input_mem_byte}.{input_mem_bit}" + input_mem_bit += 1 + if input_mem_bit > 7: + input_mem_bit = 0 + input_mem_byte += 1 + else: + new_address = f"%MW{input_mem_byte}" + input_mem_byte += 2 + + # Para salidas + elif is_output: + path_cell.value = "IO Not in Hardware\\OutputsMaster" + relocated_to_not_in_hardware_outputs += 1 + + if is_bit: + new_address = f"%M{output_mem_byte}.{output_mem_bit}" + output_mem_bit += 1 + if output_mem_bit > 7: + output_mem_bit = 0 + output_mem_byte += 1 + else: + new_address = f"%MW{output_mem_byte}" + output_mem_byte += 2 + + # Si no podemos determinar si es entrada o salida por el nombre + # Lo determinamos por el path actual + else: + if "Input" in path: + path_cell.value = "IO Not in Hardware\\InputsMaster" + relocated_to_not_in_hardware_inputs += 1 + + if is_bit: + new_address = f"%M{input_mem_byte}.{input_mem_bit}" + input_mem_bit += 1 + if input_mem_bit > 7: + input_mem_bit = 0 + input_mem_byte += 1 + else: + new_address = f"%MW{input_mem_byte}" + input_mem_byte += 2 + else: + path_cell.value = "IO Not in Hardware\\OutputsMaster" + relocated_to_not_in_hardware_outputs += 1 + + if is_bit: + new_address = f"%M{output_mem_byte}.{output_mem_bit}" + output_mem_bit += 1 + if output_mem_bit > 7: + output_mem_bit = 0 + output_mem_byte += 1 + else: + new_address = f"%MW{output_mem_byte}" + output_mem_byte += 2 + + old_address = logical_address_cell.value + logical_address_cell.value = new_address + assigned_memory_addresses += 1 + + log_message(log_path, f"Asignación memoria: {tag_name} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}") + + # Guardar el archivo actualizado + try: + workbook.save(output_path) + log_message(log_path, f"Archivo Excel guardado: {output_path}") + except Exception as e: + log_message(log_path, f"ERROR: No se pudo guardar el archivo Excel: {e}") + return False + + # Mostrar estadísticas + log_message(log_path, "\n" + "=" * 30 + " RESUMEN " + "=" * 30) + log_message(log_path, f"Total de tags procesados: {total_tags}") + log_message(log_path, f"Tags actualizados desde el Markdown: {updated_tags}") + log_message(log_path, f"Tags relocalizados a Inputs: {relocated_to_inputs}") + log_message(log_path, f"Tags relocalizados a Outputs: {relocated_to_outputs}") + log_message(log_path, f"Tags relocalizados a InputsMaster: {relocated_to_not_in_hardware_inputs}") + log_message(log_path, f"Tags relocalizados a OutputsMaster: {relocated_to_not_in_hardware_outputs}") + log_message(log_path, f"Tags con direcciones de memoria asignadas: {assigned_memory_addresses}") + + return True + +def main(): + configs = load_configuration() + working_directory = configs.get("working_directory") + + # Crear interfaz para seleccionar archivos + root = tk.Tk() + root.withdraw() # Ocultar ventana principal + + # Pedir al usuario que seleccione los archivos + print("Seleccione el archivo Excel exportado de TIA Portal:") + excel_path = filedialog.askopenfilename( + title="Seleccione el archivo Excel exportado de TIA Portal", + filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")] + ) + + if not excel_path: + print("No se seleccionó ningún archivo Excel. Saliendo...") + return + + print("Seleccione el archivo Markdown con la adaptación IO:") + md_path = filedialog.askopenfilename( + title="Seleccione el archivo Markdown con la adaptación IO", + filetypes=[("Markdown files", "*.md"), ("All files", "*.*")] + ) + + if not md_path: + print("No se seleccionó ningún archivo Markdown. Saliendo...") + return + + # Determinar la ruta de salida (mismo directorio que el Excel, pero con "_Updated" añadido) + excel_dir = os.path.dirname(excel_path) + excel_filename = os.path.basename(excel_path) + excel_name, excel_ext = os.path.splitext(excel_filename) + + output_filename = f"{excel_name}_Updated{excel_ext}" + output_path = os.path.join(excel_dir, output_filename) + + # Crear archivo de log + log_path = create_log_file(excel_dir) + + # Ejecutar el proceso de actualización + success = update_plc_tags(excel_path, md_path, output_path, log_path) + + if success: + messagebox.showinfo("Proceso completado", + f"La actualización se ha completado con éxito.\n\n" + f"Archivo de salida: {output_path}\n\n" + f"Archivo de log: {log_path}") + else: + messagebox.showerror("Error", + f"Hubo un error durante el proceso.\n\n" + f"Consulte el archivo de log para más detalles: {log_path}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.csv b/backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.csv new file mode 100644 index 0000000..53fe9e0 --- /dev/null +++ b/backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.csv @@ -0,0 +1,76 @@ +Name;Path;Data Type;Logical Address;Comment;Hmi Visible;Hmi Accessible;Hmi Writeable;Typeobject ID;Version ID +DI_Emergency_Pilz_On;Inputs;Bool;%E0.5;Pilz Emergency;True;True;True;; +DI_LSN301L;Inputs;Bool;%E0.6;LSN301_L - Deaireator Tank Minimun Level;True;True;True;; +DI_LSM302L;Inputs;Bool;%E1.0;LSM302_L - Product Tank Minimun Level;True;True;True;; +DI_PPN301_SoftStart_Ovrld;Inputs;Bool;%E10.0;PPN301 - Water_Pump_SoftStart_Ovrld;True;True;True;; +DI_UPSBatteryReady;Inputs;Bool;%E3.7;UPS Battery ready;True;True;True;; +DI_RMM301_Closed;Inputs;Bool;%E1.5;RMM301 - Feedback OFF (VM1WATER);True;True;True;; +DI_RMP302_Closed;Inputs;Bool;%E1.6;RMP302 - Feedback OFF (VM2 SYRUP);True;True;True;; +DI_RMM303_Closed;Inputs;Bool;%E1.7;RMM303 - Feedback OFF (VM3 CO2);True;True;True;; +DI_PPN301_Contactor;Inputs;Bool;%E11.0;PPN301 - Deaireator Pump Feedback;True;True;True;; +DI_PPP302_Ovrld;Inputs;Bool;%E2.2;PPP302 - Syrup Pump Overload;True;True;True;; +DI_PPP302_Contactor;Inputs;Bool;%E2.3;PPP302 - Syrup Pump Feedback;True;True;True;; +DI_PPM303_Ovrld;Inputs;Bool;%E2.4;PPM303 - Product Pump Overload;True;True;True;; +DI_PPM306_Contactor;Inputs;Bool;%E11.3;PPM306 - Recirculating Pump Feedback;True;True;True;; +DI_SyrRoom_SyrPump_Running;Inputs;Bool;%E5.0;From Syrup Room - Syrup Pump Running;True;True;True;; +DI_SyrRoom_WatPumpReady;Inputs;Bool;%E68.1;From Syrup Room - Water Pump Ready;True;True;True;; +DI_CIP_CIP_Rinse;Inputs;Bool;%E60.1;From CIP Running ;True;True;True;; +DI_CIP_Drain;Inputs;Bool;%E60.2;From CIP Drain;True;True;True;; +DI_Air_InletPress_OK;Inputs;Bool;%E7.1;Air Pressure Switch;True;True;True;; +P_AI_LTM302;Inputs;Word;%EW100;LTM302 - Product Tank Level;True;True;True;; +P_AI_PTM304;Inputs;Word;%EW102;PTM304 - Product Tank Pressure;True;True;True;; +P_AI_LTP303;Inputs;Word;%EW808;LTP303 - Syrup Tank Level;True;True;True;; +P_AI_TTN321;Inputs;Word;%EW112;TTN321 - Deaireator Temperature;True;True;True;; +P_AI_PTF203;Inputs;Word;%EW810;PTF203 - Differential Pressure;True;True;True;; +DI_CIP_CIP_Enable;Inputs;Bool;%E60.0;From CIP Enable;True;True;True;; +DI_AVM362_Open;Inputs;Bool;%E102.3;AVM362 - Feedback ON;True;True;True;; +DI_AVM362_Close;Inputs;Bool;%E112.3;AVM362 - Feedback OFF;True;True;True;; +DI_AVM346_Open;Inputs;Bool;%E102.2;AVM346 - Feedback ON;True;True;True;; +DI_AVM346_Close;Inputs;Bool;%E112.2;AVM346 - Feedback OFF;True;True;True;; +DI_UPSAlarm;Inputs;Bool;%E3.5;UPS Alarm;True;True;True;; +DI_UPSsupply;Inputs;Bool;%E3.6;UPS supply OK;True;True;True;; +DI_Emergency_Pressed;Inputs;Bool;%E4.3;Electrical Panel Emergency Button;True;True;True;; +P_AI_PTP338;Inputs;Word;%EW816;PTP338 - Syrup Inlet Pressure;True;True;True;; +P_FTM303_Density;Inputs;Real;%ED3215;MIX - Profibus Variables;True;True;True;; +P_FTM303_Density_State;Inputs;Byte;%EB3219;MIX - Profibus Variables;True;True;True;; +P_FTM303_Flow;Inputs;Real;%ED3200;MIX - Profibus Variables;True;True;True;; +P_FTM303_Flow_State;Inputs;Byte;%EB3204;MIX - Profibus Variables;True;True;True;; +P_FTM303_Temperature;Inputs;Real;%ED3225;MIX - Profibus Variables;True;True;True;; +P_FTM303_Temperature_State;Inputs;Byte;%EB3229;MIX - Profibus Variables;True;True;True;; +P_FTM303_Totalizer;Inputs;Real;%ED3240;MIX - Profibus Variables;True;True;True;; +P_FTM303_Totalizer_State;Inputs;Byte;%EB3244;MIX - Profibus Variables;True;True;True;; +P_FTN301_Flow;Inputs;Real;%ED3080;MIX - Profibus Variables;True;True;True;; +P_FTN301_Flow_State;Inputs;Byte;%EB3084;MIX - Profibus Variables;True;True;True;; +P_FTN301_Totaliz_State;Inputs;Byte;%EB3104;MIX - Profibus Variables;True;True;True;; +P_FTN301_Totalizer;Inputs;Real;%ED3100;MIX - Profibus Variables;True;True;True;; +P_FTP302_Brix;Inputs;Real;%ED2050;MIX - Profibus Variables;True;True;True;; +P_FTP302_Brix_State;Inputs;Byte;%EB2054;MIX - Profibus Variables;True;True;True;; +P_FTP302_Density;Inputs;Real;%ED2045;MIX - Profibus Variables;True;True;True;; +P_FTP302_Density_State;Inputs;Byte;%EB2049;MIX - Profibus Variables;True;True;True;; +P_FTP302_Flow;Inputs;Real;%ED2030;MIX - Profibus Variables;True;True;True;; +P_FTP302_Flow_State;Inputs;Byte;%EB2034;MIX - Profibus Variables;True;True;True;; +P_FTP302_Temp;Inputs;Real;%ED2055;MIX - Profibus Variables;True;True;True;; +P_FTP302_Temp_State;Inputs;Byte;%EB2059;MIX - Profibus Variables;True;True;True;; +P_FTP302_Totaliz_State;Inputs;Byte;%EB2074;MIX - Profibus Variables;True;True;True;; +P_FTP302_Totalizer;Inputs;Real;%ED2070;MIX - Profibus Variables;True;True;True;; +DI_PPM306_Ovrld;Inputs;Bool;%E10.3;PPM306 - Recirculating Pump Overload;True;True;True;; +DI_CIP_CleaningCompleted;Inputs;Bool;%E60.3;CIP - Cip Cleaning Completed;True;True;True;; +P_AI_TTM306;Inputs;Word;%EW108;TTM306 - Chiller Temperature;True;True;True;; +P_AI_RVN304;Inputs;Word;%EW104;RVN304 - Deaireation Valve;True;True;True;; +P_AI_PCM306;Inputs;Word;%EW106;PCM306 - Gas Pressure Injection;True;True;True;; +P_AI_ProductCO2;Inputs;Word;%EW826;Product Analizer - Product CO2;True;True;True;; +P_gPPM303_VFC_StatusWord;Inputs;Word;%EW1640;MIX - Product Pump - Profibus Variables;True;True;True;; +P_PDS_CO2;Inputs;Real;%ED15060;;True;True;True;; +P_PDS_Product_Brix;Inputs;Real;%ED15084;;True;True;True;; +P_PDS_Temperature;Inputs;Real;%ED15104;;True;True;True;; +P_PDS_Density;Inputs;Real;%ED15112;;True;True;True;; +DI_HVP301_Sensor;Inputs;Bool;%E7.2;GCP301 - Manual Syrup Valve Closed (NO);True;True;True;; +DI_PB_HornReset;Inputs;Bool;%E0.1;PB Horn Reset;True;True;True;; +DI_PB_Machine_Start;Inputs;Bool;%E0.4;PB Machine Start;True;True;True;; +DI_PB_Machine_Stop;Inputs;Bool;%E0.3;PB Machine Stop;True;True;True;; +DI_PPN301_Ovrld;Inputs;Bool;%E2.0;PPN301 - Deaireator Pump Overload;True;True;True;; +DI_AuxVoltage_On;Inputs;Bool;%E0.0;Electrical Panel Restored;True;True;True;; +DI_AlarmReset;Inputs;Bool;%E0.2;PB Machine Reset;True;True;True;; +P_AI_RVM301;Inputs;Word;%EW114;RVM301 - Product Tank Pressure Valve;True;True;True;; +DI_Min_Syrup_Level;Inputs;Bool;%E0.7; - Syrup Tank Minimun Level;True;True;True;; +DI_FSS301;Inputs;Bool;%E7.3;FSS301 - Local Cip Return Flow Switch;True;True;True;; diff --git a/backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.xlsx b/backend/script_groups/ObtainIOFromProjectTia/Inputs PLCTags.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..63139e2a2f7e8d4bce6b5f8059e239fcd8d1d79d GIT binary patch literal 8115 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/ ├── _CAx_Export.aml diff --git a/backend/script_groups/ObtainIOFromProjectTia/x3.py b/backend/script_groups/ObtainIOFromProjectTia/x3.py index 7a337a4..6d6c81a 100644 --- a/backend/script_groups/ObtainIOFromProjectTia/x3.py +++ b/backend/script_groups/ObtainIOFromProjectTia/x3.py @@ -441,6 +441,9 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id): if plc_info: plc_name_for_title = plc_info.get('name', target_plc_id) + # v31: Initialize list to store all IO data for the summary table for this PLC + all_plc_io_for_table = [] + markdown_lines = [f"# Hardware & IO Summary for PLC: {plc_name_for_title}", ""] if not plc_info: @@ -529,6 +532,9 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id): else: # --- Display Logic with Sibling IO Aggregation & Aesthetics --- for node_id, node_addr in other_device_items: + # v31: Initialize list for table data for the current device being processed + current_device_io_for_table = [] + node_info = project_data.get("devices", {}).get(node_id) if not node_info: markdown_lines.append( @@ -775,6 +781,35 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id): siemens_addr = ( f"FMT_ERROR({start_str},{length_str})" ) + # v31: Collect data for the summary table (Corrected Indentation) + current_device_io_for_table.append({ + "Network": net_info.get('name', net_id), + "Network Type": net_info.get('type', 'Unknown'), + "Device Address": node_addr, + "Device Name": display_name, # Main device name + "Sub-Device": addr_info.get('module_name','?'), # Module name + "Sub-Device OrderNo": addr_info.get('module_order_number', 'N/A'), + "Sub-Device Type": addr_info.get('module_type_name', 'N/A'), + "IO Type": io_type, + "IO Address": siemens_addr, + "Number of Bits": length_bits, + "SortKey": ( # Add a sort key for the table + net_info.get('name', net_id), + sort_key((node_id, node_addr)), # Reuse the device sort key + ( + int(addr_info.get("module_pos", "9999")) + if str(addr_info.get("module_pos", "9999")).isdigit() + else 9999 + ), + addr_info.get("module_name", ""), + io_type, + ( + int(addr_info.get("start", "0")) + if str(addr_info.get("start", "0")).isdigit() + else float("inf") + ), + ) + }) markdown_lines.append( f" - `{siemens_addr}` (Len={length_bits} bits)" @@ -811,13 +846,53 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id): ) for conn in sorted(list(set(io_conns))): markdown_lines.append(f" - {conn}") + # v31: Add collected IO for this device to the main list + all_plc_io_for_table.extend(current_device_io_for_table) markdown_lines.append("") # Spacing # --- *** END Display Logic *** --- + # --- Add IO Summary Table --- # v31: New section + if all_plc_io_for_table: + markdown_lines.append("\n## IO Summary Table") + markdown_lines.append("") + + # Define table headers + headers = [ + "Network", "Type", "Address", "Device Name", "Sub-Device", + "OrderNo", "Type", "IO Type", "IO Address", "Number of Bits" + ] + markdown_lines.append("| " + " | ".join(headers) + " |") + markdown_lines.append("|-" + "-|-".join(["---"] * len(headers)) + "-|") + + # Sort the collected data + sorted_table_data = sorted(all_plc_io_for_table, key=lambda x: x["SortKey"]) + + # Add rows to the table + for row_data in sorted_table_data: + row = [ + row_data.get("Network", "N/A"), + row_data.get("Network Type", "N/A"), + row_data.get("Device Address", "N/A"), + row_data.get("Device Name", "N/A"), + row_data.get("Sub-Device", "N/A"), + row_data.get("Sub-Device OrderNo", "N/A"), + row_data.get("Sub-Device Type", "N/A"), + row_data.get("IO Type", "N/A"), + f"`{row_data.get('IO Address', 'N/A')}`", # Format IO Address as code + row_data.get("Number of Bits", "N/A"), + ] + # Escape pipe characters within cell content if necessary (though unlikely for these fields) + row = [str(cell).replace('|', '\\|') for cell in row] + markdown_lines.append("| " + " | ".join(row) + " |") + + # --- End Add IO Summary Table --- + try: + # Re-open the file in write mode to include the table at the end with open(md_file_path, "w", encoding="utf-8") as f: f.write("\n".join(markdown_lines)) - print(f"\nMarkdown summary written to: {md_file_path}") + print(f"Markdown summary (including table) written to: {md_file_path}") + except Exception as e: print(f"ERROR writing Markdown file {md_file_path}: {e}") traceback.print_exc() @@ -1039,7 +1114,7 @@ if __name__ == "__main__": configs = load_configuration() working_directory = configs.get("working_directory") - script_version = "v30 - Enhanced Module Info in Hardware Tree" + script_version = "v31.1 - Corrected IO Summary Table Initialization" # Updated version print( f"--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter ({script_version}) ---" ) diff --git a/backend/script_groups/XML Parser to SCL/x0_main.py b/backend/script_groups/XML Parser to SCL/x0_main.py index c27f81a..dbb2763 100644 --- a/backend/script_groups/XML Parser to SCL/x0_main.py +++ b/backend/script_groups/XML Parser to SCL/x0_main.py @@ -164,10 +164,10 @@ def check_skip_status( if __name__ == "__main__": configs = load_configuration() working_directory = configs.get("working_directory") - group_config = configs.get("level2", {}) + xml_parser_config = configs.get("level2", {}) # <-- NUEVO: Leer parámetros de configuración para x3, x4, x5 --> - xml_parser_config = configs.get("XML Parser to SCL", {}) + # xml_parser_config = configs.get("XML Parser to SCL", {}) cfg_scl_output_dirname = xml_parser_config.get("scl_output_dir", "scl_output") cfg_xref_output_dirname = xml_parser_config.get("xref_output_dir", "xref_output") cfg_xref_source_subdir = xml_parser_config.get("xref_source_subdir", "source") diff --git a/data/log.txt b/data/log.txt index 48cfd2f..9af660e 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,36 +1,9 @@ -[13:56:30] Iniciando ejecución de x3.py en C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport... -[13:56:30] --- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v30 - Enhanced Module Info in Hardware Tree) --- -[13:56:30] Using Working Directory for Output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport -[13:56:34] Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml -[13:56:34] Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport -[13:56:34] Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json -[13:56:34] Output IO Debug Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md -[13:56:34] Processing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml -[13:56:34] Pass 1: Found 203 InternalElement(s). Populating device dictionary... -[13:56:34] Pass 2: Identifying PLCs and Networks (Refined v2)... -[13:56:34] Identified Network: PROFIBUS_1 (d645659a-3704-4cd6-b2c8-6165ceeed6ee) Type: Profibus -[13:56:34] Identified Network: ETHERNET_1 (f0b1c852-7dc9-4748-888e-34c60b519a75) Type: Ethernet/Profinet -[13:56:34] Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0 -[13:56:34] Pass 3: Processing InternalLinks (Robust Network Mapping & IO)... -[13:56:34] Found 116 InternalLink(s). -[13:56:34] Mapping Device/Node 'E1' (NodeID:439930b8-1bbc-4cb2-a93b-2eed931f4b12, Addr:10.1.33.11) to Network 'ETHERNET_1' -[13:56:34] --> Associating Network 'ETHERNET_1' with PLC 'PLC' (via Node 'E1' Addr: 10.1.33.11) -[13:56:34] Mapping Device/Node 'P1' (NodeID:904bb0f7-df2d-4c1d-ab65-f45480449db1, Addr:1) to Network 'PROFIBUS_1' -[13:56:34] --> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1) -[13:56:34] Mapping Device/Node 'PB1' (NodeID:2784bae8-9807-475f-89bd-bcf44282f5f4, Addr:12) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:e9c5f60a-1da2-4c9b-979e-7d03a5b58a44, Addr:20) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:dd7201c2-e127-4a9d-b6ae-7a74a4ffe418, Addr:21) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:d8825919-3a6c-4f95-aef0-62c782cfdb51, Addr:22) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:27d0e31d-46dc-4fdd-ab82-cfb91899a27c, Addr:10) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:d91d5905-aa1a-485e-b4eb-8333cc2133c2, Addr:8) to Network 'PROFIBUS_1' -[13:56:34] Mapping Device/Node 'PB1' (NodeID:0c5dfe06-786d-4ab6-b57c-8dfede56c2aa, Addr:40) to Network 'PROFIBUS_1' -[13:56:34] Data extraction and structuring complete. -[13:56:34] Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json -[13:56:34] JSON data written successfully. -[13:56:34] IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md -[13:56:34] Found 1 PLC(s). Generating individual hardware trees... -[13:56:34] Generating Hardware Tree for PLC 'PLC' (ID: a48e038f-0bcc-4b48-8373-033da316c62b) at: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md -[13:56:34] Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\Documentation\SAE196_c0.2_CAx_Export_Hardware_Tree.md -[13:56:34] Script finished. -[13:56:34] Ejecución de x3.py finalizada (success). Duración: 0:00:03.887946. -[13:56:34] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\log_x3.txt +[18:14:57] Iniciando ejecución de x5.py en C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport... +[18:14:57] Usando directorio de trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport +[18:14:57] Procesando archivo Excel: Inputs PLCTags.xlsx... +[18:14:58] Procesando archivo Excel: InputsMaster PLCTags.xlsx... +[18:14:58] Procesando archivo Excel: Outputs PLCTags.xlsx... +[18:14:58] Procesando archivo Excel: OutputsMaster PLCTags.xlsx... +[18:14:58] ¡Éxito! Archivos Excel convertidos a Markdown en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\consolidated_excel_tables.md +[18:14:58] Ejecución de x5.py finalizada (success). Duración: 0:00:00.948478. +[18:14:58] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\log_x5.txt