diff --git a/backend/script_groups/TwinCat/TwinCat.code-workspace b/backend/script_groups/TwinCat/TwinCat.code-workspace index eab781f..b3776d7 100644 --- a/backend/script_groups/TwinCat/TwinCat.code-workspace +++ b/backend/script_groups/TwinCat/TwinCat.code-workspace @@ -5,6 +5,9 @@ }, { "path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/Analisis" + }, + { + "path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/ExportTwinCat" } ], "settings": {} diff --git a/backend/script_groups/TwinCat/x1_lad_converter.py b/backend/script_groups/TwinCat/x1_lad_converter.py index 7d04e18..73a514c 100644 --- a/backend/script_groups/TwinCat/x1_lad_converter.py +++ b/backend/script_groups/TwinCat/x1_lad_converter.py @@ -22,296 +22,315 @@ sys.path.append(script_root) # Importar la función de configuración from backend.script_utils import load_configuration + class SymbolManager: """Gestor de símbolos para SymPy""" + def __init__(self): self._symbol_cache = {} - + def get_symbol(self, name): """Obtener o crear símbolo SymPy""" if name not in self._symbol_cache: # Limpiar nombre para SymPy (sin espacios, caracteres especiales) - clean_name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + clean_name = re.sub(r"[^a-zA-Z0-9_]", "_", name) self._symbol_cache[name] = symbols(clean_name) return self._symbol_cache[name] + class SimpleLadConverter: """Convertidor simplificado de LAD a código estructurado""" - + def __init__(self): self.networks = [] self.current_network_id = 0 self.symbol_manager = SymbolManager() self.sympy_expressions = {} # Mapeo de network_id -> sympy expression - + # Nuevas propiedades para estructura SCL completa self.program_name = "" - self.program_type = "PROGRAM" # PROGRAM, FUNCTION, FUNCTION_BLOCK, TYPE, VAR_GLOBAL_LIST + self.program_type = ( + "PROGRAM" # PROGRAM, FUNCTION, FUNCTION_BLOCK, TYPE, VAR_GLOBAL_LIST + ) self.program_path = "" self.var_sections = {} # VAR, VAR_INPUT, VAR_OUTPUT, etc. self.actions = {} # Diccionario de ACTIONs self.st_main_code = None # Código ST del programa principal self.type_content = None # Contenido completo de TYPE - + def parse_file(self, file_path): """Parse el archivo LAD completo incluyendo variables y ACTIONs""" - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() - + # Guardar contenido original para extraer información adicional self._original_content = content - + # Extraer información del header self._parse_header_info(content) - + # Extraer declaraciones de variables self._parse_variable_declarations(content) - + # Verificar si es un TYPE (solo datos) o VAR_GLOBAL_LIST - if hasattr(self, 'program_type') and self.program_type == 'TYPE': + if hasattr(self, "program_type") and self.program_type == "TYPE": print("Archivo TYPE detectado - Extrayendo declaraciones") self._extract_type_content(content) - elif hasattr(self, 'program_type') and self.program_type == 'VAR_GLOBAL_LIST': - print("Archivo de variables globales detectado - Extrayendo todas las secciones") + elif hasattr(self, "program_type") and self.program_type == "VAR_GLOBAL_LIST": + print( + "Archivo de variables globales detectado - Extrayendo todas las secciones" + ) self._extract_global_variables(content) else: # Encontrar sección LAD - lad_start = content.find('_LD_BODY') + lad_start = content.find("_LD_BODY") if lad_start != -1: # Extraer contenido LAD hasta ACTION o END_PROGRAM - action_start = content.find('\nACTION', lad_start) - end_program = content.find('\nEND_PROGRAM', lad_start) - + action_start = content.find("\nACTION", lad_start) + end_program = content.find("\nEND_PROGRAM", lad_start) + lad_end = action_start if action_start != -1 else end_program if lad_end == -1: lad_end = len(content) - + lad_content = content[lad_start:lad_end] - lines = lad_content.split('\n') + lines = lad_content.split("\n") self._parse_networks(lines) else: print("No se encontró _LD_BODY - Asumiendo código ST") # Extraer código ST del programa principal self._extract_st_code(content) - + # Extraer ACTIONs self._parse_actions(content) - + def _parse_header_info(self, content): """Extraer información del header del programa""" # Buscar PATH y nombre del programa - path_match = re.search(r'\(\* @PATH := \'([^\']+)\' \*\)', content) + path_match = re.search(r"\(\* @PATH := \'([^\']+)\' \*\)", content) if path_match: self.program_path = path_match.group(1) - + # Verificar si es un archivo de variables globales - global_var_match = re.search(r'\(\*\s*@GLOBAL_VARIABLE_LIST\s*:=\s*(\w+)\s*\*\)', content) + global_var_match = re.search( + r"\(\*\s*@GLOBAL_VARIABLE_LIST\s*:=\s*(\w+)\s*\*\)", content + ) if global_var_match: self.program_type = "VAR_GLOBAL_LIST" self.program_name = global_var_match.group(1) print(f"Archivo de variables globales detectado: {self.program_name}") else: # Buscar nombre del programa/function_block/function/type - program_match = re.search(r'(PROGRAM|FUNCTION_BLOCK|FUNCTION|TYPE)\s+(\w+)', content) + program_match = re.search( + r"(PROGRAM|FUNCTION_BLOCK|FUNCTION|TYPE)\s+(\w+)", content + ) if program_match: self.program_type = program_match.group(1) self.program_name = program_match.group(2) - + print(f"Programa encontrado: {self.program_name}") if self.program_path: print(f"Path: {self.program_path}") - + def _parse_variable_declarations(self, content): """Extraer todas las declaraciones de variables""" # Buscar todas las secciones VAR var_patterns = [ - (r'VAR_INPUT(.*?)END_VAR', 'VAR_INPUT'), - (r'VAR_OUTPUT(.*?)END_VAR', 'VAR_OUTPUT'), - (r'VAR_IN_OUT(.*?)END_VAR', 'VAR_IN_OUT'), - (r'VAR\s+(.*?)END_VAR', 'VAR'), + (r"VAR_INPUT(.*?)END_VAR", "VAR_INPUT"), + (r"VAR_OUTPUT(.*?)END_VAR", "VAR_OUTPUT"), + (r"VAR_IN_OUT(.*?)END_VAR", "VAR_IN_OUT"), + (r"VAR\s+(.*?)END_VAR", "VAR"), ] - + for pattern, var_type in var_patterns: matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) if matches: variables = [] for match in matches: # Parsear cada línea de variable - for line in match.split('\n'): + for line in match.split("\n"): line = line.strip() - if line and not line.startswith('(*') and ':' in line: + if line and not line.startswith("(*") and ":" in line: # Limpiar comentarios inline - if '(*' in line: - line = line[:line.find('(*')] + if "(*" in line: + line = line[: line.find("(*")] variables.append(line.strip()) - + if variables: self.var_sections[var_type] = variables print(f"Variables {var_type}: {len(variables)} encontradas") - + def _parse_networks(self, lines): """Parse todas las redes""" i = 0 expected_networks = 0 - + # Buscar cuántas redes se esperan for line in lines: - if line.strip().startswith('_NETWORKS :'): - expected_networks = int(line.strip().split(':')[1].strip()) + if line.strip().startswith("_NETWORKS :"): + expected_networks = int(line.strip().split(":")[1].strip()) print(f"Se esperan {expected_networks} redes según el archivo") break - + while i < len(lines): - if lines[i].strip() == '_NETWORK': + if lines[i].strip() == "_NETWORK": self.current_network_id += 1 print(f"Procesando red {self.current_network_id}...") i = self._parse_network(lines, i) else: i += 1 - + if len(self.networks) != expected_networks: - print(f"ADVERTENCIA: Se esperaban {expected_networks} redes pero solo se parsearon {len(self.networks)}") + print( + f"ADVERTENCIA: Se esperaban {expected_networks} redes pero solo se parsearon {len(self.networks)}" + ) print("Esto puede indicar redes con _EMPTY o estructuras no reconocidas") - + def _parse_network(self, lines, start_idx): """Parse una red individual con soporte mejorado para operadores LAD""" network = { - 'id': self.current_network_id, - 'comment': '', - 'logic': None, - 'target': '', - 'function_blocks': [] + "id": self.current_network_id, + "comment": "", + "logic": None, + "target": "", + "function_blocks": [], } - + i = start_idx + 1 - + # Parse comentario - if i < len(lines) and lines[i].strip() == '_COMMENT': + if i < len(lines) and lines[i].strip() == "_COMMENT": i, comment = self._parse_comment(lines, i) - network['comment'] = comment - + network["comment"] = comment + # Parse contenido de la red while i < len(lines): line = lines[i].strip() - - if line == '_NETWORK': + + if line == "_NETWORK": break - elif line == '_LD_ASSIGN': + elif line == "_LD_ASSIGN": i += 1 # Parsear la lógica LAD después de _LD_ASSIGN i, logic = self._parse_lad_expression(lines, i) - network['logic'] = logic - elif line.startswith('_OUTPUT'): + network["logic"] = logic + elif line.startswith("_OUTPUT"): # Buscar variable de salida i += 1 - while i < len(lines) and lines[i].strip().startswith('_'): + while i < len(lines) and lines[i].strip().startswith("_"): i += 1 - if i < len(lines) and lines[i].strip() and 'ENABLELIST' not in lines[i]: - network['target'] = lines[i].strip() + if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]: + network["target"] = lines[i].strip() i += 1 else: i += 1 - + self.networks.append(network) print(f" Red {network['id']} agregada. Total redes: {len(self.networks)}") - if network['logic']: - print(f" Con lógica: {network['logic']['type']} - {network['logic'].get('name', 'Sin nombre')}") + if network["logic"]: + print( + f" Con lógica: {network['logic']['type']} - {network['logic'].get('name', 'Sin nombre')}" + ) print(f" Target: '{network['target']}'") return i - + def _parse_lad_expression(self, lines, start_idx): """Parse una expresión LAD recursivamente""" i = start_idx - + while i < len(lines): line = lines[i].strip() - - if line == '_LD_AND': + + if line == "_LD_AND": return self._parse_and_expression(lines, i + 1) - elif line == '_LD_OR': + elif line == "_LD_OR": return self._parse_or_expression(lines, i + 1) - elif line == '_LD_CONTACT': + elif line == "_LD_CONTACT": return self._parse_contact(lines, i + 1) - elif line.startswith('_FUNCTIONBLOCK'): + elif line.startswith("_FUNCTIONBLOCK"): return self._parse_function_block(lines, i) - elif line == '_EMPTY': + elif line == "_EMPTY": # Red vacía que puede contener funciones después print(f" Encontrada _EMPTY dentro de _LD_ASSIGN en línea {i}") i += 1 # Buscar funciones en las siguientes líneas i, empty_logic = self._parse_empty_network(lines, i) return i, empty_logic - elif line.startswith('_OUTPUT') or line == 'ENABLELIST : 0': + elif line.startswith("_OUTPUT") or line == "ENABLELIST : 0": break else: i += 1 - + return i, None - + def _parse_and_expression(self, lines, start_idx): """Parse una expresión AND""" i = start_idx operands = [] - + # Buscar operadores - if i < len(lines) and lines[i].strip().startswith('_LD_OPERATOR'): + if i < len(lines) and lines[i].strip().startswith("_LD_OPERATOR"): # Extraer número de operandos operator_line = lines[i].strip() - num_operands = int(operator_line.split(':')[-1].strip()) if ':' in operator_line else 2 + num_operands = ( + int(operator_line.split(":")[-1].strip()) if ":" in operator_line else 2 + ) i += 1 - + # Parse cada operando for _ in range(num_operands): i, operand = self._parse_lad_expression(lines, i) if operand: operands.append(operand) - - return i, {'type': 'AND', 'operands': operands} - + + return i, {"type": "AND", "operands": operands} + def _parse_or_expression(self, lines, start_idx): """Parse una expresión OR""" i = start_idx operands = [] - + # Buscar operadores - if i < len(lines) and lines[i].strip().startswith('_LD_OPERATOR'): + if i < len(lines) and lines[i].strip().startswith("_LD_OPERATOR"): # Extraer número de operandos operator_line = lines[i].strip() - num_operands = int(operator_line.split(':')[-1].strip()) if ':' in operator_line else 2 + num_operands = ( + int(operator_line.split(":")[-1].strip()) if ":" in operator_line else 2 + ) i += 1 - + # Parse cada operando for _ in range(num_operands): i, operand = self._parse_lad_expression(lines, i) if operand: operands.append(operand) - - return i, {'type': 'OR', 'operands': operands} - + + return i, {"type": "OR", "operands": operands} + def _parse_contact(self, lines, start_idx): """Parse un contacto LAD""" i = start_idx contact_name = "" negated = False - + # Obtener nombre del contacto if i < len(lines): contact_name = lines[i].strip() i += 1 - + # Verificar si hay expresión - if i < len(lines) and lines[i].strip() == '_EXPRESSION': + if i < len(lines) and lines[i].strip() == "_EXPRESSION": i += 1 # Verificar si está negado if i < len(lines): - if lines[i].strip() == '_NEGATIV': + if lines[i].strip() == "_NEGATIV": negated = True i += 1 - elif lines[i].strip() == '_POSITIV': + elif lines[i].strip() == "_POSITIV": i += 1 - - return i, {'type': 'CONTACT', 'name': contact_name, 'negated': negated} - + + return i, {"type": "CONTACT", "name": contact_name, "negated": negated} + def _parse_function_block(self, lines, start_idx): """Parse un bloque de función o llamada a ACTION""" i = start_idx + 1 @@ -320,100 +339,114 @@ class SimpleLadConverter: action_call = None max_iterations = 50 # Protección contra bucles infinitos iterations = 0 - + if i < len(lines): fb_name = lines[i].strip() i += 1 - + # PATRÓN IDENTIFICADO: ??? indica llamada a ACTION if fb_name == "???": - print(f" Detectado patrón ??? en línea {i-1} - Buscando ACTION call...") + print( + f" Detectado patrón ??? en línea {i-1} - Buscando ACTION call..." + ) # Buscar el nombre completo de la ACTION más adelante action_call = self._find_action_call_name(lines, i) if action_call: print(f" ✓ ACTION call encontrada: {action_call}") - return i, {'type': 'ACTION_CALL', 'name': action_call, 'inputs': []} + return i, {"type": "ACTION_CALL", "name": action_call, "inputs": []} else: print(f" ⚠ Patrón ??? pero no se encontró nombre de ACTION") else: print(f" Detectado Function Block directo: {fb_name}") - + # Parse inputs del function block normal - while (i < len(lines) and - not lines[i].strip().startswith('_OUTPUT') and - iterations < max_iterations): - + while ( + i < len(lines) + and not lines[i].strip().startswith("_OUTPUT") + and iterations < max_iterations + ): + line = lines[i].strip() iterations += 1 - - if line.startswith('_OPERAND'): + + if line.startswith("_OPERAND"): i += 2 # Saltar _EXPRESSION if i < len(lines): inputs.append(lines[i].strip()) i += 1 - elif line and not line.startswith('_'): + elif line and not line.startswith("_"): # Para function blocks normales, recopilar inputs # Para ACTION calls perdidas, último intento - if '.' in line and fb_name == "???" and action_call is None: + if "." in line and fb_name == "???" and action_call is None: action_call = line print(f" ✓ ACTION call encontrada (fallback): {action_call}") - return i, {'type': 'ACTION_CALL', 'name': action_call, 'inputs': inputs} - elif line.startswith('ENABLELIST'): + return i, { + "type": "ACTION_CALL", + "name": action_call, + "inputs": inputs, + } + elif line.startswith("ENABLELIST"): # Fin del bloque break else: i += 1 - + # Salida de emergencia del bucle if iterations >= max_iterations: - print(f" ADVERTENCIA: Bucle infinito evitado en function block en línea {start_idx}") + print( + f" ADVERTENCIA: Bucle infinito evitado en function block en línea {start_idx}" + ) break - - return i, {'type': 'FUNCTION_BLOCK', 'name': fb_name, 'inputs': inputs} - + + return i, {"type": "FUNCTION_BLOCK", "name": fb_name, "inputs": inputs} + def _find_action_call_name(self, lines, start_idx): """Buscar el nombre completo de la ACTION después de ???""" i = start_idx print(f" Buscando ACTION name desde línea {start_idx}...") - + while i < len(lines) and i < start_idx + 15: # Buscar en las próximas 15 líneas line = lines[i].strip() print(f" Línea {i}: '{line}'") - + # Buscar patrón namespace.actionname (ej: _Filling_Head_PID_Ctrl.Read_Analog) - if ('.' in line and - 'ENABLELIST' not in line and - 'EXPRESSION' not in line and - 'BOX_EXPR' not in line and - 'ENABLED' not in line and - 'OUTPUTS' not in line and - 'NO_SET' not in line and - 'OUTPUT' not in line and - line != '_POSITIV' and - line != '_NEGATIV' and - line != '_NO_SET'): + if ( + "." in line + and "ENABLELIST" not in line + and "EXPRESSION" not in line + and "BOX_EXPR" not in line + and "ENABLED" not in line + and "OUTPUTS" not in line + and "NO_SET" not in line + and "OUTPUT" not in line + and line != "_POSITIV" + and line != "_NEGATIV" + and line != "_NO_SET" + ): print(f" ✓ ACTION name encontrado: {line}") return line i += 1 - - print(f" ⚠ No se encontró ACTION name en {start_idx + 15 - start_idx} líneas") + + print( + f" ⚠ No se encontró ACTION name en {start_idx + 15 - start_idx} líneas" + ) return None - + def _parse_comment(self, lines, start_idx): """Parse comentario""" i = start_idx + 1 comment_lines = [] - + while i < len(lines): line = lines[i].strip() - if line == '_END_COMMENT': + if line == "_END_COMMENT": break - if line and not line.startswith('_'): + if line and not line.startswith("_"): comment_lines.append(line) i += 1 - - return i + 1, ' '.join(comment_lines) - + + return i + 1, " ".join(comment_lines) + def _parse_empty_network(self, lines, start_idx): """Parse una red que empieza con _EMPTY y puede contener funciones""" i = start_idx @@ -421,24 +454,26 @@ class SimpleLadConverter: iterations = 0 function_found = None target_found = None - + print(f" Entrando a _parse_empty_network desde línea {start_idx}") - - while (i < len(lines) and - iterations < max_iterations and - not lines[i].strip().startswith('_OUTPUT') and - not lines[i].strip() == '_NETWORK'): - + + while ( + i < len(lines) + and iterations < max_iterations + and not lines[i].strip().startswith("_OUTPUT") + and not lines[i].strip() == "_NETWORK" + ): + line = lines[i].strip() iterations += 1 print(f" Línea {i}: '{line}'") - - if line.startswith('_FUNCTIONBLOCK'): + + if line.startswith("_FUNCTIONBLOCK"): # Encontrada llamada a función/ACTION print(f" ENCONTRADO _FUNCTIONBLOCK en línea {i}") i, logic = self._parse_function_block(lines, i) function_found = logic - elif line.startswith('_FUNCTION'): + elif line.startswith("_FUNCTION"): # Otra variante de función print(f" ENCONTRADO _FUNCTION en línea {i}") i += 1 @@ -446,86 +481,98 @@ class SimpleLadConverter: while i < len(lines) and i < start_idx + 10: func_line = lines[i].strip() print(f" Buscando nombre función línea {i}: '{func_line}'") - if (func_line and - not func_line.startswith('_') and - 'ENABLELIST' not in func_line): + if ( + func_line + and not func_line.startswith("_") + and "ENABLELIST" not in func_line + ): print(f" ENCONTRADO nombre función: {func_line}") - function_found = {'type': 'FUNCTION_CALL', 'name': func_line, 'inputs': []} + function_found = { + "type": "FUNCTION_CALL", + "name": func_line, + "inputs": [], + } break i += 1 - elif line.startswith('ENABLELIST'): + elif line.startswith("ENABLELIST"): print(f" Encontrado ENABLELIST, continuando búsqueda...") # No terminar aquí, continuar buscando _ASSIGN i += 1 - elif line.startswith('_ASSIGN'): + elif line.startswith("_ASSIGN"): print(f" ENCONTRADO _ASSIGN en línea {i}") # Buscar función dentro de _ASSIGN i += 1 i, assign_logic = self._parse_assign_section(lines, i) if assign_logic: function_found = assign_logic - elif line.startswith('_OUTPUT'): + elif line.startswith("_OUTPUT"): # Buscar el target después de _OUTPUT print(f" ENCONTRADO _OUTPUT, buscando target...") i += 1 - while i < len(lines) and lines[i].strip().startswith('_'): + while i < len(lines) and lines[i].strip().startswith("_"): i += 1 - if i < len(lines) and lines[i].strip() and 'ENABLELIST' not in lines[i]: + if i < len(lines) and lines[i].strip() and "ENABLELIST" not in lines[i]: target_found = lines[i].strip() print(f" ENCONTRADO target: {target_found}") break else: i += 1 - + # Si encontramos una función, crear red independiente if function_found and target_found: - print(f" Creando red independiente para función con target: {target_found}") + print( + f" Creando red independiente para función con target: {target_found}" + ) self._create_function_network(function_found, target_found) return i, function_found elif function_found: print(f" Función encontrada pero sin target específico") # Crear target por defecto basado en el tipo de función - if function_found['type'] == 'ACTION_CALL': - default_target = 'mDummy' # Variable dummy típica para ACTION calls - elif function_found['type'] == 'FUNCTION_CALL': + if function_found["type"] == "ACTION_CALL": + default_target = "mDummy" # Variable dummy típica para ACTION calls + elif function_found["type"] == "FUNCTION_CALL": # Para funciones como ProductLiterInTank, usar la variable global correspondiente - if function_found['name'] == 'gProductTankLevel': - default_target = 'gTankProdAmount' # Variable que se asigna según el contexto + if function_found["name"] == "gProductTankLevel": + default_target = ( + "gTankProdAmount" # Variable que se asigna según el contexto + ) else: - default_target = 'mDummy' + default_target = "mDummy" else: # Caso por defecto para otros tipos de función (FUNCTION_BLOCK, etc.) - default_target = 'mDummy' - + default_target = "mDummy" + print(f" Usando target por defecto: {default_target}") self._create_function_network(function_found, default_target) return i, function_found - + print(f" _parse_empty_network terminó sin encontrar función") return i, None - + def _parse_assign_section(self, lines, start_idx): """Parse una sección _ASSIGN que puede contener funciones""" i = start_idx max_iterations = 15 iterations = 0 - + print(f" Entrando a _parse_assign_section desde línea {start_idx}") - - while (i < len(lines) and - iterations < max_iterations and - not lines[i].strip() == '_NETWORK' and - not lines[i].strip() == 'ENABLELIST_END'): - + + while ( + i < len(lines) + and iterations < max_iterations + and not lines[i].strip() == "_NETWORK" + and not lines[i].strip() == "ENABLELIST_END" + ): + line = lines[i].strip() iterations += 1 print(f" Línea {i}: '{line}'") - - if line.startswith('_FUNCTIONBLOCK'): + + if line.startswith("_FUNCTIONBLOCK"): print(f" ENCONTRADO _FUNCTIONBLOCK en _ASSIGN: línea {i}") i, logic = self._parse_function_block(lines, i) return i, logic - elif line.startswith('_FUNCTION'): + elif line.startswith("_FUNCTION"): print(f" ENCONTRADO _FUNCTION en _ASSIGN: línea {i}") # Buscar el nombre de la función en las siguientes líneas func_name = None @@ -533,66 +580,70 @@ class SimpleLadConverter: while j < len(lines) and j < i + 8: func_line = lines[j].strip() print(f" Buscando nombre función línea {j}: '{func_line}'") - if (func_line and - not func_line.startswith('_') and - 'ENABLELIST' not in func_line and - ':' not in func_line): + if ( + func_line + and not func_line.startswith("_") + and "ENABLELIST" not in func_line + and ":" not in func_line + ): func_name = func_line print(f" ENCONTRADO nombre función: {func_name}") break j += 1 - + if func_name: - return j, {'type': 'FUNCTION_CALL', 'name': func_name, 'inputs': []} + return j, {"type": "FUNCTION_CALL", "name": func_name, "inputs": []} else: i += 1 - elif line == 'ENABLELIST_END': + elif line == "ENABLELIST_END": print(f" Encontrado ENABLELIST_END, terminando") break else: i += 1 - + print(f" _parse_assign_section terminó sin encontrar función") return i, None - + def _create_function_network(self, function_logic, target_name): """Crear una red independiente para una función o ACTION call""" self.current_network_id += 1 - + function_network = { - 'id': self.current_network_id, - 'comment': f'Llamada a función: {function_logic.get("name", "unknown")}', - 'logic': function_logic, - 'target': target_name, - 'function_blocks': [] + "id": self.current_network_id, + "comment": f'Llamada a función: {function_logic.get("name", "unknown")}', + "logic": function_logic, + "target": target_name, + "function_blocks": [], } - + self.networks.append(function_network) - print(f" Red de función {function_network['id']} creada para {function_logic['type']}: {function_logic.get('name', 'Sin nombre')}") + print( + f" Red de función {function_network['id']} creada para {function_logic['type']}: {function_logic.get('name', 'Sin nombre')}" + ) print(f" Target: '{target_name}'") return function_network - + def convert_to_structured(self): """Convertir a código SCL completo con variables y ACTIONs""" output = [] - + # Header del archivo output.append("(* Código SCL generado desde LAD TwinCAT *)") output.append("(* Convertidor mejorado con SymPy - Estructura DNF preferida *)") if self.program_path: output.append(f"(* Path original: {self.program_path} *)") output.append("") - + # Verificar si es un TYPE o VAR_GLOBAL_LIST - manejarlos diferente - program_type = self.program_type if hasattr(self, 'program_type') else "PROGRAM" + program_type = self.program_type if hasattr(self, "program_type") else "PROGRAM" if program_type == "TYPE": return self._convert_type_to_scl(output) elif program_type == "VAR_GLOBAL_LIST": return self._convert_global_vars_to_scl(output) - + # Declaración del programa/función program_name = self.program_name if self.program_name else "ConvertedProgram" - + # Para FUNCTIONs, necesitamos detectar el tipo de retorno if program_type == "FUNCTION": # Buscar tipo de retorno en el código original (ej: FUNCTION ArrayToReal : REAL) @@ -603,132 +654,147 @@ class SimpleLadConverter: output.append(f"FUNCTION {program_name} : BOOL") # Tipo por defecto else: output.append(f"{program_type} {program_name}") - + # Declaraciones de variables self._add_variable_sections(output) - + # Inicio del código principal output.append("") output.append("(* === CÓDIGO PRINCIPAL === *)") output.append("") - + # Código principal - LAD o ST if self.networks: # Hay redes LAD - generar código desde redes output.append(" (* Código LAD convertido *)") for network in self.networks: output.append(f" // Red {network['id']}") - if network['comment']: + if network["comment"]: output.append(f" // {network['comment']}") - - if network['logic'] and network['target']: + + if network["logic"] and network["target"]: # Verificar si es una llamada a ACTION sin condiciones - if (network['logic']['type'] == 'ACTION_CALL' and - network['target'] in ['mDummy', 'EN_Out']): # Variables dummy típicas + if network["logic"]["type"] == "ACTION_CALL" and network[ + "target" + ] in [ + "mDummy", + "EN_Out", + ]: # Variables dummy típicas # Es una llamada a ACTION incondicional - action_name = network['logic']['name'] + action_name = network["logic"]["name"] output.append(f" CALL {action_name}();") - output.append(f" {network['target']} := TRUE; // ACTION ejecutada") - elif (network['logic']['type'] == 'FUNCTION_CALL' and - network['target'] in ['mDummy', 'EN_Out', 'gTankProdAmount']): # Variables típicas + output.append( + f" {network['target']} := TRUE; // ACTION ejecutada" + ) + elif network["logic"]["type"] == "FUNCTION_CALL" and network[ + "target" + ] in [ + "mDummy", + "EN_Out", + "gTankProdAmount", + ]: # Variables típicas # Es una llamada a FUNCTION incondicional - function_call = self._convert_logic_to_string(network['logic']) + function_call = self._convert_logic_to_string(network["logic"]) output.append(f" {network['target']} := {function_call};") else: # Usar expresión DNF optimizada si está disponible - if network['id'] in self.sympy_expressions: - sympy_expr = self.sympy_expressions[network['id']] + if network["id"] in self.sympy_expressions: + sympy_expr = self.sympy_expressions[network["id"]] condition_str = self._format_dnf_for_lad(sympy_expr) # output.append(f" // Optimizada con SymPy DNF") else: # Fallback al método original - condition_str = self._convert_logic_to_string(network['logic']) + condition_str = self._convert_logic_to_string( + network["logic"] + ) output.append(f" // Sin optimización SymPy") - + if condition_str: # Si hay saltos de línea en la condición (múltiples términos OR) - if '\n' in condition_str: + if "\n" in condition_str: output.append(f" IF {condition_str} THEN") else: output.append(f" IF {condition_str} THEN") - + output.append(f" {network['target']} := TRUE;") output.append(" ELSE") output.append(f" {network['target']} := FALSE;") output.append(" END_IF;") else: - output.append(f" {network['target']} := TRUE; // Logic no reconocida") - + output.append( + f" {network['target']} := TRUE; // Logic no reconocida" + ) + output.append("") - + elif self.st_main_code: # Hay código ST - copiarlo directamente preservando indentación output.append(" (* Código ST original *)") - for line in self.st_main_code.split('\n'): + for line in self.st_main_code.split("\n"): if line.strip(): # Preservar indentación original pero agregar indentación base SCL output.append(f" {line}") else: output.append("") - + else: # No hay código principal output.append(" (* Sin código principal detectado *)") output.append("") - + # Agregar ACTIONs como subfunciones self._add_actions_as_procedures(output, program_name) - + # Fin del programa principal - program_type = self.program_type if hasattr(self, 'program_type') else "PROGRAM" + program_type = self.program_type if hasattr(self, "program_type") else "PROGRAM" output.append(f"END_{program_type}") - - return '\n'.join(output) - + + return "\n".join(output) + def _add_variable_sections(self, output): """Agregar todas las secciones de variables al código""" # Orden preferido de las secciones - section_order = ['VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR'] - + section_order = ["VAR_INPUT", "VAR_OUTPUT", "VAR_IN_OUT", "VAR"] + for section_type in section_order: if section_type in self.var_sections: output.append(f"{section_type}") for var_line in self.var_sections[section_type]: # Asegurar que termine con punto y coma - if not var_line.endswith(';'): - var_line += ' ;' + if not var_line.endswith(";"): + var_line += " ;" output.append(f" {var_line}") output.append("END_VAR") output.append("") - + def _add_actions_as_procedures(self, output, program_name): """Agregar ACTIONs como procedimientos independientes""" if not self.actions: return - + output.append("") output.append("(* === SUBFUNCIONES (ACTIONs convertidas) === *)") output.append("") - + for action_name, action_data in self.actions.items(): # Nombre completo como se usa en TwinCAT (sin doble underscore) full_name = f"{program_name}_{action_name}" - + output.append(f"PROCEDURE {full_name}") output.append("(* Convertida desde ACTION *)") output.append("") - - if isinstance(action_data, dict) and action_data['type'] == 'LAD': + + if isinstance(action_data, dict) and action_data["type"] == "LAD": # ACTION con código LAD - convertir a SCL output.append(" (* Código LAD convertido a SCL *)") - - for network in action_data['networks']: + + for network in action_data["networks"]: output.append(f" // Red {network['id']}") - if network['comment']: + if network["comment"]: output.append(f" // {network['comment']}") - - if network['logic'] and network['target']: - condition_str = self._convert_logic_to_string(network['logic']) + + if network["logic"] and network["target"]: + condition_str = self._convert_logic_to_string(network["logic"]) if condition_str: output.append(f" IF {condition_str} THEN") output.append(f" {network['target']} := TRUE;") @@ -736,305 +802,320 @@ class SimpleLadConverter: output.append(f" {network['target']} := FALSE;") output.append(" END_IF;") else: - output.append(f" {network['target']} := TRUE; // Logic no reconocida") - + output.append( + f" {network['target']} := TRUE; // Logic no reconocida" + ) + output.append("") - - elif isinstance(action_data, dict) and action_data['type'] == 'ST': + + elif isinstance(action_data, dict) and action_data["type"] == "ST": # ACTION con código ST - copiar directamente preservando indentación output.append(" (* Código ST original *)") - for line in action_data['code'].split('\n'): + for line in action_data["code"].split("\n"): if line.strip(): # Preservar indentación original pero agregar indentación base SCL output.append(f" {line}") else: output.append("") - + else: # Compatibilidad hacia atrás - código directo (legacy) output.append(" (* Código original - modo compatibilidad *)") - for line in str(action_data).split('\n'): + for line in str(action_data).split("\n"): if line.strip(): # Preservar indentación original pero agregar indentación base SCL output.append(f" {line}") else: output.append("") - + output.append("") output.append("END_PROCEDURE") output.append("") - + def _convert_logic_to_string(self, logic): """Convertir lógica LAD a string estructurado""" if not logic: return "" - - if logic['type'] == 'CONTACT': - if logic['negated']: + + if logic["type"] == "CONTACT": + if logic["negated"]: return f"NOT {logic['name']}" else: - return logic['name'] - - elif logic['type'] == 'AND': + return logic["name"] + + elif logic["type"] == "AND": operand_strings = [] - for operand in logic['operands']: + for operand in logic["operands"]: operand_str = self._convert_logic_to_string(operand) if operand_str: operand_strings.append(operand_str) - + if len(operand_strings) > 1: return "(" + " AND ".join(operand_strings) + ")" elif len(operand_strings) == 1: return operand_strings[0] else: return "" - - elif logic['type'] == 'OR': + + elif logic["type"] == "OR": operand_strings = [] - for operand in logic['operands']: + for operand in logic["operands"]: operand_str = self._convert_logic_to_string(operand) if operand_str: operand_strings.append(operand_str) - + if len(operand_strings) > 1: return "(" + " OR ".join(operand_strings) + ")" elif len(operand_strings) == 1: return operand_strings[0] else: return "" - - elif logic['type'] == 'FUNCTION_BLOCK': - inputs_str = ", ".join(logic['inputs']) if logic['inputs'] else "" + + elif logic["type"] == "FUNCTION_BLOCK": + inputs_str = ", ".join(logic["inputs"]) if logic["inputs"] else "" return f"{logic['name']}({inputs_str})" - - elif logic['type'] == 'ACTION_CALL': - return logic['name'] # Solo devolver el nombre, no CALL aquí - - elif logic['type'] == 'FUNCTION_CALL': + + elif logic["type"] == "ACTION_CALL": + return logic["name"] # Solo devolver el nombre, no CALL aquí + + elif logic["type"] == "FUNCTION_CALL": return f"{logic['name']}()" - + return "" - + def save_to_file(self, output_path): """Guardar código estructurado""" structured_code = self.convert_to_structured() - - with open(output_path, 'w', encoding='utf-8') as f: + + with open(output_path, "w", encoding="utf-8") as f: f.write(structured_code) - + return structured_code def print_debug_info(self): """Mostrar información de debug sobre los networks parseados""" print(f"\n=== DEBUG INFO - {len(self.networks)} networks encontrados ===") - + for network in self.networks: print(f"\nRed {network['id']}:") - if network['comment']: + if network["comment"]: print(f" Comentario: {network['comment']}") print(f" Target: {network['target']}") - - if network['logic']: + + if network["logic"]: print(f" Lógica: {self._debug_logic_string(network['logic'])}") - condition_str = self._convert_logic_to_string(network['logic']) + condition_str = self._convert_logic_to_string(network["logic"]) print(f" Condición: {condition_str}") else: print(" Sin lógica") - + def _debug_logic_string(self, logic, indent=0): """Crear string de debug para la lógica""" if not logic: return "None" - + prefix = " " * indent - - if logic['type'] == 'CONTACT': - neg_str = " (NEGADO)" if logic['negated'] else "" + + if logic["type"] == "CONTACT": + neg_str = " (NEGADO)" if logic["negated"] else "" return f"{prefix}CONTACT: {logic['name']}{neg_str}" - - elif logic['type'] == 'AND': + + elif logic["type"] == "AND": result = f"{prefix}AND:\n" - for operand in logic['operands']: + for operand in logic["operands"]: result += self._debug_logic_string(operand, indent + 1) + "\n" return result.rstrip() - - elif logic['type'] == 'OR': + + elif logic["type"] == "OR": result = f"{prefix}OR:\n" - for operand in logic['operands']: + for operand in logic["operands"]: result += self._debug_logic_string(operand, indent + 1) + "\n" return result.rstrip() - - elif logic['type'] == 'FUNCTION_BLOCK': + + elif logic["type"] == "FUNCTION_BLOCK": return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}" - - elif logic['type'] == 'ACTION_CALL': + + elif logic["type"] == "ACTION_CALL": return f"{prefix}ACTION_CALL: {logic['name']}" - - elif logic['type'] == 'FUNCTION_CALL': + + elif logic["type"] == "FUNCTION_CALL": return f"{prefix}FUNCTION_CALL: {logic['name']}" - + return f"{prefix}UNKNOWN: {logic}" def _logic_to_sympy(self, logic): """Convertir lógica LAD a expresión SymPy""" if not logic: return None - - if logic['type'] == 'CONTACT': - symbol = self.symbol_manager.get_symbol(logic['name']) - if logic['negated']: + + if logic["type"] == "CONTACT": + symbol = self.symbol_manager.get_symbol(logic["name"]) + if logic["negated"]: return Not(symbol) else: return symbol - - elif logic['type'] == 'AND': + + elif logic["type"] == "AND": sympy_operands = [] - for operand in logic['operands']: + for operand in logic["operands"]: operand_sympy = self._logic_to_sympy(operand) if operand_sympy is not None: sympy_operands.append(operand_sympy) - + if len(sympy_operands) > 1: return And(*sympy_operands) elif len(sympy_operands) == 1: return sympy_operands[0] else: return None - - elif logic['type'] == 'OR': + + elif logic["type"] == "OR": sympy_operands = [] - for operand in logic['operands']: + for operand in logic["operands"]: operand_sympy = self._logic_to_sympy(operand) if operand_sympy is not None: sympy_operands.append(operand_sympy) - + if len(sympy_operands) > 1: return Or(*sympy_operands) elif len(sympy_operands) == 1: return sympy_operands[0] else: return None - - elif logic['type'] == 'FUNCTION_BLOCK': + + elif logic["type"] == "FUNCTION_BLOCK": # Para function blocks, creamos un símbolo especial fb_name = f"{logic['name']}({', '.join(logic['inputs'])})" return self.symbol_manager.get_symbol(fb_name) - - elif logic['type'] == 'ACTION_CALL': + + elif logic["type"] == "ACTION_CALL": # Para llamadas a ACTION, creamos un símbolo especial action_name = f"CALL_{logic['name'].replace('.', '_')}" return self.symbol_manager.get_symbol(action_name) - + return None - + def _sympy_to_structured_string(self, sympy_expr): """Convertir expresión SymPy a string estructurado""" if sympy_expr is None: return "" - + # Crear un mapeo inverso de símbolos a nombres originales symbol_to_name = {} for original_name, symbol in self.symbol_manager._symbol_cache.items(): symbol_to_name[str(symbol)] = original_name - + # Convertir la expresión a string str_expr = str(sympy_expr) - + # Reemplazar símbolos por nombres originales for symbol_str, original_name in symbol_to_name.items(): str_expr = str_expr.replace(symbol_str, original_name) - + # Reemplazar operadores SymPy por operadores IEC61131-3 - str_expr = str_expr.replace('&', ' AND ') - str_expr = str_expr.replace('|', ' OR ') - str_expr = str_expr.replace('~', 'NOT ') - + str_expr = str_expr.replace("&", " AND ") + str_expr = str_expr.replace("|", " OR ") + str_expr = str_expr.replace("~", "NOT ") + # Limpiar espacios múltiples - str_expr = re.sub(r'\s+', ' ', str_expr) - + str_expr = re.sub(r"\s+", " ", str_expr) + return str_expr.strip() - + def optimize_expressions(self): """Optimizar todas las expresiones usando SymPy - Preferir DNF para LAD""" print("\n=== Optimizando expresiones con SymPy (forzando DNF para LAD) ===") - + for network in self.networks: - if network['logic']: + if network["logic"]: print(f"\nOptimizando Red {network['id']}:") - + # Convertir a SymPy - sympy_expr = self._logic_to_sympy(network['logic']) + sympy_expr = self._logic_to_sympy(network["logic"]) if sympy_expr: print(f" Expresión original: {sympy_expr}") - + # Simplificar primero try: simplified = simplify(sympy_expr) print(f" Simplificada: {simplified}") - + # Verificar complejidad antes de DNF num_symbols = len(simplified.free_symbols) complexity = self._estimate_expression_complexity(simplified) - + if num_symbols > 12 or complexity > 800: - print(f" ADVERTENCIA: Expresión muy compleja ({num_symbols} símbolos, complejidad {complexity})") - print(f" Saltando conversión DNF por rendimiento - usando simplificación básica") + print( + f" ADVERTENCIA: Expresión muy compleja ({num_symbols} símbolos, complejidad {complexity})" + ) + print( + f" Saltando conversión DNF por rendimiento - usando simplificación básica" + ) final_expr = simplified else: # SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND)) dnf_expr = to_dnf(simplified) print(f" DNF (forma LAD preferida): {dnf_expr}") - + # Post-procesar para eliminar contradicciones final_expr = self._post_process_expression(dnf_expr) - + # Verificar si el post-procesamiento cambió algo if str(final_expr) != str(dnf_expr): print(f" Post-procesada: {final_expr}") - - self.sympy_expressions[network['id']] = final_expr - + + self.sympy_expressions[network["id"]] = final_expr + except Exception as e: print(f" Error optimizando: {e}") - self.sympy_expressions[network['id']] = sympy_expr + self.sympy_expressions[network["id"]] = sympy_expr def group_common_conditions(self): """Agrupar networks con condiciones similares o complementarias""" print("\n=== Analizando agrupación de condiciones ===") - + # Buscar networks que podrían agruparse groupable_networks = [] for network in self.networks: - if (network['logic'] and network['target'] and - network['id'] in self.sympy_expressions): + if ( + network["logic"] + and network["target"] + and network["id"] in self.sympy_expressions + ): groupable_networks.append(network) - + if len(groupable_networks) < 2: print("No hay suficientes networks para agrupar") return - + # Analizar condiciones comunes print(f"Analizando {len(groupable_networks)} networks para agrupación:") - + for i, net1 in enumerate(groupable_networks): - for j, net2 in enumerate(groupable_networks[i+1:], i+1): - expr1 = self.sympy_expressions[net1['id']] - expr2 = self.sympy_expressions[net2['id']] - + for j, net2 in enumerate(groupable_networks[i + 1 :], i + 1): + expr1 = self.sympy_expressions[net1["id"]] + expr2 = self.sympy_expressions[net2["id"]] + # Buscar factores comunes try: # Extraer factores comunes si es posible common_factors = self._find_common_factors(expr1, expr2) if common_factors: - print(f" Red {net1['id']} y Red {net2['id']} comparten: {common_factors}") - + print( + f" Red {net1['id']} y Red {net2['id']} comparten: {common_factors}" + ) + # Verificar si son complementarias (útil para SET/RESET) if self._are_complementary(expr1, expr2): - print(f" Red {net1['id']} y Red {net2['id']} son complementarias") - + print( + f" Red {net1['id']} y Red {net2['id']} son complementarias" + ) + except Exception as e: - print(f" Error analizando Red {net1['id']} y Red {net2['id']}: {e}") - + print( + f" Error analizando Red {net1['id']} y Red {net2['id']}: {e}" + ) + def _find_common_factors(self, expr1, expr2): """Encontrar factores comunes entre dos expresiones""" try: @@ -1042,14 +1123,14 @@ class SimpleLadConverter: symbols1 = expr1.free_symbols symbols2 = expr2.free_symbols common_symbols = symbols1.intersection(symbols2) - + if len(common_symbols) > 1: return f"{len(common_symbols)} símbolos comunes" - + return None except: return None - + def _are_complementary(self, expr1, expr2): """Verificar si dos expresiones son complementarias""" try: @@ -1057,7 +1138,7 @@ class SimpleLadConverter: complement = Not(expr2) simplified_complement = simplify(complement) simplified_expr1 = simplify(expr1) - + return simplified_expr1.equals(simplified_complement) except: return False @@ -1068,61 +1149,63 @@ class SimpleLadConverter: # Contar operadores AND/OR anidados complexity = 0 expr_str = str(expr) - + # Contar número de operadores - and_count = expr_str.count('&') - or_count = expr_str.count('|') - not_count = expr_str.count('~') - + and_count = expr_str.count("&") + or_count = expr_str.count("|") + not_count = expr_str.count("~") + complexity += and_count * 2 # AND - complexity += or_count * 4 # OR (más costoso en DNF) + complexity += or_count * 4 # OR (más costoso en DNF) complexity += not_count * 1 # NOT - + # Penalizar anidamiento profundo - depth = expr_str.count('(') + depth = expr_str.count("(") complexity += depth * 6 - + # Número de símbolos únicos num_symbols = len(expr.free_symbols) complexity += num_symbols * 15 - + # Detectar patrones problemáticos específicos # CNF con muchos términos (patrón típico que causa explosión) if and_count > 10 and or_count > 15: complexity += 2000 # Penalización severa para CNF complejas - + # Expresiones con muchos términos negativos (difíciles para DNF) if not_count > 8: complexity += 500 - + # Longitud total como indicador de complejidad if len(expr_str) > 1000: complexity += len(expr_str) // 2 - + return complexity except: return 999999 # Asumir muy complejo si hay error - + def _post_process_expression(self, expr): """Post-procesar expresión para eliminar contradicciones obvias""" try: # Detectar contradicciones como X & ~X que deberían ser False cleaned_expr = expr - + # Aplicar simplificaciones adicionales cleaned_expr = simplify(cleaned_expr) - + # Si la expresión contiene contradicciones obvias, intentar limpiar free_symbols = cleaned_expr.free_symbols for symbol in free_symbols: # Verificar si tenemos symbol & ~symbol en alguna parte contradiction = And(symbol, Not(symbol)) if cleaned_expr.has(contradiction): - print(f" Detectada contradicción eliminable: {symbol} AND NOT {symbol}") + print( + f" Detectada contradicción eliminable: {symbol} AND NOT {symbol}" + ) # Reemplazar contradicción por False cleaned_expr = cleaned_expr.replace(contradiction, sympy.false) cleaned_expr = simplify(cleaned_expr) - + return cleaned_expr except: return expr @@ -1131,186 +1214,194 @@ class SimpleLadConverter: """Formatear expresión DNF para código LAD más legible""" if sympy_expr is None: return "" - + # Crear mapeo de símbolos a nombres originales symbol_to_name = {} for original_name, symbol in self.symbol_manager._symbol_cache.items(): symbol_to_name[str(symbol)] = original_name - + # Convertir a string y analizar la estructura str_expr = str(sympy_expr) - + # Reemplazar símbolos por nombres originales for symbol_str, original_name in symbol_to_name.items(): str_expr = str_expr.replace(symbol_str, original_name) - + # Reemplazar operadores SymPy por IEC61131-3 primero - str_expr = str_expr.replace('&', ' AND ') - str_expr = str_expr.replace('|', ' OR ') - str_expr = str_expr.replace('~', 'NOT ') - + str_expr = str_expr.replace("&", " AND ") + str_expr = str_expr.replace("|", " OR ") + str_expr = str_expr.replace("~", "NOT ") + # Limpiar espacios múltiples - str_expr = re.sub(r'\s+', ' ', str_expr).strip() - + str_expr = re.sub(r"\s+", " ", str_expr).strip() + # Si es una expresión OR de términos AND principales, formatear cada término # Buscar el patrón de OR principales (no anidados en paréntesis) - if ' OR ' in str_expr and not str_expr.startswith('('): + if " OR " in str_expr and not str_expr.startswith("("): # Dividir por OR de nivel principal # Esto es más complejo debido a paréntesis anidados parts = self._split_main_or_terms(str_expr) - + if len(parts) > 1: formatted_terms = [] for part in parts: part = part.strip() # Asegurar que cada término tenga paréntesis si es complejo - if ' AND ' in part and not (part.startswith('(') and part.endswith(')')): + if " AND " in part and not ( + part.startswith("(") and part.endswith(")") + ): part = f"({part})" formatted_terms.append(part) - + # Unir con OR y saltos de línea para mejor legibilidad - return '\n OR '.join(formatted_terms) - + return "\n OR ".join(formatted_terms) + return str_expr - + def _split_main_or_terms(self, expr): """Dividir expresión por OR de nivel principal, respetando paréntesis""" parts = [] current_part = "" paren_level = 0 i = 0 - + while i < len(expr): char = expr[i] - - if char == '(': + + if char == "(": paren_level += 1 current_part += char - elif char == ')': + elif char == ")": paren_level -= 1 current_part += char - elif paren_level == 0 and expr[i:i+4] == ' OR ': + elif paren_level == 0 and expr[i : i + 4] == " OR ": # OR de nivel principal encontrado parts.append(current_part.strip()) current_part = "" i += 3 # Saltar ' OR ' else: current_part += char - + i += 1 - + # Agregar la última parte if current_part.strip(): parts.append(current_part.strip()) - + return parts if len(parts) > 1 else [expr] def _parse_actions(self, content): """Extraer todas las ACTIONs del programa""" # Buscar patrón ACTION nombre: ... END_ACTION - action_pattern = r'ACTION\s+(\w+)\s*:(.*?)END_ACTION' + action_pattern = r"ACTION\s+(\w+)\s*:(.*?)END_ACTION" action_matches = re.findall(action_pattern, content, re.DOTALL | re.IGNORECASE) - + for action_name, action_code in action_matches: # Limpiar el código de la ACTION clean_code = action_code.strip() - + # Verificar si es código LAD o ST - if '_LD_BODY' in clean_code: + if "_LD_BODY" in clean_code: # Es código LAD - necesita parsing - print(f"ACTION LAD encontrada: {action_name} ({len(clean_code)} caracteres)") - self.actions[action_name] = self._parse_action_lad(action_name, clean_code) + print( + f"ACTION LAD encontrada: {action_name} ({len(clean_code)} caracteres)" + ) + self.actions[action_name] = self._parse_action_lad( + action_name, clean_code + ) else: # Es código ST - copiar directamente - print(f"ACTION ST encontrada: {action_name} ({len(clean_code)} caracteres)") - self.actions[action_name] = {'type': 'ST', 'code': clean_code} - + print( + f"ACTION ST encontrada: {action_name} ({len(clean_code)} caracteres)" + ) + self.actions[action_name] = {"type": "ST", "code": clean_code} + print(f"Total ACTIONs: {len(self.actions)}") - + def _parse_action_lad(self, action_name, action_content): """Parsear una ACTION que contiene código LAD""" # Crear un convertidor temporal para esta ACTION action_converter = SimpleLadConverter() action_converter.symbol_manager = self.symbol_manager # Compartir símbolos - + # Encontrar sección LAD - lad_start = action_content.find('_LD_BODY') + lad_start = action_content.find("_LD_BODY") if lad_start != -1: # Extraer contenido LAD hasta el final lad_content = action_content[lad_start:] - lines = lad_content.split('\n') + lines = lad_content.split("\n") action_converter._parse_networks(lines) - + return { - 'type': 'LAD', - 'networks': action_converter.networks, - 'raw_content': action_content + "type": "LAD", + "networks": action_converter.networks, + "raw_content": action_content, } - + def _extract_st_code(self, content): """Extraer código ST del programa principal""" # Buscar desde después de END_VAR hasta ACTION o END_PROGRAM - var_end = content.rfind('END_VAR') + var_end = content.rfind("END_VAR") if var_end == -1: return - + # Buscar el final del código principal - action_start = content.find('\nACTION', var_end) - end_program = content.find('\nEND_PROGRAM', var_end) - + action_start = content.find("\nACTION", var_end) + end_program = content.find("\nEND_PROGRAM", var_end) + code_end = action_start if action_start != -1 else end_program if code_end == -1: code_end = len(content) - + # Extraer código ST - st_code = content[var_end + 7:code_end].strip() # +7 para saltar "END_VAR" - + st_code = content[var_end + 7 : code_end].strip() # +7 para saltar "END_VAR" + if st_code: # Almacenar código ST para usar en la generación self.st_main_code = st_code print(f"Código ST principal extraído: {len(st_code)} caracteres") else: self.st_main_code = None - + def _extract_function_return_type(self): """Extraer el tipo de retorno de una FUNCTION desde el header""" - if not hasattr(self, '_original_content'): + if not hasattr(self, "_original_content"): return None - + # Buscar patrón FUNCTION nombre : TIPO - match = re.search(r'FUNCTION\s+\w+\s*:\s*(\w+)', self._original_content) + match = re.search(r"FUNCTION\s+\w+\s*:\s*(\w+)", self._original_content) if match: return match.group(1) return None - + def _extract_type_content(self, content): """Extraer el contenido completo de un TYPE""" # Buscar desde TYPE hasta END_TYPE - type_start = content.find('TYPE') + type_start = content.find("TYPE") if type_start == -1: return - + # Buscar END_TYPE - type_end = content.find('END_TYPE', type_start) + type_end = content.find("END_TYPE", type_start) if type_end == -1: type_end = len(content) else: - type_end += len('END_TYPE') - + type_end += len("END_TYPE") + # Extraer contenido del TYPE type_content = content[type_start:type_end].strip() - + if type_content: self.type_content = type_content print(f"Contenido TYPE extraído: {len(type_content)} caracteres") else: self.type_content = None - + def _convert_type_to_scl(self, output): """Convertir TYPE a SCL - simplemente copiar el contenido original""" if self.type_content: # Copiar directamente el contenido del TYPE preservando formato - for line in self.type_content.split('\n'): + for line in self.type_content.split("\n"): if line.strip(): output.append(line) else: @@ -1324,69 +1415,74 @@ class SimpleLadConverter: output.append(" Dummy : INT;") output.append("END_STRUCT") output.append("END_TYPE") - - return '\n'.join(output) - + + return "\n".join(output) + def _extract_global_variables(self, content): """Extraer todas las secciones de variables globales""" # Buscar todas las secciones VAR_GLOBAL var_global_patterns = [ - (r'VAR_GLOBAL\s+PERSISTENT(.*?)END_VAR', 'VAR_GLOBAL PERSISTENT'), - (r'VAR_GLOBAL((?:(?!PERSISTENT)(?!END_VAR).)*?)END_VAR', 'VAR_GLOBAL'), + (r"VAR_GLOBAL\s+PERSISTENT(.*?)END_VAR", "VAR_GLOBAL PERSISTENT"), + (r"VAR_GLOBAL((?:(?!PERSISTENT)(?!END_VAR).)*?)END_VAR", "VAR_GLOBAL"), ] - + for pattern, var_type in var_global_patterns: matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) if matches: variables = [] for match in matches: # Parsear cada línea de variable - for line in match.split('\n'): + for line in match.split("\n"): line = line.strip() - if (line and not line.startswith('(*') and - not line.startswith('(************') and - ':' in line and not line.startswith('*')): + if ( + line + and not line.startswith("(*") + and not line.startswith("(************") + and ":" in line + and not line.startswith("*") + ): # Limpiar comentarios inline - if '(*' in line: + if "(*" in line: # Mantener el comentario inline pass # No limpiar comentarios para preservar información variables.append(line.strip()) - + if variables: if var_type not in self.var_sections: self.var_sections[var_type] = [] self.var_sections[var_type].extend(variables) - + # Mostrar resumen for var_type, variables in self.var_sections.items(): print(f"Variables {var_type}: {len(variables)} encontradas") - + def _convert_global_vars_to_scl(self, output): """Convertir archivo de variables globales a SCL""" # Para archivos de variables globales, simplemente copiar las declaraciones - + # Agregar secciones de variables encontradas - section_order = ['VAR_GLOBAL', 'VAR_GLOBAL PERSISTENT'] - + section_order = ["VAR_GLOBAL", "VAR_GLOBAL PERSISTENT"] + for section_type in section_order: if section_type in self.var_sections: output.append(f"{section_type}") for var_line in self.var_sections[section_type]: # Asegurar que termine con punto y coma - if not var_line.endswith(';'): - var_line += ' ;' + if not var_line.endswith(";"): + var_line += " ;" output.append(f" {var_line}") output.append("END_VAR") output.append("") - + # Si no se encontraron variables, agregar estructura básica if not self.var_sections: output.append("VAR_GLOBAL") output.append(" (* No se detectaron variables globales *)") output.append(" Dummy : INT ;") output.append("END_VAR") - - return '\n'.join(output) + + return "\n".join(output) + def main(): """Función principal - Convierte todos los archivos .EXP a .SCL""" @@ -1399,13 +1495,15 @@ def main(): print(f"Archivo: {debug_file}") else: print("=== Convertidor Masivo LAD a SCL con SymPy ===") - + # Cargar configuración configs = load_configuration() - + # Verificar que se cargó correctamente if not configs: - print("Advertencia: No se pudo cargar la configuración, usando valores por defecto") + print( + "Advertencia: No se pudo cargar la configuración, usando valores por defecto" + ) working_directory = "./" scl_output_dir = "TwinCat/scl" debug_mode = True @@ -1419,7 +1517,7 @@ def main(): level1_config = configs.get("level1", {}) level2_config = configs.get("level2", {}) level3_config = configs.get("level3", {}) - + # Parámetros de configuración debug_mode = level1_config.get("debug_mode", True) show_optimizations = level1_config.get("show_optimizations", True) @@ -1429,34 +1527,45 @@ def main(): max_display_lines = level2_config.get("max_display_lines", 50) sympy_optimization = level3_config.get("sympy_optimization", True) group_analysis = level3_config.get("group_analysis", True) - force_regenerate = level2_config.get("force_regenerate", False) # Nueva opción - + force_regenerate = level2_config.get( + "force_regenerate", False + ) # Nueva opción + + # Directorio de entrada para archivos .EXP (de level3 o working_directory como fallback) + input_directory = level3_config.get("twincat_exp_directory", working_directory) + # Verificar directorio de trabajo if not os.path.exists(working_directory): print(f"Error: El directorio de trabajo no existe: {working_directory}") return - + + # Verificar directorio de entrada + if not os.path.exists(input_directory): + print(f"Error: El directorio de entrada no existe: {input_directory}") + return + # Crear directorio de salida SCL full_scl_path = os.path.join(working_directory, scl_output_dir) if not os.path.exists(full_scl_path): os.makedirs(full_scl_path) print(f"Directorio creado: {full_scl_path}") - + # Determinar archivos a procesar if debug_file: # Modo debug - archivo específico - if not debug_file.endswith('.EXP'): - debug_file += '.EXP' - - debug_file_path = os.path.join(working_directory, debug_file) + if not debug_file.endswith(".EXP"): + debug_file += ".EXP" + + debug_file_path = os.path.join(input_directory, debug_file) if not os.path.exists(debug_file_path): print(f"Error: No se encontró el archivo {debug_file_path}") return - + exp_files = [debug_file_path] print(f"Procesando archivo específico: {debug_file}") + print(f"Directorio de entrada: {input_directory}") print(f"Directorio de salida SCL: {full_scl_path}") - + # En modo debug, forzar regeneración y mostrar más información force_regenerate = True debug_mode = True @@ -1464,103 +1573,112 @@ def main(): max_display_lines = 100 else: # Modo normal - todos los archivos - exp_pattern = os.path.join(working_directory, "*.EXP") + exp_pattern = os.path.join(input_directory, "*.EXP") exp_files = glob.glob(exp_pattern) - + if not exp_files: - print(f"No se encontraron archivos .EXP en: {working_directory}") + print(f"No se encontraron archivos .EXP en: {input_directory}") return - - print(f"Encontrados {len(exp_files)} archivos .EXP en: {working_directory}") + + print(f"Encontrados {len(exp_files)} archivos .EXP en: {input_directory}") print(f"Directorio de salida SCL: {full_scl_path}") - + print() - + # Procesar cada archivo successful_conversions = 0 failed_conversions = 0 - + for exp_file in exp_files: filename = os.path.basename(exp_file) base_name = os.path.splitext(filename)[0] scl_filename = f"{base_name}.scl" scl_output_path = os.path.join(full_scl_path, scl_filename) - + # Verificar si ya existe el archivo SCL (exportación progresiva) if os.path.exists(scl_output_path) and not force_regenerate: print(f"{'='*60}") print(f"SALTANDO: {filename} - Ya existe {scl_filename}") - print(f" (usa force_regenerate: true en configuración para forzar regeneración)") + print( + f" (usa force_regenerate: true en configuración para forzar regeneración)" + ) successful_conversions += 1 # Contar como exitoso continue - + print(f"{'='*60}") print(f"Procesando: {filename}") print(f"Salida: {scl_filename}") - + try: # Crear nuevo convertidor para cada archivo converter = SimpleLadConverter() - + # Parsear archivo converter.parse_file(exp_file) - + print(f" ✓ Redes encontradas: {len(converter.networks)}") - print(f" ✓ Secciones de variables: {list(converter.var_sections.keys())}") + print( + f" ✓ Secciones de variables: {list(converter.var_sections.keys())}" + ) print(f" ✓ ACTIONs encontradas: {list(converter.actions.keys())}") - + # Mostrar información de debug si está habilitado if debug_mode: converter.print_debug_info() - + # Optimizar expresiones con SymPy si está habilitado if sympy_optimization and show_optimizations: converter.optimize_expressions() - + # Analizar agrupación de condiciones si está habilitado if group_analysis and show_optimizations: converter.group_common_conditions() - + # Convertir y guardar print(f" Generando código SCL...") structured_code = converter.save_to_file(scl_output_path) - + # Mostrar parte del código generado si está habilitado if show_generated_code: - lines = structured_code.split('\n') + lines = structured_code.split("\n") display_lines = min(max_display_lines, len(lines)) - print(f" Código SCL generado ({len(lines)} líneas, mostrando {display_lines}):") + print( + f" Código SCL generado ({len(lines)} líneas, mostrando {display_lines}):" + ) for i in range(display_lines): print(f" {i+1:3d}: {lines[i]}") if len(lines) > display_lines: print(f" ... ({len(lines) - display_lines} líneas más)") - + print(f" ✓ Guardado en: {scl_output_path}") successful_conversions += 1 - + except Exception as e: print(f" ✗ Error procesando {filename}: {e}") if debug_mode: import traceback + traceback.print_exc() failed_conversions += 1 - + print() - + # Resumen final print(f"{'='*60}") print(f"RESUMEN DE CONVERSIÓN:") print(f" ✓ Exitosas: {successful_conversions}") print(f" ✗ Fallidas: {failed_conversions}") print(f" 📁 Directorio salida: {full_scl_path}") - + if successful_conversions > 0: print(f"\n✓ Conversión masiva completada!") - + except Exception as e: print(f"Error general: {e}") import traceback + traceback.print_exc() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/data/log.txt b/data/log.txt index efd823e..a8a9918 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,5 +1,265 @@ -[12:53:36] Iniciando ejecución de x1_lad_converter.py en C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis... -[12:53:36] === Convertidor Masivo LAD a SCL con SymPy === -[12:53:36] No se encontraron archivos .EXP en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis -[12:53:36] Ejecución de x1_lad_converter.py finalizada (success). Duración: 0:00:00.471141. -[12:53:36] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\TwinCat\log_x1_lad_converter.txt +[16:07:48] Iniciando ejecución de x6_full_io_documentation.py en C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis... +[16:07:48] 🚀 Iniciando documentación completa de IOs de TwinCAT +[16:07:48] ================================================================================ +[16:07:48] 📁 Directorio de trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis +[16:07:48] 📁 Directorio de resultados: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat +[16:07:48] 🔍 Escaneando definiciones TwinCAT activas en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat/scl +[16:07:48] ✅ Encontradas 141 definiciones de IO activas. +[16:07:48] 🔍 Buscando usos de variables definidas en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat/scl +[16:07:48] 📄 Analizando uso en: ADSVARREAD.scl +[16:07:48] 📄 Analizando uso en: ADSVARTRANSLATE.scl +[16:07:49] 📄 Analizando uso en: ADSVARWRITE.scl +[16:07:49] 📄 Analizando uso en: AMMONIACTRL.scl +[16:07:49] 📄 Analizando uso en: ARRAYTOREAL.scl +[16:07:49] 📄 Analizando uso en: BLENDERPROCEDURE_VARIABLES.scl +[16:07:49] 📄 Analizando uso en: BLENDERRINSE.scl +[16:07:49] 📄 Analizando uso en: BLENDER_PID_CTRL_LOOP.scl +[16:07:49] 📄 Analizando uso en: BLENDER_PROCEDURECALL.scl +[16:07:49] 📄 Analizando uso en: BLENDER_RUNCONTROL.scl +[16:07:49] 📄 Analizando uso en: BLENDER_VARIABLES.scl +[16:07:49] 📄 Analizando uso en: BLENDFILLRECSTRUCT.scl +[16:07:49] 📄 Analizando uso en: BLENDFILLSENDSTRUCT.scl +[16:07:49] 📄 Analizando uso en: BLENDFILLSYSTEM_STARTUP.scl +[16:07:49] 📄 Analizando uso en: BRIXTRACKING.scl +[16:07:49] 📄 Analizando uso en: BYTES_TO_DWORD.scl +[16:07:49] 📄 Analizando uso en: BYTES_TO_WORD.scl +[16:07:49] 📄 Analizando uso en: CALC_INJPRESS.scl +[16:07:49] 📄 Analizando uso en: CARBOWATERLINE.scl +[16:07:50] 📄 Analizando uso en: CENTRALCIP_CTRL.scl +[16:07:50] 📄 Analizando uso en: CETRIFUGAL_HEAD.scl +[16:07:50] 📄 Analizando uso en: CIPRECEIVESTRUCT.scl +[16:07:50] 📄 Analizando uso en: CIPSENDSTRUCT.scl +[16:07:50] 📄 Analizando uso en: CIP_CVQ.scl +[16:07:50] 📄 Analizando uso en: CIP_LINK_TYPE.scl +[16:07:50] 📄 Analizando uso en: CIP_LIST_ELEMENT.scl +[16:07:50] 📄 Analizando uso en: CIP_MAIN.scl +[16:07:50] 📄 Analizando uso en: CIP_PROGRAM_VARIABLES.scl +[16:07:50] 📄 Analizando uso en: CIP_SIMPLE_TYPE.scl +[16:07:50] 📄 Analizando uso en: CIP_STEP_TYPE.scl +[16:07:50] 📄 Analizando uso en: CIP_WAITEVENT_TYPE.scl +[16:07:50] 📄 Analizando uso en: CLEANBOOLARRAY.scl +[16:07:50] 📄 Analizando uso en: CLOCK_SIGNAL.scl +[16:07:50] 📄 Analizando uso en: CLOCK_VARIABLES.scl +[16:07:50] 📄 Analizando uso en: CO2EQPRESS.scl +[16:07:50] 📄 Analizando uso en: CO2INJPRESSURE.scl +[16:07:50] 📄 Analizando uso en: CO2_SOLUBILITY.scl +[16:07:50] 📄 Analizando uso en: CONVERTREAL.scl +[16:07:50] 📄 Analizando uso en: CVQ_0_6_PERC.scl +[16:07:50] 📄 Analizando uso en: CVQ_1P7_8_PERC.scl +[16:07:50] 📄 Analizando uso en: DATA_FROM_CIP.scl +[16:07:50] 📄 Analizando uso en: DATA_TO_CIP.scl +[16:07:50] 📄 Analizando uso en: DEAIRCO2TEMPCOMP.scl +[16:07:50] 📄 Analizando uso en: DEAIREATIONVALVE.scl +[16:07:50] 📄 Analizando uso en: DEAIREATOR_STARTUP.scl +[16:07:50] 📄 Analizando uso en: DELAY.scl +[16:07:50] 📄 Analizando uso en: DELTAP.scl +[16:07:50] 📄 Analizando uso en: DENSIMETER_CALIBRATION.scl +[16:07:50] 📄 Analizando uso en: DERIVE.scl +[16:07:50] 📄 Analizando uso en: DEVICENET_VARIABLES.scl +[16:07:50] 📄 Analizando uso en: DWORD_TO_BYTES.scl +[16:07:50] 📄 Analizando uso en: EXEC_SIMPLE_CIP.scl +[16:07:50] 📄 Analizando uso en: FASTRINSE.scl +[16:07:50] 📄 Analizando uso en: FB41_PIDCONTROLLER.scl +[16:07:50] 📄 Analizando uso en: FC_CONTROL_WORD.scl +[16:07:50] 📄 Analizando uso en: FC_STATUS_WORD.scl +[16:07:50] 📄 Analizando uso en: FEEDFORWARD.scl +[16:07:50] 📄 Analizando uso en: FILLERHEAD.scl +[16:07:50] 📄 Analizando uso en: FILLERRECEIVESTRUCT.scl +[16:07:50] 📄 Analizando uso en: FILLERRINSE.scl +[16:07:50] 📄 Analizando uso en: FILLERRINSETANK_CTRL.scl +[16:07:50] 📄 Analizando uso en: FILLERSENDSTRUCT.scl +[16:07:50] 📄 Analizando uso en: FILLER_CONTROL.scl +[16:07:50] 📄 Analizando uso en: FILLINGTIME.scl +[16:07:50] 📄 Analizando uso en: FIRSTPRODUCTION.scl +[16:07:51] 📄 Analizando uso en: FLOW_TO_PRESS_LOSS.scl +[16:07:51] 📄 Analizando uso en: FREQ_TO_MMH2O.scl +[16:07:51] 📄 Analizando uso en: FRICTIONLOSS.scl +[16:07:51] 📄 Analizando uso en: GETPRODBRIXCO2_FROMANALOGINPUT.scl +[16:07:51] 📄 Analizando uso en: GETPRODO2_FROMANALOGINPUT.scl +[16:07:51] 📄 Analizando uso en: GLOBAL_ALARMS.scl +[16:07:51] 📄 Analizando uso en: GLOBAL_VARIABLES_IN_OUT.scl +[16:07:51] 📄 Analizando uso en: HMI_ALARMS.scl +[16:07:51] 📄 Analizando uso en: HMI_BLENDER_PARAMETERS.scl +[16:07:51] 📄 Analizando uso en: HMI_IO_SHOWING.scl +[16:07:51] 📄 Analizando uso en: HMI_LOCAL_CIP_VARIABLES.scl +[16:07:51] 📄 Analizando uso en: HMI_SERVICE.scl +[16:07:51] 📄 Analizando uso en: HMI_VARIABLES_CMD.scl +[16:07:51] 📄 Analizando uso en: HMI_VARIABLES_STATUS.scl +[16:07:51] 📄 Analizando uso en: INPUT.scl +[16:07:51] 📄 Analizando uso en: INPUT_CIP_SIGNALS.scl +[16:07:51] 📄 Analizando uso en: INPUT_SIGNAL.scl +[16:07:51] 📄 Analizando uso en: INTEGRAL.scl +[16:07:51] 📄 Analizando uso en: LOCALCIP_CTRL.scl +[16:07:51] 📄 Analizando uso en: LOWPASSFILTER.scl +[16:07:51] 📄 Analizando uso en: LOWPASSFILTEROPT.scl +[16:07:51] 📄 Analizando uso en: MASELLI.scl +[16:07:51] 📄 Analizando uso en: MASELLIOPTO_TYPE.scl +[16:07:51] 📄 Analizando uso en: MASELLIUC05_TYPE.scl +[16:07:51] 📄 Analizando uso en: MASELLIUR22_TYPE.scl +[16:07:51] 📄 Analizando uso en: MASELLI_CONTROL.scl +[16:07:51] 📄 Analizando uso en: MAXCARBOCO2_VOL.scl +[16:07:51] 📄 Analizando uso en: MESSAGESCROLL.scl +[16:07:51] 📄 Analizando uso en: MESSAGE_SCROLL.scl +[16:07:51] 📄 Analizando uso en: MFMANALOG_VALUES.scl +[16:07:51] 📄 Analizando uso en: MFM_REAL_STRUCT.scl +[16:07:51] 📄 Analizando uso en: MMH2O_TO_FREQ.scl +[16:07:51] 📄 Analizando uso en: MODVALVEFAULT.scl +[16:07:51] 📄 Analizando uso en: MOVEARRAY.scl +[16:07:51] 📄 Analizando uso en: MPDS1000.scl +[16:07:51] 📄 Analizando uso en: MPDS1000_CONTROL.scl +[16:07:52] 📄 Analizando uso en: MPDS1000_TYPE.scl +[16:07:52] 📄 Analizando uso en: MPDS2000.scl +[16:07:52] 📄 Analizando uso en: MPDS2000_CONTROL.scl +[16:07:52] 📄 Analizando uso en: MPDS2000_TYPE.scl +[16:07:52] 📄 Analizando uso en: MPDS_PA_CONTROL.scl +[16:07:52] 📄 Analizando uso en: MSE_SLOPE.scl +[16:07:52] 📄 Analizando uso en: MYVAR.scl +[16:07:52] 📄 Analizando uso en: OR_ARRAYBOOL.scl +[16:07:52] 📄 Analizando uso en: OUTPUT.scl +[16:07:52] 📄 Analizando uso en: PARAMETERNAMETYPE.scl +[16:07:52] 📄 Analizando uso en: PA_MPDS.scl +[16:07:52] 📄 Analizando uso en: PERIPHERIAL.scl +[16:07:52] 📄 Analizando uso en: PID_VARIABLES.scl +[16:07:52] 📄 Analizando uso en: PLC CONFIGURATION.scl +[16:07:52] 📄 Analizando uso en: PNEUMATIC_VALVE_CTRL.scl +[16:07:52] 📄 Analizando uso en: PPM_O2.scl +[16:07:52] 📄 Analizando uso en: PRODBRIXRECOVERY.scl +[16:07:52] 📄 Analizando uso en: PRODTANK_DRAIN.scl +[16:07:52] 📄 Analizando uso en: PRODTANK_RUNOUT.scl +[16:07:52] 📄 Analizando uso en: PRODUCTAVAILABLE.scl +[16:07:52] 📄 Analizando uso en: PRODUCTION_VARIABLES.scl +[16:07:52] 📄 Analizando uso en: PRODUCTLITERINTANK.scl +[16:07:52] 📄 Analizando uso en: PRODUCTPIPEDRAIN.scl +[16:07:52] 📄 Analizando uso en: PRODUCTPIPERUNOUT.scl +[16:07:52] 📄 Analizando uso en: PRODUCTQUALITY.scl +[16:07:53] 📄 Analizando uso en: PRODUCTTANKBRIX.scl +[16:07:53] 📄 Analizando uso en: PRODUCTTANK_PRESSCTRL.scl +[16:07:53] 📄 Analizando uso en: PROFIBUS_DATA.scl +[16:07:53] 📄 Analizando uso en: PROFIBUS_NETWORK.scl +[16:07:53] 📄 Analizando uso en: PROFIBUS_VARIABLES.scl +[16:07:53] 📄 Analizando uso en: PULSEPRESSURE.scl +[16:07:53] 📄 Analizando uso en: PUMPSCONTROL.scl +[16:07:53] 📄 Analizando uso en: READANALOGIN.scl +[16:07:53] 📄 Analizando uso en: READPERIPHERIAL.scl +[16:07:53] 📄 Analizando uso en: SAFETIES.scl +[16:07:53] 📄 Analizando uso en: SELCHECKBRIXSOURCE.scl +[16:07:53] 📄 Analizando uso en: SIGNALS_INTEFACE.scl +[16:07:53] 📄 Analizando uso en: SIGNAL_GEN.scl +[16:07:53] 📄 Analizando uso en: SINUSOIDAL_SIGNAL.scl +[16:07:53] 📄 Analizando uso en: SLEWLIMIT.scl +[16:07:53] 📄 Analizando uso en: SLIM_BLOCK.scl +[16:07:53] 📄 Analizando uso en: SLIM_VARIABLES.scl +[16:07:53] 📄 Analizando uso en: SOFTNET_VARIABLES.scl +[16:07:53] 📄 Analizando uso en: SPEEDADJUST.scl +[16:07:53] 📄 Analizando uso en: SP_AND_P_VARIABLES.scl +[16:07:53] 📄 Analizando uso en: STANDARD.LIB_5.6.98 09_39_02.scl +[16:07:53] 📄 Analizando uso en: STATISTICALANALISYS.scl +[16:07:53] 📄 Analizando uso en: SYRBRIX_AUTOCORRECTION.scl +[16:07:53] 📄 Analizando uso en: SYRUPDENSITY.scl +[16:07:53] 📄 Analizando uso en: SYRUPROOMCTRL.scl +[16:07:53] 📄 Analizando uso en: SYRUP_LINE_MFM_PREP.scl +[16:07:53] 📄 Analizando uso en: SYRUP_MFM_STARTUP.scl +[16:07:53] 📄 Analizando uso en: SYRUP_RUNOUT.scl +[16:07:53] 📄 Analizando uso en: SYSTEMRUNOUT_VARIABLES.scl +[16:07:53] 📄 Analizando uso en: SYSTEM_DATAS.scl +[16:07:53] 📄 Analizando uso en: SYSTEM_RUN_OUT.scl +[16:07:53] 📄 Analizando uso en: TANKLEVEL.scl +[16:07:53] 📄 Analizando uso en: TANKLEVELTOHEIGHT.scl +[16:07:53] 📄 Analizando uso en: TASK CONFIGURATION.scl +[16:07:53] 📄 Analizando uso en: TCPLCUTILITIES.LIB_11.12.01 09_39_02.scl +[16:07:53] 📄 Analizando uso en: TCSYSTEM.LIB_16.9.02 09_39_02.scl +[16:07:53] 📄 Analizando uso en: TESTFLOWMETERS.scl +[16:07:54] 📄 Analizando uso en: UDP_STRUCT.scl +[16:07:54] 📄 Analizando uso en: UV_LAMP.scl +[16:07:54] 📄 Analizando uso en: VACUUMCTRL.scl +[16:07:54] 📄 Analizando uso en: VALVEFAULT.scl +[16:07:54] 📄 Analizando uso en: VALVEFLOW.scl +[16:07:54] 📄 Analizando uso en: VARIABLE_CONFIGURATION.scl +[16:07:54] 📄 Analizando uso en: VOID.scl +[16:07:54] 📄 Analizando uso en: WATERDENSITY.scl +[16:07:54] 📄 Analizando uso en: WORD_TO_BYTES.scl +[16:07:54] 📄 Analizando uso en: WRITEPERIPHERIAL.scl +[16:07:54] 📄 Analizando uso en: _BLENDER_CTRL_MAIN.scl +[16:07:54] 📄 Analizando uso en: _BLENDER_PID_MAIN.scl +[16:07:54] 📄 Analizando uso en: _BOOLARRAY_TO_DWORD.scl +[16:07:54] 📄 Analizando uso en: _BOOLARRAY_TO_WORD.scl +[16:07:54] 📄 Analizando uso en: _DWORD_SWAP_BYTEARRAY.scl +[16:07:54] 📄 Analizando uso en: _DWORD_TO_BOOLARRAY.scl +[16:07:54] 📄 Analizando uso en: _FILLING_HEAD_PID_CTRL.scl +[16:07:54] 📄 Analizando uso en: _PUMPCONTROL.scl +[16:07:54] 📄 Analizando uso en: _STEPMOVE.scl +[16:07:54] 📄 Analizando uso en: _WORD_TO_BOOLARRAY.scl +[16:07:54] ✅ Encontrados 196 usos para 61 variables distintas. +[16:07:54] 📄 Generando tabla resumen: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\TwinCAT_Full_IO_List.md +[16:07:54] ✅ Tabla resumen generada exitosamente. +[16:07:54] 📄 Generando reporte de snippets: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\TwinCAT_IO_Usage_Snippets.md +[16:07:54] Generando snippets para 61 variables con uso... +[16:07:54] 📝 Procesando 1/61: gProduct_VFC_StatusWord (2 usos) +[16:07:54] 📝 Procesando 2/61: DI_AuxVoltage_On (1 usos) +[16:07:54] 📝 Procesando 3/61: DI_Reset_Horn_Btn (2 usos) +[16:07:54] 📝 Procesando 4/61: DI_Reset_Btn (77 usos) +[16:07:54] 📝 Procesando 5/61: DI_Blender_Stop_Btn (3 usos) +[16:07:54] 📝 Procesando 6/61: DI_Blender_Start_Btn (1 usos) +[16:07:54] 📝 Procesando 7/61: DI_PowerSuppliesOk (2 usos) +[16:07:54] 📝 Procesando 8/61: DI_Min_Deair_Level (1 usos) +[16:07:54] 📝 Procesando 9/61: DI_ProdTankEmpty (1 usos) +[16:07:54] 📝 Procesando 10/61: DI_BatteryNotReady (1 usos) +[16:07:54] 📝 Procesando 11/61: DI_VM1_Water_Valve_Closed (1 usos) +[16:07:54] 📝 Procesando 12/61: DI_VM2_Syrup_Valve_Closed (1 usos) +[16:07:54] 📝 Procesando 13/61: DI_VM3_CO2_Valve_Closed (1 usos) +[16:07:54] 📝 Procesando 14/61: DI_Product_Pump_Contactor (1 usos) +[16:07:54] 📝 Procesando 15/61: DI_SyrRoom_Pump_Ready (1 usos) +[16:07:54] 📝 Procesando 16/61: DI_CIP_CIPMode (1 usos) +[16:07:54] 📝 Procesando 17/61: DI_CIP_RinseMode (1 usos) +[16:07:54] 📝 Procesando 18/61: DI_CIP_DrainRequest (1 usos) +[16:07:54] 📝 Procesando 19/61: DI_CIP_CIPCompleted (1 usos) +[16:07:54] 📝 Procesando 20/61: DI_Air_InletPress_OK (1 usos) +[16:07:54] 📝 Procesando 21/61: DI_Syrup_Line_Drain_Sensor (1 usos) +[16:07:54] 📝 Procesando 22/61: gWaterTotCtrl_Node20 (3 usos) +[16:07:54] 📝 Procesando 23/61: gSyrControl_Node21 (7 usos) +[16:07:54] 📝 Procesando 24/61: gCO2Control_Node22 (7 usos) +[16:07:54] 📝 Procesando 25/61: gProductTotCtrl_Node17 (3 usos) +[16:07:54] 📝 Procesando 26/61: gProduct_VFC_ControlWord (2 usos) +[16:07:54] 📝 Procesando 27/61: gProduct_VFC_MainRefValue (2 usos) +[16:07:54] 📝 Procesando 28/61: DO_HoldBrixMeter (2 usos) +[16:07:54] 📝 Procesando 29/61: DO_SyrupRoomPump_Run (2 usos) +[16:07:54] 📝 Procesando 30/61: DO_SyrupRoomWaterReq (2 usos) +[16:07:54] 📝 Procesando 31/61: DO_CIP_CIPRequest (2 usos) +[16:07:54] 📝 Procesando 32/61: DO_CIP_DrainCompleted (2 usos) +[16:07:54] 📝 Procesando 33/61: DO_Horn (2 usos) +[16:07:54] 📝 Procesando 34/61: DO_Blender_Run_Lamp (2 usos) +[16:07:54] 📝 Procesando 35/61: DO_Alarm_Lamp (2 usos) +[16:07:54] 📝 Procesando 36/61: DO_RotorAlarm_Lamp (2 usos) +[16:07:54] 📝 Procesando 37/61: DO_Water_Pump_Run (2 usos) +[16:07:54] 📝 Procesando 38/61: DO_Syrup_Pump_Run (2 usos) +[16:07:54] 📝 Procesando 39/61: DO_Product_Pump_Run (3 usos) +[16:07:54] 📝 Procesando 40/61: DO_EV11_BlowOff_Valve (2 usos) +[16:07:54] 📝 Procesando 41/61: DO_EV13_Prod_Recirc_Valve (2 usos) +[16:07:54] 📝 Procesando 42/61: DO_EV14_DeairDrain_Valve (2 usos) +[16:07:54] 📝 Procesando 43/61: DO_EV15_ProductTank_Drain_Valve (2 usos) +[16:07:54] 📝 Procesando 44/61: DO_EV17_BufferTankSprayBall_Valve (2 usos) +[16:07:54] 📝 Procesando 45/61: DO_EV18_DeairOverfill_Valve (2 usos) +[16:07:54] 📝 Procesando 46/61: DO_EV21_ProdTankOverfill_Valve (2 usos) +[16:07:54] 📝 Procesando 47/61: DO_EV22_WaterPumpPrime_Valve (2 usos) +[16:07:54] 📝 Procesando 48/61: DO_EV23_SerpentineDrain_valve (2 usos) +[16:07:54] 📝 Procesando 49/61: DO_EV24_SyrupRecirc_Valve (2 usos) +[16:07:54] 📝 Procesando 50/61: DO_EV26_CO2InjShutOff_Valve (2 usos) +[16:07:54] 📝 Procesando 51/61: DO_EV27_DeairSprayBall_Valve (2 usos) +[16:07:54] 📝 Procesando 52/61: DO_EV28_DeairStartCO2Inj_Valve (2 usos) +[16:07:54] 📝 Procesando 53/61: DO_EV44_SyrupLineDrain (2 usos) +[16:07:54] 📝 Procesando 54/61: DO_EV45_ProductChillerDrain (2 usos) +[16:07:54] 📝 Procesando 55/61: DO_EV61_SyrupTankSprayBall (2 usos) +[16:07:54] 📝 Procesando 56/61: DO_EV62_ProductOutlet (3 usos) +[16:07:54] 📝 Procesando 57/61: DO_EV69_Blender_ProductPipeDrain (2 usos) +[16:07:54] 📝 Procesando 58/61: DO_EV81_Prod_Recirc_Chiller_Valve (2 usos) +[16:07:54] 📝 Procesando 59/61: DO_EV01_Deair_Lvl_Ctrl_Valve (2 usos) +[16:07:54] 📝 Procesando 60/61: DO_EV02_Deair_FillUp_Valve (2 usos) +[16:07:54] 📝 Procesando 61/61: gPAmPDSInlinePumpStop (2 usos) +[16:07:54] Generando tabla para 80 variables no usadas... +[16:07:54] ✅ Reporte de snippets generado exitosamente. +[16:07:54] 🎉 Análisis completado exitosamente! +[16:07:54] 📁 Archivos generados en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat +[16:07:54] 📄 TwinCAT_Full_IO_List.md +[16:07:54] 📄 TwinCAT_IO_Usage_Snippets.md +[16:07:54] Ejecución de x6_full_io_documentation.py finalizada (success). Duración: 0:00:06.496612. +[16:07:54] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\TwinCat\log_x6_full_io_documentation.txt