Simatic_XML_Parser_to_SCL/ToUpload/parsers/parser_utils.py

479 lines
20 KiB
Python

# ToUpload/parsers/parser_utils.py
# -*- coding: utf-8 -*-
from lxml import etree
import traceback
# --- Namespaces (Común para muchos parsers) ---
ns = {
"iface": "http://www.siemens.com/automation/Openness/SW/Interface/v5",
"flg": "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4",
"st": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StructuredText/v3",
"stl": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StatementList/v4",
}
# --- Funciones Comunes de Extracción de Texto y Nodos ---
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
"""Extrae texto multilingüe de un elemento XML."""
if element is None:
return ""
try:
# Intenta buscar el idioma por defecto
xpath_expr_default = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{default_lang}']/iface:AttributeList/iface:Text"
text_items_default = element.xpath(xpath_expr_default, namespaces=ns)
if text_items_default and text_items_default[0].text is not None:
return text_items_default[0].text.strip()
# Intenta buscar el idioma de fallback
xpath_expr_fallback = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{fallback_lang}']/iface:AttributeList/iface:Text"
text_items_fallback = element.xpath(xpath_expr_fallback, namespaces=ns)
if text_items_fallback and text_items_fallback[0].text is not None:
return text_items_fallback[0].text.strip()
# Si no encuentra ninguno, toma el primer texto que encuentre
xpath_expr_any = ".//iface:MultilingualTextItem/iface:AttributeList/iface:Text"
text_items_any = element.xpath(xpath_expr_any, namespaces=ns)
if text_items_any and text_items_any[0].text is not None:
return text_items_any[0].text.strip()
# Fallback si MultilingualText está vacío o tiene una estructura inesperada
return ""
except Exception as e:
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
# traceback.print_exc() # Descomentar para más detalles del error
return ""
def get_symbol_name(symbol_element):
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
# Adaptado para usar namespace flg
if symbol_element is None:
return None
try:
# Asume que Component está dentro de Symbol y ambos están en el namespace flg
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
# Formatear correctamente con comillas dobles si es necesario (ej. DBs)
return (
".".join(
f'"{c}"' if not c.startswith("#") and '"' not in c else c
for c in components
)
if components
else None
)
except Exception as e:
print(f"Advertencia: Excepción en get_symbol_name: {e}")
return None
def parse_access(access_element):
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
# Adaptado para usar namespace flg
if access_element is None:
return None
uid = access_element.get("UId")
scope = access_element.get("Scope")
info = {"uid": uid, "scope": scope, "type": "unknown"}
# Buscar Symbol o Constant usando el namespace flg
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
constant = access_element.xpath("./flg:Constant", namespaces=ns)
if symbol:
info["type"] = "variable"
# Llamar a get_symbol_name que ahora espera flg:Symbol
info["name"] = get_symbol_name(symbol[0])
if info["name"] is None:
info["type"] = "error_parsing_symbol"
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
# Intentar extraer texto directamente como fallback muy básico
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
info["name"] = (
f'"_ERR_PARSING_{raw_text[:20]}"'
if raw_text
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
)
# return info # Podríamos devolver el error aquí
elif constant:
info["type"] = "constant"
# Buscar ConstantType y ConstantValue usando el namespace flg
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
# Extraer texto
info["datatype"] = (
const_type_elem[0].text.strip()
if const_type_elem and const_type_elem[0].text is not None
else "Unknown"
)
value_str = (
const_val_elem[0].text.strip()
if const_val_elem and const_val_elem[0].text is not None
else None
)
if value_str is None:
info["type"] = "error_parsing_constant"
info["value"] = None
print(f"Error: Constante sin valor Access UID={uid}")
# return info
# Inferir tipo si es Unknown (igual que antes)
if info["datatype"] == "Unknown" and value_str:
val_lower = value_str.lower()
if val_lower in ["true", "false"]:
info["datatype"] = "Bool"
elif value_str.isdigit() or (
value_str.startswith("-") and value_str[1:].isdigit()
):
info["datatype"] = "Int" # O DInt? Int es más seguro
elif "." in value_str:
try:
float(value_str)
info["datatype"] = "Real" # O LReal? Real es más seguro
except ValueError:
pass # Podría ser string con punto
elif "#" in value_str:
# Inferir tipo desde prefijo (T#, DT#, '...', etc.)
parts = value_str.split("#", 1)
prefix = parts[0].upper()
if prefix == "T":
info["datatype"] = "Time"
elif prefix == "LT":
info["datatype"] = "LTime"
elif prefix == "S5T":
info["datatype"] = "S5Time"
elif prefix == "D":
info["datatype"] = "Date"
elif prefix == "DT":
info["datatype"] = "DT"
elif prefix == "DTL":
info["datatype"] = "DTL"
elif prefix == "TOD":
info["datatype"] = "Time_Of_Day"
# Añadir más prefijos si es necesario (WSTRING#, STRING#, etc.)
elif value_str.startswith("'") and value_str.endswith("'"):
info["datatype"] = "String" # O Char? String es más probable
else:
info["datatype"] = (
"TypedConstant" # Genérico si no se reconoce prefijo
)
elif value_str.startswith("'") and value_str.endswith("'"):
info["datatype"] = "String" # O Char?
info["value"] = value_str # Guardar valor original
# Intentar conversión numérica/booleana (igual que antes)
dtype_lower = info["datatype"].lower()
# Quitar prefijo y comillas para la conversión
val_str_processed = value_str
if isinstance(value_str, str):
if "#" in value_str:
val_str_processed = value_str.split("#", 1)[-1]
if (
val_str_processed.startswith("'")
and val_str_processed.endswith("'")
and len(val_str_processed) > 1
):
val_str_processed = val_str_processed[1:-1]
try:
if dtype_lower in [
"int",
"dint",
"udint",
"sint",
"usint",
"lint",
"ulint",
"word",
"dword",
"lword",
"byte",
]:
info["value"] = int(val_str_processed)
elif dtype_lower == "bool":
info["value"] = (
val_str_processed.lower() == "true" or val_str_processed == "1"
)
elif dtype_lower in ["real", "lreal"]:
info["value"] = float(val_str_processed)
# Mantener string para otros tipos (Time, Date, String, Char, TypedConstant)
except (ValueError, TypeError) as e:
# Permitir que el valor sea un string si la conversión falla (podría ser una constante simbólica)
# print(f"Advertencia: No se pudo convertir valor constante '{val_str_processed}' a {dtype_lower} UID={uid}. Manteniendo string. Error: {e}")
info["value"] = value_str # Mantener string original
else:
info["type"] = "unknown_structure"
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
# return info
# Verificar nombre faltante después de intentar parsear
if info["type"] == "variable" and info.get("name") is None:
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
info["type"] = "error_no_name"
# return info
return info
def parse_part(part_element):
"""Parsea un nodo <flg:Part> de LAD/FBD."""
# Asume que Part está en namespace flg
if part_element is None:
return None
uid = part_element.get("UId")
name = part_element.get("Name")
if not uid or not name:
print(
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
)
return None
template_values = {}
try:
# TemplateValue parece NO tener namespace flg
for tv in part_element.xpath("./TemplateValue"):
tv_name = tv.get("Name")
tv_type = tv.get("Type")
if tv_name and tv_type:
template_values[tv_name] = tv_type
except Exception as e:
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
negated_pins = {}
try:
# Negated parece NO tener namespace flg
for negated_elem in part_element.xpath("./Negated"):
negated_pin_name = negated_elem.get("Name")
if negated_pin_name:
negated_pins[negated_pin_name] = True
except Exception as e:
print(f"Advertencia: Error extrayendo Negated Pins Part UID={uid}: {e}")
return {
"uid": uid,
"type": name, # El 'type' de la instrucción (e.g., 'Add', 'Contact')
"template_values": template_values,
"negated_pins": negated_pins,
}
def parse_call(call_element):
"""Parsea un nodo <flg:Call> de LAD/FBD."""
# Asume que Call está en namespace flg
if call_element is None:
return None
uid = call_element.get("UId")
if not uid:
print(
f"Error: Call encontrado sin UID: {etree.tostring(call_element, encoding='unicode')}"
)
return None
# << CORRECCIÓN: CallInfo y sus hijos están en el namespace por defecto (flg) >>
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
if not call_info_elem:
print(f"Error: Call UID {uid} sin elemento flg:CallInfo.")
# Intentar sin namespace como fallback por si acaso
call_info_elem_no_ns = call_element.xpath("./CallInfo")
if not call_info_elem_no_ns:
print(
f"Error: Call UID {uid} sin elemento CallInfo (probado sin NS tambien)."
)
return {
"uid": uid,
"type": "Call_error",
"error": "Missing CallInfo",
} # Devolver error
else:
# Si se encontró sin NS, usar ese (menos probable pero posible)
print(f"Advertencia: Call UID {uid} encontró CallInfo SIN namespace.")
call_info = call_info_elem_no_ns[0]
else:
call_info = call_info_elem[0] # Usar el encontrado con namespace
block_name = call_info.get("Name")
block_type = call_info.get("BlockType") # FC, FB
if not block_name or not block_type:
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
return {
"uid": uid,
"type": "Call_error",
"error": "Missing Name or BlockType in CallInfo",
}
instance_name = None
instance_scope = None
# Buscar Instance y Component (que también deberían estar en namespace flg)
# Solo relevante si es FB
if block_type == "FB":
instance_elem_list = call_info.xpath("./flg:Instance", namespaces=ns)
if instance_elem_list:
instance_elem = instance_elem_list[0]
instance_scope = instance_elem.get("Scope") # GlobalDB, LocalVariable, etc.
# Buscar Component dentro de Instance
component_elem_list = instance_elem.xpath("./flg:Component", namespaces=ns)
if component_elem_list:
component_elem = component_elem_list[0]
db_name_raw = component_elem.get("Name")
if db_name_raw:
# Asegurar comillas dobles para nombres de DB
instance_name = (
f'"{db_name_raw}"'
if not db_name_raw.startswith('"')
else db_name_raw
)
else:
print(
f"Advertencia: <flg:Component> en <flg:Instance> FB Call UID {uid} sin 'Name'."
)
else:
print(
f"Advertencia: No se encontró <flg:Component> en <flg:Instance> FB Call UID {uid}."
)
else:
print(
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
)
# Aquí podríamos intentar buscar si el scope del Call es LocalVariable para inferir STAT
call_scope = call_element.get("Scope") # Scope del <Call> mismo
if call_scope == "LocalVariable":
# Si la llamada es local y no tiene <Instance>, probablemente es una multi-instancia STAT
instance_name = f'"{block_name}"' # Usar el nombre del bloque como nombre de instancia STAT (convención común)
instance_scope = "Static" # Marcar como estático
print(
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
)
# else: # Error si es Global y no tiene Instance? Depende de la semántica deseada.
# print(f"Error: FB Call '{block_name}' UID {uid} no es STAT y no tiene <flg:Instance>.")
# return {"uid": uid, "type": "Call_error", "error": "FB Call sin datos de instancia"}
# El 'type' aquí es genérico 'Call', la distinción FC/FB se hace con block_type
call_data = {
"uid": uid,
"type": "Call",
"block_name": block_name,
"block_type": block_type, # FC o FB
}
if instance_name:
call_data["instance_db"] = instance_name # Nombre formateado SCL
if instance_scope:
call_data["instance_scope"] = instance_scope # Static, GlobalDB, etc.
return call_data
def parse_interface_members(member_elements):
"""
Parsea recursivamente una lista de elementos <Member> de una interfaz o estructura.
Maneja miembros simples, structs anidados y arrays con valores iniciales.
Usa el namespace 'iface'.
"""
members_data = []
if not member_elements:
return members_data
for member in member_elements:
member_name = member.get("Name")
member_dtype_raw = member.get(
"Datatype"
) # Puede tener comillas o ser Array[...] of "..."
member_version = member.get("Version") # v1.0 etc.
member_remanence = member.get("Remanence", "NonRetain")
member_accessibility = member.get("Accessibility", "Public")
if not member_name or not member_dtype_raw:
print(
"Advertencia: Miembro sin nombre o tipo de dato encontrado. Saltando."
)
continue
# Combinar tipo y versión si existe versión separada
member_dtype = (
f"{member_dtype_raw}:v{member_version}"
if member_version
else member_dtype_raw
)
member_info = {
"name": member_name,
"datatype": member_dtype, # Guardar el tipo original (puede tener comillas, versión)
"remanence": member_remanence,
"accessibility": member_accessibility,
"start_value": None,
"comment": None,
"children": [], # Para Structs
"array_elements": {}, # Para Arrays
}
# Comentario del miembro
comment_node = member.xpath("./iface:Comment", namespaces=ns)
if comment_node:
# Comentario está dentro de Comment/MultiLanguageText
member_info["comment"] = get_multilingual_text(comment_node[0])
# Valor inicial
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
if start_value_node:
# Puede ser un nombre de constante o un valor literal
constant_name = start_value_node[0].get("ConstantName")
member_info["start_value"] = (
constant_name
if constant_name
else (
start_value_node[0].text
if start_value_node[0].text is not None
else ""
)
)
# No intentar convertir aquí, se hará en x3 según el tipo de dato
# --- Structs Anidados ---
# Los miembros de un struct están dentro de Sections/Section/Member
nested_sections = member.xpath(
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
) # Sección sin nombre específico
if nested_sections:
# Llamada recursiva
member_info["children"] = parse_interface_members(nested_sections)
# --- Arrays ---
# Buscar elementos <Subelement> para valores iniciales de array
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
subelements = member.xpath("./iface:Subelement", namespaces=ns)
for sub in subelements:
path = sub.get("Path") # Path es el índice: '0', '1', '0,0', etc.
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
if path and sub_start_value_node:
constant_name = sub_start_value_node[0].get("ConstantName")
value = (
constant_name
if constant_name
else (
sub_start_value_node[0].text
if sub_start_value_node[0].text is not None
else ""
)
)
member_info["array_elements"][path] = value
# Parsear comentario del subelemento si es necesario
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
if path and sub_comment_node:
sub_comment_text = get_multilingual_text(sub_comment_node[0])
# ¿Cómo guardar comentario de subelemento? Podría ser un dict en array_elements
if isinstance(member_info["array_elements"].get(path), dict):
member_info["array_elements"][path][
"comment"
] = sub_comment_text
else: # Si solo estaba el valor, convertir a dict
current_val = member_info["array_elements"].get(path)
member_info["array_elements"][path] = {
"value": current_val,
"comment": sub_comment_text,
}
members_data.append(member_info)
return members_data