527 lines
22 KiB
Python
527 lines
22 KiB
Python
# ToUpload/parsers/parse_stl.py
|
|
# -*- coding: utf-8 -*-
|
|
from lxml import etree
|
|
import traceback
|
|
import re # Needed for substitutions in get_access_text_stl
|
|
|
|
# Importar desde las utilidades del parser
|
|
# ns y get_multilingual_text son necesarios
|
|
from .parser_utils import ns, get_multilingual_text
|
|
|
|
# --- Funciones Auxiliares de Reconstrucción STL ---
|
|
|
|
|
|
def get_access_text_stl(access_element):
|
|
"""
|
|
Reconstruye una representación textual simple de un Access en STL.
|
|
Intenta manejar los diferentes tipos de acceso definidos en el XSD.
|
|
"""
|
|
if access_element is None:
|
|
return "_ERR_ACCESS_"
|
|
|
|
# --- Símbolo (Variable, Constante Simbólica) ---
|
|
# Busca <Symbol> dentro del <Access> usando el namespace stl
|
|
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 (puede estar en el Access o Componente, priorizar Componente)
|
|
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true"
|
|
has_quotes_access = False
|
|
access_parent = comp.xpath("ancestor::stl:Access[1]", namespaces=ns)
|
|
if access_parent:
|
|
has_quotes_attr = access_parent[0].xpath(
|
|
"./stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns
|
|
)
|
|
has_quotes_access = (
|
|
has_quotes_attr and has_quotes_attr[0].lower() == "true"
|
|
)
|
|
|
|
has_quotes = has_quotes_comp or has_quotes_access
|
|
is_temp = name.startswith("#")
|
|
|
|
if i > 0:
|
|
parts.append(".") # Separador para estructuras
|
|
|
|
# Aplicar comillas si es necesario
|
|
if has_quotes or (
|
|
i == 0 and not is_temp and '"' not in name and "." not in name
|
|
):
|
|
# Añadir comillas si HasQuotes es true, o si es el primer componente,
|
|
# no es temporal, no tiene ya comillas, y no es parte de una DB (ej. DB10.DBX0.0)
|
|
parts.append(f'"{name}"')
|
|
else:
|
|
parts.append(name)
|
|
|
|
# Índices de Array (Access anidado dentro de Component)
|
|
index_access = comp.xpath("./stl:Access", namespaces=ns)
|
|
if index_access:
|
|
indices = [get_access_text_stl(ia) for ia in index_access]
|
|
# Limpiar índices (quitar saltos de línea, etc.)
|
|
indices_cleaned = [idx.replace("\n", "").strip() for idx in indices]
|
|
parts.append(f"[{','.join(indices_cleaned)}]")
|
|
|
|
return "".join(parts)
|
|
|
|
# --- Constante Literal ---
|
|
# Busca <Constant> dentro del <Access> usando el namespace stl
|
|
constant_elem = access_element.xpath("./stl:Constant", namespaces=ns)
|
|
if constant_elem:
|
|
# Obtener valor y tipo
|
|
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 == "ltime":
|
|
return f"LT#{const_val}" # Añadido LTIME
|
|
if const_type == "dtl":
|
|
return f"DTL#{const_val}" # Añadido DTL
|
|
|
|
# Strings y Chars (Escapar comillas simples internas)
|
|
if const_type == "string":
|
|
replaced_val = const_val.replace("'", "''")
|
|
return f"'{replaced_val}'"
|
|
if const_type == "char":
|
|
replaced_val = const_val.replace("'", "''")
|
|
return f"'{replaced_val}'"
|
|
if const_type == "wstring":
|
|
replaced_val = const_val.replace("'", "''")
|
|
return f"WSTRING#'{replaced_val}'"
|
|
if const_type == "wchar":
|
|
replaced_val = const_val.replace("'", "''")
|
|
return f"WCHAR#'{replaced_val}'"
|
|
|
|
# Tipos numéricos con prefijo opcional (Hexadecimal)
|
|
if const_val.startswith("16#"):
|
|
if const_type == "byte":
|
|
return f"B#{const_val}"
|
|
if const_type == "word":
|
|
return f"W#{const_val}"
|
|
if const_type == "dword":
|
|
return f"DW#{const_val}"
|
|
if const_type == "lword":
|
|
return f"LW#{const_val}" # Añadido LWORD
|
|
|
|
# Formato Real (añadir .0 si es necesario)
|
|
if (
|
|
const_type in ["real", "lreal"]
|
|
and "." not in const_val
|
|
and "e" not in const_val.lower()
|
|
):
|
|
# Verificar si es un número antes de añadir .0
|
|
try:
|
|
float(const_val) # Intenta convertir a float
|
|
return f"{const_val}.0"
|
|
except ValueError:
|
|
return const_val # No es número, devolver tal cual
|
|
# Otros tipos numéricos o desconocidos
|
|
return const_val # Valor por defecto
|
|
|
|
# --- Etiqueta (Label) ---
|
|
# Busca <Label> dentro del <Access> usando el namespace stl
|
|
label_elem = access_element.xpath("./stl:Label", namespaces=ns)
|
|
if label_elem:
|
|
return label_elem[0].get("Name", "_ERR_LABEL_")
|
|
|
|
# --- Acceso Indirecto (Punteros) ---
|
|
# Busca <Indirect> dentro del <Access> usando el namespace stl
|
|
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, Long
|
|
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",
|
|
} # Mapeo XSD a STL
|
|
width_char = width_map.get(
|
|
width, width[0] if width else "?"
|
|
) # Usar primera letra como fallback
|
|
|
|
# Área: DB, DI, L son comunes. Otras podrían necesitar mapeo.
|
|
area_char = (
|
|
area[0] if area else "?"
|
|
) # Usar primera letra (I, O, M, L, T, C, DB, DI...)
|
|
|
|
# Formato: AREAREG[puntero], ej. DBX[AR1,P#0.0] o LX[AR2,P#10.5]
|
|
return f"{area}{width_char}[{reg},{p_format_offset}]"
|
|
|
|
# --- Dirección Absoluta ---
|
|
# Busca <Address> dentro del <Access> usando el namespace stl
|
|
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")
|
|
# El tipo (Type) del Address define el ancho por defecto
|
|
addr_type_str = address_elem[0].get(
|
|
"Type", "Bool"
|
|
) # Bool, Byte, Word, DWord, Int, DInt, Real...
|
|
block_num_str = address_elem[0].get(
|
|
"BlockNumber"
|
|
) # Para DB10.DBX0.0 o DI5.DIW2
|
|
|
|
try:
|
|
bit_offset = int(bit_offset_str)
|
|
byte_offset = bit_offset // 8
|
|
bit_in_byte = bit_offset % 8
|
|
|
|
# Determinar ancho (X, B, W, D) basado en Type
|
|
addr_width = "X" # Default bit (Bool)
|
|
type_lower = addr_type_str.lower()
|
|
if type_lower in ["byte", "sint", "usint"]:
|
|
addr_width = "B"
|
|
elif type_lower in ["word", "int", "uint", "timer", "counter"]:
|
|
addr_width = "W" # T y C usan W para direccionamiento base
|
|
elif type_lower in [
|
|
"dword",
|
|
"dint",
|
|
"udint",
|
|
"real",
|
|
"time",
|
|
"dt",
|
|
"tod",
|
|
"date_and_time",
|
|
]:
|
|
addr_width = "D"
|
|
elif type_lower in [
|
|
"lreal",
|
|
"ltime",
|
|
"lword",
|
|
"lint",
|
|
"ulint",
|
|
"ltod",
|
|
"ldt",
|
|
"date_and_ltime",
|
|
]:
|
|
addr_width = "D" # Asumir que direccionamiento base usa D para 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) # Usar nombre XML si no está en el mapa
|
|
|
|
if stl_area in ["T", "C"]:
|
|
# Temporizadores y Contadores usan solo el número (offset de byte)
|
|
return f"{stl_area}{byte_offset}" # T 5, C 10
|
|
|
|
elif stl_area in ["DB", "DI"]:
|
|
block_num = (
|
|
block_num_str if block_num_str else ""
|
|
) # Número de bloque si existe
|
|
# Formato: DBNum.DBAnchoByte.Bit o DINum.DIAnchoByte.Bit o DBAnchoByte.Bit (si BlockNum es None)
|
|
db_prefix = f"{stl_area}{block_num}." if block_num else ""
|
|
return f"{db_prefix}{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
|
else: # I, Q, M, L, PI, PQ
|
|
# Formato: AreaAnchoByte.Bit (ej: M B 10 . 1 -> MB10.1 ; I W 0 . 0 -> IW0.0)
|
|
# Corrección: No añadir bit si el ancho no es X
|
|
if addr_width == "X":
|
|
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
|
else:
|
|
return f"{stl_area}{addr_width}{byte_offset}" # ej: MB10, IW0, QW4
|
|
|
|
except ValueError:
|
|
return f"{area}?{bit_offset_str}?" # Error de formato
|
|
|
|
# --- CallInfo (para operando de CALL) ---
|
|
# Busca <CallInfo> dentro del <Access> usando el namespace stl
|
|
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
|
|
|
|
# El operando de CALL depende del tipo de bloque
|
|
if btype == "FB":
|
|
# Para CALL FB, el operando es el DB de instancia
|
|
instance_node = call_info_elem[0].xpath(
|
|
".//stl:Component/@Name", namespaces=ns
|
|
) # Buscar nombre dentro de Instance/Component
|
|
if instance_node:
|
|
db_name_raw = instance_node[0]
|
|
# Añadir comillas si no las tiene
|
|
return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw
|
|
else:
|
|
return f'"_ERR_FB_INSTANCE_NAME_({name})_"'
|
|
else: # FC o desconocido
|
|
# Para CALL FC, el operando es el nombre del FC
|
|
# Añadir comillas si no las tiene
|
|
return f'"{name}"' if '"' not in name else name
|
|
|
|
# Fallback si no se reconoce el tipo de Access
|
|
scope = access_element.get("Scope", "UnknownScope")
|
|
return f"_{scope}_?"
|
|
|
|
|
|
def get_comment_text_stl(comment_element):
|
|
"""
|
|
Extrae texto de un LineComment o Comment para STL usando get_multilingual_text.
|
|
Se asume que get_multilingual_text ya está importado y maneja <Comment> y <LineComment>.
|
|
"""
|
|
return get_multilingual_text(comment_element) if comment_element is not None else ""
|
|
|
|
|
|
def reconstruct_stl_from_statementlist(statement_list_node):
|
|
"""
|
|
Reconstruye el código STL como una cadena de texto desde <StatementList>.
|
|
Usa las funciones auxiliares get_access_text_stl y get_comment_text_stl.
|
|
"""
|
|
if statement_list_node is None:
|
|
return "// Error: StatementList node not found.\n"
|
|
|
|
stl_lines = []
|
|
# Buscar todos los StlStatement hijos usando el namespace 'stl'
|
|
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 //)
|
|
# Buscar <Comment> o <LineComment> que sean hijos directos de StlStatement
|
|
# y NO tengan el atributo Inserted="true" (o no tengan Inserted)
|
|
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) # Usa la función auxiliar
|
|
if comment_text:
|
|
for comment_line in comment_text.splitlines():
|
|
stl_lines.append(
|
|
f"// {comment_line.strip()}"
|
|
) # Añadir como comentario SCL
|
|
|
|
# 2. Etiqueta (LabelDeclaration)
|
|
# Buscar <LabelDeclaration> hijo directo
|
|
label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns)
|
|
label_str = ""
|
|
if label_decl:
|
|
label_name_node = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns)
|
|
if label_name_node:
|
|
label_str = f"{label_name_node[0]}:" # Añadir dos puntos
|
|
# Comentarios después de la etiqueta (inline) - Tienen Inserted="true"
|
|
label_comments = label_decl[0].xpath(
|
|
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
|
|
namespaces=ns,
|
|
)
|
|
for lcomm in label_comments:
|
|
inline_comment += f" // {get_comment_text_stl(lcomm).strip()}" # Acumular comentarios inline
|
|
|
|
if label_str:
|
|
line_parts.append(
|
|
label_str
|
|
) # Añadir etiqueta (si existe) a las partes de la línea
|
|
|
|
# 3. Instrucción (StlToken)
|
|
# Buscar <StlToken> hijo directo
|
|
instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns)
|
|
instruction_str = ""
|
|
if instruction_token:
|
|
token_text = instruction_token[0].get("Text", "_ERR_TOKEN_")
|
|
# Manejar casos especiales definidos en el XSD
|
|
if token_text == "EMPTY_LINE":
|
|
if (
|
|
not stl_lines or stl_lines[-1]
|
|
): # Evitar múltiples líneas vacías seguidas
|
|
stl_lines.append("") # Añadir línea vacía
|
|
continue # Saltar resto del statement (no hay instrucción ni operando)
|
|
elif token_text == "COMMENT":
|
|
# Ya manejado por initial_comments. Si hubiera comentarios SÓLO aquí, se necesitaría extraerlos.
|
|
pass # Asumir manejado antes
|
|
elif token_text == "Assign":
|
|
instruction_str = "=" # Mapear Assign a '='
|
|
elif token_text == "OPEN_DB":
|
|
instruction_str = "AUF" # Mapear OPEN_DB a AUF
|
|
elif token_text == "OPEN_DI":
|
|
instruction_str = "AUF DI" # Mapear OPEN_DI a AUF DI
|
|
# Añadir más mapeos si son necesarios (ej. EQ_I a ==I)
|
|
else:
|
|
instruction_str = token_text # Usar el texto del token como instrucción
|
|
|
|
# Comentarios asociados al token (inline) - Tienen Inserted="true"
|
|
token_comments = instruction_token[0].xpath(
|
|
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
|
|
namespaces=ns,
|
|
)
|
|
for tcomm in token_comments:
|
|
inline_comment += f" // {get_comment_text_stl(tcomm).strip()}"
|
|
|
|
if instruction_str:
|
|
# Añadir tabulación si hubo etiqueta para alinear instrucciones
|
|
line_parts.append("\t" + instruction_str if label_str else instruction_str)
|
|
|
|
# 4. Operando (Access)
|
|
# Buscar <Access> hijo directo
|
|
access_elem = stmt.xpath("./stl:Access", namespaces=ns)
|
|
access_str = ""
|
|
if access_elem:
|
|
# Usar la función auxiliar para reconstruir el texto del operando
|
|
access_text = get_access_text_stl(access_elem[0])
|
|
access_str = access_text
|
|
# Comentarios asociados al Access (inline) - Tienen Inserted="true"
|
|
# Buscar DENTRO del Access
|
|
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).strip()}"
|
|
|
|
if access_str:
|
|
line_parts.append(access_str) # Añadir operando (si existe)
|
|
|
|
# Construir línea final si hay partes (etiqueta, instrucción u operando)
|
|
if line_parts:
|
|
# Unir partes con tabulación si hay más de una (etiqueta+instrucción o instrucción+operando)
|
|
# Ajustar espacios/tabulaciones para legibilidad
|
|
if len(line_parts) > 1:
|
|
# Caso Etiqueta + Instrucción + (Operando opcional)
|
|
if label_str and instruction_str:
|
|
current_line = f"{line_parts[0]:<8}\t{line_parts[1]}" # Etiqueta alineada, tab, instrucción
|
|
if access_str:
|
|
current_line += f"\t{line_parts[2]}" # Tab, operando
|
|
# Caso Instrucción + Operando (sin etiqueta)
|
|
elif instruction_str and access_str:
|
|
current_line = f"\t{line_parts[0]}\t{line_parts[1]}" # Tab, instrucción, tab, operando
|
|
# Caso solo Instrucción (sin etiqueta ni operando)
|
|
elif instruction_str:
|
|
current_line = f"\t{line_parts[0]}" # Tab, instrucción
|
|
else: # Otros casos (solo etiqueta, solo operando? improbable)
|
|
current_line = "\t".join(line_parts)
|
|
else: # Solo una parte (instrucción sin operando o solo etiqueta?)
|
|
current_line = line_parts[0] if label_str else f"\t{line_parts[0]}"
|
|
|
|
# Añadir comentario inline al final si existe, con tabulación
|
|
if inline_comment:
|
|
current_line += f"\t{inline_comment.strip()}"
|
|
|
|
# Añadir la línea construida si no está vacía
|
|
if current_line.strip():
|
|
stl_lines.append(current_line.rstrip()) # Quitar espacios finales
|
|
|
|
# Añadir BE al final si es necesario (lógica específica del bloque, no generalizable aquí)
|
|
# stl_lines.append("BE") # Ejemplo - QUITAR O ADAPTAR
|
|
|
|
return "\n".join(stl_lines)
|
|
|
|
|
|
# --- Función Principal del Parser STL (Corregida v4) ---
|
|
|
|
|
|
def parse_stl_network(network_element):
|
|
"""
|
|
Parsea una red STL extrayendo el código fuente reconstruido. (v4)
|
|
Devuelve un diccionario representando la red para el JSON.
|
|
"""
|
|
network_id = network_element.get("ID", "UnknownSTL_ID")
|
|
network_lang = "STL"
|
|
reconstructed_stl = "// STL extraction failed: Reason unknown.\n" # Default error
|
|
parsing_error_msg = None
|
|
network_title = f"Network {network_id}" # Default title
|
|
network_comment = "" # Default comment
|
|
|
|
try:
|
|
# Buscar NetworkSource usando local-name()
|
|
network_source_node_list = network_element.xpath(
|
|
".//*[local-name()='NetworkSource']"
|
|
)
|
|
|
|
statement_list_node = None
|
|
if network_source_node_list:
|
|
network_source_node = network_source_node_list[0]
|
|
# Buscar StatementList dentro del NetworkSource encontrado, usando local-name()
|
|
statement_list_node_list = network_source_node.xpath(
|
|
".//*[local-name()='StatementList']"
|
|
)
|
|
if statement_list_node_list:
|
|
statement_list_node = statement_list_node_list[0]
|
|
else:
|
|
parsing_error_msg = "StatementList node not found inside NetworkSource."
|
|
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
|
|
else:
|
|
parsing_error_msg = "NetworkSource node not found using local-name()."
|
|
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
|
|
|
|
# Intentar reconstruir SOLO si encontramos el nodo StatementList
|
|
if statement_list_node is not None:
|
|
# La función reconstruct_stl_from_statementlist debe estar definida arriba
|
|
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node)
|
|
elif parsing_error_msg:
|
|
reconstructed_stl = f"// STL extraction failed: {parsing_error_msg}\n"
|
|
|
|
except Exception as e_parse:
|
|
parsing_error_msg = f"Exception during STL network parsing: {e_parse}"
|
|
print(f" ERROR parseando Red {network_id} (STL): {parsing_error_msg}")
|
|
traceback.print_exc()
|
|
reconstructed_stl = f"// ERROR durante el parseo de STL: {e_parse}\n"
|
|
|
|
# Crear la estructura de datos para la red
|
|
parsed_network_data = {
|
|
"id": network_id,
|
|
"language": network_lang,
|
|
"title": network_title,
|
|
"comment": network_comment,
|
|
"logic": [
|
|
{
|
|
"instruction_uid": f"STL_{network_id}",
|
|
"type": "RAW_STL_CHUNK",
|
|
"stl": reconstructed_stl,
|
|
}
|
|
],
|
|
}
|
|
if parsing_error_msg:
|
|
parsed_network_data["error"] = f"Parser failed: {parsing_error_msg}"
|
|
|
|
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"], "parser_func": parse_stl_network}
|