# ToUpload/parsers/parse_stl.py # -*- coding: utf-8 -*- from lxml import etree # Importar desde las utilidades del parser from .parser_utils import ns # Solo necesitamos los namespaces aquí # --- Funciones Auxiliares de Reconstrucción STL (Adaptadas de x1) --- def get_access_text_stl(access_element): """Reconstruye una representación textual simple de un Access en STL.""" if access_element is None: return "_ERR_ACCESS_" scope = access_element.get("Scope") # Símbolo (Variable, Constante Simbólica) symbol_elem = access_element.xpath("./stl:Symbol", namespaces=ns) if symbol_elem: components = symbol_elem[0].xpath("./stl:Component", namespaces=ns) parts = [] for i, comp in enumerate(components): name = comp.get("Name", "_ERR_COMP_") # Comprobar HasQuotes (en Access padre?) has_quotes_elem = comp.xpath("ancestor::stl:Access/stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true" is_temp = name.startswith("#") if i > 0: parts.append(".") # Aplicar comillas if has_quotes or (i == 0 and not is_temp and '"' not in name): parts.append(f'"{name}"') else: parts.append(name) # Índices de Array index_access = comp.xpath("./stl:Access", namespaces=ns) if index_access: indices = [get_access_text_stl(ia) for ia in index_access] parts.append(f"[{','.join(indices)}]") return "".join(parts) # Constante Literal constant_elem = access_element.xpath("./stl:Constant", namespaces=ns) if constant_elem: val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns) type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns) const_type = (type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else "") const_val = (val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONST_") # Añadir prefijos estándar STL if const_type == "time": return f"T#{const_val}" if const_type == "s5time": return f"S5T#{const_val}" if const_type == "date": return f"D#{const_val}" if const_type == "dt": return f"DT#{const_val}" if const_type == "time_of_day" or const_type=="tod": return f"TOD#{const_val}" if const_type.lower() == "string": replaced_val = const_val.replace("'", "''") access_str = f"'{replaced_val}'" if const_type.lower() == "char": replaced_val = const_val.replace("'", "''") access_str = f"'{replaced_val}'" if const_type == "wstring": replaced_val = const_val.replace("'", "''") access_str = f"WSTRING#'{replaced_val}'" if const_type == "wchar": replaced_val = const_val.replace("'", "''") access_str = f"WCHAR#'{replaced_val}'" # Añadir más si es necesario (WSTRING#, BYTE#, WORD#...) if const_type == "byte" and const_val.startswith("16#"): return f"B#{const_val}" # Formato B#16#FF if const_type == "word" and const_val.startswith("16#"): return f"W#{const_val}" if const_type == "dword" and const_val.startswith("16#"): return f"DW#{const_val}" # Real con punto decimal if const_type == "real" and '.' not in const_val and 'e' not in const_val.lower(): return f"{const_val}.0" return const_val # Valor por defecto # Etiqueta label_elem = access_element.xpath("./stl:Label", namespaces=ns) if label_elem: return label_elem[0].get("Name", "_ERR_LABEL_") # Acceso Indirecto (Punteros) indirect_elem = access_element.xpath("./stl:Indirect", namespaces=ns) if indirect_elem: reg = indirect_elem[0].get("Register", "AR?") # AR1, AR2 offset_str = indirect_elem[0].get("BitOffset", "0") area = indirect_elem[0].get("Area", "DB") # DB, DI, L, etc. width = indirect_elem[0].get("Width", "X") # Bit, Byte, Word, Double try: bit_offset = int(offset_str) byte_offset = bit_offset // 8 bit_in_byte = bit_offset % 8 p_format_offset = f"P#{byte_offset}.{bit_in_byte}" except ValueError: p_format_offset = "P#?.?" width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D", "Long": "D"} width_char = width_map.get(width, width[0] if width else "?") return f"{area}{width_char}[{reg},{p_format_offset}]" # Dirección Absoluta (I, Q, M, PI, PQ, T, C, DBX, DIX, L) address_elem = access_element.xpath("./stl:Address", namespaces=ns) if address_elem: area = address_elem[0].get("Area", "??") # Input, Output, Memory, DB, DI, Local, Timer, Counter... bit_offset_str = address_elem[0].get("BitOffset", "0") addr_type_str = address_elem[0].get("Type", "Bool") # Bool, Byte, Word, DWord, Int, DInt, Real... try: bit_offset = int(bit_offset_str) byte_offset = bit_offset // 8 bit_in_byte = bit_offset % 8 # Determinar ancho (X, B, W, D) addr_width = "X" # Default bit if addr_type_str in ["Byte", "SInt", "USInt"]: addr_width = "B" elif addr_type_str in ["Word", "Int", "UInt"]: addr_width = "W" elif addr_type_str in ["DWord", "DInt", "UDInt", "Real", "Time", "DT", "TOD"]: addr_width = "D" elif addr_type_str in ["LReal", "LTime", "LWord", "LInt", "ULInt"]: addr_width = "D" # L se maneja como D en direccionamiento base? O usar L? Chequear estándar. STL clásico no tenía L. # Mapear Área XML a Área STL area_map = {"Input": "I", "Output": "Q", "Memory": "M", "PeripheryInput": "PI", "PeripheryOutput": "PQ", "DB": "DB", "DI": "DI", "Local": "L", "Timer": "T", "Counter": "C"} stl_area = area_map.get(area, area) if stl_area in ["DB", "DI"]: block_num = address_elem[0].get("BlockNumber") # Para DB10.DBX0.0 if block_num: return f"{stl_area}{block_num}.{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" else: # Para acceso con registro DB/DI (DBX, DIW, etc.) return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" elif stl_area in ["T", "C"]: return f"{stl_area}{byte_offset}" # T 5, C 10 (offset es el número) else: # I, Q, M, L, PI, PQ return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # M10.1, IW0, QB5, etc. except ValueError: return f"{area}?{bit_offset_str}?" # CallInfo (para CALL FC10, CALL FB20, DB10) call_info_elem = access_element.xpath("./stl:CallInfo", namespaces=ns) if call_info_elem: name = call_info_elem[0].get("Name", "_ERR_CALL_") btype = call_info_elem[0].get("BlockType", "FC") # FC, FB, DB instance_node = call_info_elem[0].xpath("./stl:Instance/stl:Component/@Name", namespaces=ns) if btype == "FB" and instance_node: # Para CALL FB, el operando es el DB de instancia db_name_raw = instance_node[0] return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw elif btype == "DB": return f'DB "{name}"' # O solo DB name? ej. DB10 else: # FC return f'{btype} "{name}"' # FC "Nombre" return f"_{scope}_?" # Fallback def get_comment_text_stl(comment_element): """Extrae texto de un LineComment o Comment para STL.""" if comment_element is None: return "" # STL Comments suelen tener directamente text_nodes = comment_element.xpath("./stl:Text/text()", namespaces=ns) if text_nodes: return text_nodes[0].strip() return "" # Vacío si no hay def reconstruct_stl_from_statementlist(statement_list_node): """Reconstruye el código STL como una cadena de texto desde .""" if statement_list_node is None: return "// Error: StatementList node not found.\n" stl_lines = [] statements = statement_list_node.xpath("./stl:StlStatement", namespaces=ns) for stmt in statements: line_parts = [] inline_comment = "" # Comentarios en la misma línea # 1. Comentarios iniciales (línea completa //) initial_comments = stmt.xpath("child::stl:Comment[not(@Inserted='true')] | child::stl:LineComment[not(@Inserted='true')]", namespaces=ns) for comm in initial_comments: comment_text = get_comment_text_stl(comm) if comment_text: for comment_line in comment_text.splitlines(): stl_lines.append(f"// {comment_line}") # 2. Etiqueta (Label) label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns) label_str = "" if label_decl: label_name = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns) if label_name: label_str = f"{label_name[0]}:" # Comentarios después de la etiqueta (inline) label_comments = label_decl[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns) for lcomm in label_comments: inline_comment += f" // {get_comment_text_stl(lcomm)}" if label_str: line_parts.append(label_str) # 3. Instrucción (StlToken) instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns) instruction_str = "" if instruction_token: token_text = instruction_token[0].get("Text", "_ERR_TOKEN_") if token_text == "EMPTY_LINE": stl_lines.append("") # Línea vacía continue # Saltar resto del statement elif token_text == "COMMENT": # Marcador de línea de comentario completo # Ya manejado por initial_comments? Verificar XML. Si no, extraer comentario aquí. pass # Asumir manejado antes else: instruction_str = token_text # Comentarios asociados al token (inline) token_comments = instruction_token[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns) for tcomm in token_comments: inline_comment += f" // {get_comment_text_stl(tcomm)}" if instruction_str: # Añadir tabulación si hay etiqueta line_parts.append("\t" + instruction_str if label_str else instruction_str) # 4. Operando (Access) access_elem = stmt.xpath("./stl:Access", namespaces=ns) access_str = "" if access_elem: access_text = get_access_text_stl(access_elem[0]) access_str = access_text # Comentarios dentro del Access (inline) access_comments = access_elem[0].xpath("child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']", namespaces=ns) for acc_comm in access_comments: inline_comment += f" // {get_comment_text_stl(acc_comm)}" if access_str: line_parts.append(access_str) # Construir línea final current_line = " ".join(lp for lp in line_parts if lp) # Unir partes con espacio if inline_comment: current_line += f"\t{inline_comment.strip()}" # Añadir comentario con tab if current_line.strip(): # Añadir solo si no está vacía después de todo stl_lines.append(current_line.rstrip()) # Quitar espacios finales return "\n".join(stl_lines) def parse_stl_network(network_element): """ Parsea una red STL extrayendo el código fuente reconstruido. Devuelve un diccionario representando la red para el JSON. """ network_id = network_element.get("ID", "UnknownSTL_ID") network_lang = "STL" # Buscar NetworkSource y luego StatementList network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns) statement_list_node = None if network_source_node: statement_list_node_list = network_source_node[0].xpath("./stl:StatementList", namespaces=ns) if statement_list_node_list: statement_list_node = statement_list_node_list[0] reconstructed_stl = "// STL extraction failed: StatementList node not found.\n" if statement_list_node is not None: reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node) # Crear la estructura de datos para la red parsed_network_data = { "id": network_id, "language": network_lang, "logic": [ # STL se guarda como un único bloque lógico { "instruction_uid": f"STL_{network_id}", # UID sintético "type": "RAW_STL_CHUNK", # Tipo especial para STL crudo "stl": reconstructed_stl, # El código STL reconstruido } ], } return parsed_network_data # --- Función de Información del Parser --- def get_parser_info(): """Devuelve la información para este parser.""" return { 'language': ['STL'], # Lenguaje soportado 'parser_func': parse_stl_network # Función a llamar }