388 lines
16 KiB
Python
388 lines
16 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, asegurando devolver siempre string."""
|
|
if element is None:
|
|
return "" # Devolver cadena vacía si el elemento es None
|
|
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)
|
|
# CORRECCIÓN: Devolver "" si .text es None
|
|
if text_items_default and text_items_default[0].text is not None:
|
|
return text_items_default[0].text.strip()
|
|
# Intentar 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)
|
|
# CORRECCIÓN: Devolver "" si .text es None
|
|
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)
|
|
# CORRECCIÓN: Devolver "" si .text es None
|
|
if text_items_any and text_items_any[0].text is not None:
|
|
return text_items_any[0].text.strip()
|
|
|
|
# Fallback final si no se encontró ningún MultilingualTextItem con texto
|
|
return "" # Asegurar retorno de string vacío
|
|
except Exception as e:
|
|
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
|
|
# traceback.print_exc() # Descomentar para más detalles del error
|
|
return "" # Devolver cadena vacía en caso de excepción
|
|
|
|
|
|
def get_symbol_name(symbol_element):
|
|
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
|
if symbol_element is None:
|
|
return None
|
|
try:
|
|
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
|
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."""
|
|
if access_element is None:
|
|
return None
|
|
uid = access_element.get("UId")
|
|
scope = access_element.get("Scope")
|
|
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
|
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
|
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
|
|
|
if symbol:
|
|
info["type"] = "variable"
|
|
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}")
|
|
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}"'
|
|
)
|
|
elif constant:
|
|
info["type"] = "constant"
|
|
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
|
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
|
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}")
|
|
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"
|
|
elif "." in value_str:
|
|
try:
|
|
float(value_str)
|
|
info["datatype"] = "Real"
|
|
except ValueError:
|
|
pass
|
|
elif "#" in value_str:
|
|
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"
|
|
elif value_str.startswith("'") and value_str.endswith("'"):
|
|
info["datatype"] = "String"
|
|
else:
|
|
info["datatype"] = "TypedConstant"
|
|
elif value_str.startswith("'") and value_str.endswith("'"):
|
|
info["datatype"] = "String"
|
|
info["value"] = value_str
|
|
dtype_lower = info["datatype"].lower()
|
|
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)
|
|
except (ValueError, TypeError):
|
|
info["value"] = value_str
|
|
else:
|
|
info["type"] = "unknown_structure"
|
|
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
|
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
|
|
|
|
|
|
def parse_part(part_element):
|
|
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
|
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 = {}
|
|
negated_pins = {}
|
|
try:
|
|
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}")
|
|
try:
|
|
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,
|
|
"template_values": template_values,
|
|
"negated_pins": negated_pins,
|
|
}
|
|
|
|
|
|
def parse_call(call_element):
|
|
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
|
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
|
|
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
|
|
if not call_info_elem:
|
|
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.")
|
|
return {"uid": uid, "type": "Call_error", "error": "Missing CallInfo"}
|
|
else:
|
|
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]
|
|
block_name = call_info.get("Name")
|
|
block_type = call_info.get("BlockType")
|
|
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, instance_scope = None, None
|
|
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")
|
|
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:
|
|
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?"
|
|
)
|
|
call_scope = call_element.get("Scope")
|
|
if call_scope == "LocalVariable":
|
|
instance_name = f'"{block_name}"'
|
|
instance_scope = "Static"
|
|
print(
|
|
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
|
)
|
|
call_data = {
|
|
"uid": uid,
|
|
"type": "Call",
|
|
"block_name": block_name,
|
|
"block_type": block_type,
|
|
}
|
|
if instance_name:
|
|
call_data["instance_db"] = instance_name
|
|
if instance_scope:
|
|
call_data["instance_scope"] = instance_scope
|
|
return call_data
|
|
|
|
|
|
def parse_interface_members(member_elements):
|
|
"""Parsea recursivamente miembros de interfaz/estructura."""
|
|
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")
|
|
member_version = member.get("Version")
|
|
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. Saltando.")
|
|
continue
|
|
member_dtype = (
|
|
f"{member_dtype_raw}:v{member_version}"
|
|
if member_version
|
|
else member_dtype_raw
|
|
)
|
|
member_info = {
|
|
"name": member_name,
|
|
"datatype": member_dtype,
|
|
"remanence": member_remanence,
|
|
"accessibility": member_accessibility,
|
|
"start_value": None,
|
|
"comment": None,
|
|
"children": [],
|
|
"array_elements": {},
|
|
}
|
|
comment_node = member.xpath("./iface:Comment", namespaces=ns)
|
|
if comment_node:
|
|
member_info["comment"] = get_multilingual_text(
|
|
comment_node[0]
|
|
) # Usa la función robusta
|
|
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
|
|
if start_value_node:
|
|
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 None
|
|
)
|
|
) # Devolver None si está vacío
|
|
nested_sections = member.xpath(
|
|
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
|
)
|
|
if nested_sections:
|
|
member_info["children"] = parse_interface_members(nested_sections)
|
|
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")
|
|
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 None
|
|
)
|
|
) # Devolver None si está vacío
|
|
member_info["array_elements"][path] = value
|
|
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]
|
|
) # Usa la función robusta
|
|
if isinstance(member_info["array_elements"].get(path), dict):
|
|
member_info["array_elements"][path][
|
|
"comment"
|
|
] = sub_comment_text
|
|
else:
|
|
member_info["array_elements"][path] = {
|
|
"value": member_info["array_elements"].get(path),
|
|
"comment": sub_comment_text,
|
|
}
|
|
members_data.append(member_info)
|
|
return members_data
|