#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado Versión mejorada con SymPy para optimización de expresiones lógicas """ import re import sympy from sympy import symbols, And, Or, Not, simplify from sympy.logic.boolalg import to_dnf 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) 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_path = "" self.var_sections = {} # VAR, VAR_INPUT, VAR_OUTPUT, etc. self.actions = {} # Diccionario de ACTIONs 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: content = f.read() # Extraer información del header self._parse_header_info(content) # Extraer declaraciones de variables self._parse_variable_declarations(content) # Encontrar sección LAD 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) 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') self._parse_networks(lines) else: print("No se encontró _LD_BODY") # 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) if path_match: self.program_path = path_match.group(1) # Buscar nombre del programa/function_block program_match = re.search(r'(PROGRAM|FUNCTION_BLOCK)\s+(\w+)', content) if program_match: 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'), ] 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'): line = line.strip() if line and not line.startswith('(*') and ':' in line: # Limpiar comentarios inline 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 while i < len(lines): if lines[i].strip() == '_NETWORK': self.current_network_id += 1 i = self._parse_network(lines, i) else: i += 1 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': [] } i = start_idx + 1 # Parse comentario if i < len(lines) and lines[i].strip() == '_COMMENT': i, comment = self._parse_comment(lines, i) network['comment'] = comment # Parse contenido de la red while i < len(lines): line = lines[i].strip() if line == '_NETWORK': break 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'): # Buscar variable de salida i += 1 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() i += 1 else: i += 1 self.networks.append(network) 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': return self._parse_and_expression(lines, i + 1) elif line == '_LD_OR': return self._parse_or_expression(lines, i + 1) elif line == '_LD_CONTACT': return self._parse_contact(lines, i + 1) elif line.startswith('_FUNCTIONBLOCK'): return self._parse_function_block(lines, i) 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'): # Extraer número de operandos operator_line = lines[i].strip() 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} 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'): # Extraer número de operandos operator_line = lines[i].strip() 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} 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': i += 1 # Verificar si está negado if i < len(lines): if lines[i].strip() == '_NEGATIV': negated = True i += 1 elif lines[i].strip() == '_POSITIV': i += 1 return i, {'type': 'CONTACT', 'name': contact_name, 'negated': negated} def _parse_function_block(self, lines, start_idx): """Parse un bloque de función""" i = start_idx + 1 fb_name = "" inputs = [] if i < len(lines): fb_name = lines[i].strip() i += 1 # Parse inputs del function block while i < len(lines) and not lines[i].strip().startswith('_OUTPUT'): line = lines[i].strip() if line.startswith('_OPERAND'): i += 2 # Saltar _EXPRESSION if i < len(lines): inputs.append(lines[i].strip()) i += 1 else: i += 1 return i, {'type': 'FUNCTION_BLOCK', 'name': fb_name, 'inputs': inputs} 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': break if line and not line.startswith('_'): comment_lines.append(line) i += 1 return i + 1, ' '.join(comment_lines) 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("") # Declaración del programa program_name = self.program_name if self.program_name else "ConvertedProgram" output.append(f"PROGRAM {program_name}") # Declaraciones de variables self._add_variable_sections(output) # Inicio del código principal output.append("") output.append("(* === CÓDIGO PRINCIPAL === *)") output.append("") # Lógica de las redes LAD for network in self.networks: output.append(f" // Red {network['id']}") if network['comment']: output.append(f" // {network['comment']}") if network['logic'] and network['target']: # Usar expresión DNF optimizada si está disponible 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']) 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: 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("") # Fin del programa principal output.append("END_PROGRAM") # Agregar ACTIONs como subfunciones self._add_actions_as_procedures(output, program_name) 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'] 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 += ' ;' 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_code in self.actions.items(): # Nombre completo como se usa en TwinCAT full_name = f"{program_name}_{action_name}" output.append(f"PROCEDURE {full_name}") output.append("(* Convertida desde ACTION *)") output.append("") # Agregar el código de la ACTION con indentación for line in action_code.split('\n'): if line.strip(): output.append(f" {line.strip()}") 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']: return f"NOT {logic['name']}" else: return logic['name'] elif logic['type'] == 'AND': operand_strings = [] 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': operand_strings = [] 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 "" return f"{logic['name']}({inputs_str})" 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: f.write(structured_code) print(f"Código guardado en: {output_path}") 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']: print(f" Comentario: {network['comment']}") print(f" Target: {network['target']}") if network['logic']: print(f" Lógica: {self._debug_logic_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 "" return f"{prefix}CONTACT: {logic['name']}{neg_str}" elif logic['type'] == 'AND': result = f"{prefix}AND:\n" for operand in logic['operands']: result += self._debug_logic_string(operand, indent + 1) + "\n" return result.rstrip() elif logic['type'] == 'OR': result = f"{prefix}OR:\n" for operand in logic['operands']: result += self._debug_logic_string(operand, indent + 1) + "\n" return result.rstrip() elif logic['type'] == 'FUNCTION_BLOCK': return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}" 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']: return Not(symbol) else: return symbol elif logic['type'] == 'AND': sympy_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': sympy_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': # Para function blocks, creamos un símbolo especial fb_name = f"{logic['name']}({', '.join(logic['inputs'])})" return self.symbol_manager.get_symbol(fb_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 ') # Limpiar espacios múltiples 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']: print(f"\nOptimizando Red {network['id']}:") # Convertir a SymPy 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}") # 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 except Exception as e: print(f" Error optimizando: {e}") 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): 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']] # 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}") # 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") except Exception as 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: # Convertir a conjuntos de símbolos para análisis básico 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: # Verificar si expr1 == NOT(expr2) simplificado complement = Not(expr2) simplified_complement = simplify(complement) simplified_expr1 = simplify(expr1) return simplified_expr1.equals(simplified_complement) except: return False 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}") # Reemplazar contradicción por False cleaned_expr = cleaned_expr.replace(contradiction, sympy.false) cleaned_expr = simplify(cleaned_expr) return cleaned_expr except: return expr def _format_dnf_for_lad(self, sympy_expr): """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 ') # Limpiar espacios múltiples 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('('): # 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(')')): 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 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 == '(': paren_level += 1 current_part += char elif char == ')': paren_level -= 1 current_part += char 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_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() self.actions[action_name] = clean_code print(f"ACTION encontrada: {action_name} ({len(clean_code)} caracteres)") print(f"Total ACTIONs: {len(self.actions)}") def main(): """Función principal""" converter = SimpleLadConverter() try: print("=== Convertidor LAD a SCL con SymPy ===") # Por ahora probar con SYRUPROOMCTRL que tiene variables y ACTIONs file_path = ".example/SYRUPROOMCTRL.EXP" output_name = "SYRUPROOMCTRL_scl" print(f"Parseando archivo {file_path}...") converter.parse_file(file_path) print(f"Redes encontradas: {len(converter.networks)}") print(f"Secciones de variables: {list(converter.var_sections.keys())}") print(f"ACTIONs encontradas: {list(converter.actions.keys())}") # Mostrar información de debug converter.print_debug_info() # NUEVO: Optimizar expresiones con SymPy converter.optimize_expressions() # NUEVO: Analizar agrupación de condiciones converter.group_common_conditions() # Convertir y guardar print("\nGenerando código SCL completo...") structured_code = converter.save_to_file(f"{output_name}.txt") # Mostrar el código generado lines = structured_code.split('\n') print(f"\nCódigo SCL generado ({len(lines)} líneas):") for i, line in enumerate(lines): print(f"{i+1:3d}: {line}") print(f"\n✓ Conversión SCL completada!") except Exception as e: print(f"Error: {e}") import traceback traceback.print_exc() if __name__ == "__main__": main()