278 lines
13 KiB
Python
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
|
|
} |