Simatic_XML_Parser_to_SCL/ToUpload/parsers/parse_stl.py

278 lines
13 KiB
Python

# 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 <Text> 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 <Text>
def reconstruct_stl_from_statementlist(statement_list_node):
"""Reconstruye el código STL como una cadena de texto desde <StatementList>."""
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
}