688 lines
26 KiB
Python
688 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
import os
|
|
from lxml import etree
|
|
import traceback
|
|
from collections import defaultdict
|
|
|
|
# --- Namespaces ---
|
|
ns = {
|
|
"iface": "http://www.siemens.com/automation/Openness/SW/Interface/v5",
|
|
"flg": "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4",
|
|
}
|
|
|
|
|
|
# --- Helper Functions ---
|
|
# get_multilingual_text, get_symbol_name, parse_access - No changes needed from previous corrected version
|
|
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
|
|
if element is None:
|
|
return ""
|
|
try:
|
|
# Try default language first
|
|
xpath_expr_default = (
|
|
f".//iface:MultilingualTextItem[iface:Culture='{default_lang}']/iface:Text"
|
|
)
|
|
text_items = element.xpath(xpath_expr_default, namespaces=ns)
|
|
if text_items and text_items[0].text is not None:
|
|
return text_items[0].text.strip()
|
|
|
|
# Try fallback language
|
|
xpath_expr_fallback = (
|
|
f".//iface:MultilingualTextItem[iface:Culture='{fallback_lang}']/iface:Text"
|
|
)
|
|
text_items = element.xpath(xpath_expr_fallback, namespaces=ns)
|
|
if text_items and text_items[0].text is not None:
|
|
return text_items[0].text.strip()
|
|
|
|
# Try any language if specific ones fail
|
|
xpath_expr_any = ".//iface:MultilingualTextItem/iface:Text"
|
|
text_items = element.xpath(xpath_expr_any, namespaces=ns)
|
|
if text_items and text_items[0].text is not None:
|
|
return text_items[0].text.strip()
|
|
|
|
return "" # No text found
|
|
except Exception as e:
|
|
# print(f"Advertencia: Error extrayendo MultilingualText: {e}") # Reduced verbosity
|
|
return ""
|
|
|
|
|
|
def get_symbol_name(symbol_element):
|
|
"""Extracts the full symbolic name, adding quotes around each component."""
|
|
if symbol_element is None:
|
|
return None
|
|
try:
|
|
# Namespace might be missing on Component, use local-name()
|
|
components = symbol_element.xpath("./*[local-name()='Component']/@Name")
|
|
# Ensure quotes are added correctly
|
|
return ".".join(f'"{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):
|
|
"""Parses Access elements (variables, constants)."""
|
|
if access_element is None:
|
|
return None
|
|
uid = access_element.get("UId")
|
|
scope = access_element.get(
|
|
"Scope"
|
|
) # GlobalVariable, LocalVariable, LiteralConstant, TypedConstant etc.
|
|
info = {"uid": uid, "scope": scope, "type": "unknown_access"} # Default type
|
|
|
|
symbol = access_element.xpath("./*[local-name()='Symbol']")
|
|
constant = access_element.xpath("./*[local-name()='Constant']")
|
|
# Add check for ConstantValue tag directly under Access (sometimes happens for literals)
|
|
const_val_direct = access_element.xpath("./*[local-name()='ConstantValue']/text()")
|
|
|
|
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}")
|
|
elif constant:
|
|
info["type"] = "constant"
|
|
const_type_elem = constant[0].xpath("./*[local-name()='ConstantType']")
|
|
const_val_elem = constant[0].xpath("./*[local-name()='ConstantValue']")
|
|
info["datatype"] = (
|
|
const_type_elem[0].text.strip()
|
|
if const_type_elem and const_type_elem[0].text
|
|
else "Unknown"
|
|
)
|
|
info["value"] = (
|
|
const_val_elem[0].text.strip()
|
|
if const_val_elem and const_val_elem[0].text
|
|
else None
|
|
)
|
|
|
|
if info["value"] is None:
|
|
info["type"] = "error_parsing_constant"
|
|
print(f"Error: Constante sin valor Access UID={uid}")
|
|
# Handle S5Time specifically - store its original format
|
|
elif info["datatype"] == "Unknown" and scope == "TypedConstant":
|
|
if info["value"].upper().startswith("S5T#"):
|
|
info["datatype"] = "S5Time" # Mark as S5Time, value remains S5T#...
|
|
# Add other typed constant checks if necessary (e.g., C#, P#)
|
|
|
|
elif const_val_direct and scope == "LiteralConstant":
|
|
info["type"] = "constant"
|
|
info["value"] = const_val_direct[0].strip()
|
|
# Infer datatype for literals
|
|
val_lower = info["value"].lower()
|
|
if val_lower in ["true", "false"]:
|
|
info["datatype"] = "Bool"
|
|
elif info["value"].isdigit() or (
|
|
info["value"].startswith("-") and info["value"][1:].isdigit()
|
|
):
|
|
info["datatype"] = "Int" # Could be DInt etc, Int is safe default
|
|
elif "." in info["value"] or "e" in val_lower:
|
|
try:
|
|
float(info["value"])
|
|
info["datatype"] = "Real" # Could be LReal
|
|
except ValueError:
|
|
info["datatype"] = "String" # If float conversion fails
|
|
else:
|
|
info["datatype"] = "String" # Default literal type
|
|
|
|
# If still unknown, log warning
|
|
if info["type"] == "unknown_access":
|
|
# Don't warn for Constant scope as it might be handled later
|
|
if scope != "Constant":
|
|
print(
|
|
f"Advertencia: Access UID={uid} scope={scope} no es Symbol ni Constant reconocible."
|
|
)
|
|
|
|
# Ensure variable has a name
|
|
if info["type"] == "variable" and not info.get("name"):
|
|
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
|
|
info["type"] = "error_no_name"
|
|
|
|
return info
|
|
|
|
|
|
def parse_part(part_element):
|
|
"""Parses Part elements (standard instructions), extracting UID, name, template values, and negated pins."""
|
|
if part_element is None:
|
|
return None
|
|
uid = part_element.get("UId")
|
|
name_orig = part_element.get("Name") # Instruction type (e.g., Contact, Coil, Move)
|
|
if not uid or not name_orig:
|
|
print(
|
|
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
|
)
|
|
return None
|
|
|
|
template_values = {}
|
|
try:
|
|
for tv in part_element.xpath("./*[local-name()='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:
|
|
for negated_elem in part_element.xpath("./*[local-name()='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}")
|
|
|
|
version = part_element.get("Version")
|
|
# Map XML names to internal types used by x2_process
|
|
name_mapped = name_orig
|
|
if name_orig == "Se":
|
|
name_mapped = "TON_S5"
|
|
elif name_orig == "Sd":
|
|
name_mapped = "TONR_S5"
|
|
elif name_orig == "PBox":
|
|
name_mapped = "P_TRIG"
|
|
elif name_orig == "NBox":
|
|
name_mapped = "N_TRIG"
|
|
elif name_orig == "RCoil":
|
|
name_mapped = "R"
|
|
elif name_orig == "SCoil":
|
|
name_mapped = "S"
|
|
elif name_orig == "SdCoil":
|
|
name_mapped = "SR" # Map S5 Set-Dominant to SR internal type
|
|
elif name_orig == "BLKMOV":
|
|
name_mapped = "BLKMOV" # Keep as is
|
|
# Add other mappings if necessary (e.g., RsCoil -> RS)
|
|
|
|
part_data = {
|
|
"uid": uid,
|
|
"type": name_mapped, # Use the mapped type
|
|
"original_type": name_orig, # Store original name for reference if needed
|
|
"template_values": template_values,
|
|
"negated_pins": negated_pins,
|
|
}
|
|
if version:
|
|
part_data["version"] = version
|
|
|
|
return part_data
|
|
|
|
|
|
# parse_call - No changes needed from previous corrected version
|
|
def parse_call(call_element):
|
|
"""Parses Call elements (FC/FB calls)."""
|
|
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
|
|
|
|
# Use local-name() for CallInfo
|
|
call_info_elem = call_element.xpath("./*[local-name()='CallInfo']")
|
|
if not call_info_elem:
|
|
print(f"Error: Call UID {uid} sin elemento CallInfo.")
|
|
return None
|
|
call_info = call_info_elem[0]
|
|
|
|
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 None
|
|
|
|
call_data = {
|
|
"uid": uid,
|
|
"type": "Call", # Generic type for our JSON
|
|
"block_name": block_name,
|
|
"block_type": block_type,
|
|
"template_values": {}, # Add fields for consistency with parse_part
|
|
"negated_pins": {},
|
|
}
|
|
|
|
# Instance info for FBs
|
|
instance_name = None
|
|
if block_type == "FB":
|
|
# Use local-name() for Instance and Symbol
|
|
instance_elem = call_info.xpath("./*[local-name()='Instance']")
|
|
if instance_elem:
|
|
symbol_elem = instance_elem[0].xpath("./*[local-name()='Symbol']")
|
|
if symbol_elem:
|
|
instance_name = get_symbol_name(symbol_elem[0])
|
|
if instance_name:
|
|
call_data["instance_db"] = (
|
|
instance_name # Store the formatted name directly
|
|
)
|
|
|
|
return call_data
|
|
|
|
|
|
# --- Function parse_network (Main logic per network) ---
|
|
def parse_network(network_element):
|
|
if network_element is None:
|
|
return {
|
|
"id": "ERROR",
|
|
"title": "Invalid Network Element",
|
|
"logic": [],
|
|
"error": "Input element was None",
|
|
}
|
|
network_id = network_element.get("ID")
|
|
|
|
title_node = network_element.xpath(
|
|
".//*[local-name()='MultilingualText'][@CompositionName='Title']"
|
|
)
|
|
network_title = (
|
|
get_multilingual_text(title_node[0]) if title_node else f"Network {network_id}"
|
|
)
|
|
|
|
comment_node = network_element.xpath(
|
|
".//*[local-name()='MultilingualText'][@CompositionName='Comment']"
|
|
)
|
|
network_comment = get_multilingual_text(comment_node[0]) if comment_node else ""
|
|
|
|
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
|
|
if not flgnet_list:
|
|
return {
|
|
"id": network_id,
|
|
"title": network_title,
|
|
"comment": network_comment,
|
|
"logic": [],
|
|
"error": "FlgNet not found",
|
|
}
|
|
flgnet = flgnet_list[0]
|
|
|
|
# 1. Parse Access, Parts, and Calls
|
|
access_map = {}
|
|
for acc in flgnet.xpath(".//flg:Access", namespaces=ns):
|
|
if acc_info := parse_access(acc):
|
|
access_map[acc_info["uid"]] = acc_info
|
|
|
|
parts_and_calls_map = {}
|
|
instruction_elements = flgnet.xpath(".//flg:Part | .//flg:Call", namespaces=ns)
|
|
for element in instruction_elements:
|
|
parsed_info = None
|
|
if element.tag == etree.QName(ns["flg"], "Part"):
|
|
parsed_info = parse_part(element)
|
|
elif element.tag == etree.QName(ns["flg"], "Call"):
|
|
parsed_info = parse_call(element)
|
|
|
|
if parsed_info and "uid" in parsed_info:
|
|
parts_and_calls_map[parsed_info["uid"]] = parsed_info
|
|
|
|
# 2. Parse Wires
|
|
wire_connections = defaultdict(list)
|
|
source_connections = defaultdict(list)
|
|
flg_ns_uri = ns["flg"]
|
|
for wire in flgnet.xpath(".//flg:Wire", namespaces=ns):
|
|
source_uid, source_pin, dest_uid, dest_pin = None, None, None, None
|
|
source_elem = wire.xpath("./*[1]")[0]
|
|
dest_elem = wire.xpath("./*[2]")[0]
|
|
|
|
if source_elem.tag == etree.QName(flg_ns_uri, "Powerrail"):
|
|
source_uid, source_pin = "POWERRAIL", "out"
|
|
elif source_elem.tag == etree.QName(flg_ns_uri, "IdentCon"):
|
|
source_uid, source_pin = source_elem.get("UId"), "value"
|
|
elif source_elem.tag == etree.QName(flg_ns_uri, "NameCon"):
|
|
source_uid, source_pin = source_elem.get("UId"), source_elem.get("Name")
|
|
|
|
if dest_elem.tag == etree.QName(flg_ns_uri, "IdentCon"):
|
|
dest_uid, dest_pin = dest_elem.get("UId"), "value"
|
|
elif dest_elem.tag == etree.QName(flg_ns_uri, "NameCon"):
|
|
dest_uid, dest_pin = dest_elem.get("UId"), dest_elem.get("Name")
|
|
elif dest_elem.tag == etree.QName(flg_ns_uri, "OpenCon"):
|
|
dest_uid, dest_pin = "OPEN", "in"
|
|
|
|
if dest_uid and dest_pin and source_uid is not None and source_pin is not None:
|
|
if dest_uid != "OPEN":
|
|
dest_key = (dest_uid, dest_pin)
|
|
source_info = (source_uid, source_pin)
|
|
if source_info not in wire_connections[dest_key]:
|
|
wire_connections[dest_key].append(source_info)
|
|
source_key = (source_uid, source_pin)
|
|
dest_info = (dest_uid, dest_pin)
|
|
if dest_info not in source_connections[source_key]:
|
|
source_connections[source_key].append(dest_info)
|
|
|
|
# 3. Build Initial Logic Representation
|
|
all_logic_steps = {}
|
|
for instr_uid, instr_info in parts_and_calls_map.items():
|
|
instruction_repr = {
|
|
"instruction_uid": instr_uid,
|
|
**instr_info,
|
|
"inputs": {},
|
|
"outputs": {},
|
|
}
|
|
|
|
# *** ADD DSTBLK to possible inputs ***
|
|
possible_input_pins = {
|
|
"en",
|
|
"in",
|
|
"in1",
|
|
"in2",
|
|
"in3",
|
|
"in4",
|
|
"operand",
|
|
"bit",
|
|
"pre",
|
|
"clk",
|
|
"s",
|
|
"tv",
|
|
"r",
|
|
"S",
|
|
"R1",
|
|
"SRCBLK",
|
|
"DSTBLK",
|
|
}
|
|
|
|
for dest_pin_name in possible_input_pins:
|
|
dest_key = (instr_uid, dest_pin_name)
|
|
if dest_key in wire_connections:
|
|
sources_list = wire_connections[dest_key]
|
|
input_sources_repr = []
|
|
for source_uid, source_pin in sources_list:
|
|
if source_uid == "POWERRAIL":
|
|
input_sources_repr.append({"type": "powerrail"})
|
|
elif source_uid in access_map:
|
|
input_sources_repr.append(access_map[source_uid])
|
|
elif source_uid in parts_and_calls_map:
|
|
input_sources_repr.append(
|
|
{
|
|
"type": "connection",
|
|
"source_instruction_type": parts_and_calls_map[
|
|
source_uid
|
|
]["type"],
|
|
"source_instruction_uid": source_uid,
|
|
"source_pin": source_pin,
|
|
}
|
|
)
|
|
else:
|
|
input_sources_repr.append(
|
|
{"type": "unknown_source", "uid": source_uid}
|
|
)
|
|
if len(input_sources_repr) == 1:
|
|
instruction_repr["inputs"][dest_pin_name] = input_sources_repr[0]
|
|
elif len(input_sources_repr) > 1:
|
|
instruction_repr["inputs"][dest_pin_name] = input_sources_repr
|
|
|
|
possible_output_pins = {"out", "out1", "Q", "eno", "RET_VAL", "q", "et"}
|
|
for src_pin_name in possible_output_pins:
|
|
source_key = (instr_uid, src_pin_name)
|
|
if source_key in source_connections:
|
|
dest_access_list = []
|
|
for dest_uid, dest_pin in source_connections[source_key]:
|
|
if dest_uid in access_map:
|
|
if access_map[dest_uid] not in dest_access_list:
|
|
dest_access_list.append(access_map[dest_uid])
|
|
if dest_access_list:
|
|
instruction_repr["outputs"][src_pin_name] = dest_access_list
|
|
|
|
all_logic_steps[instr_uid] = instruction_repr
|
|
|
|
# 4. EN Connection Inference
|
|
functional_block_types = {
|
|
"Move",
|
|
"Add",
|
|
"Sub",
|
|
"Mul",
|
|
"Div",
|
|
"Mod",
|
|
"Convert",
|
|
"Call",
|
|
"BLKMOV",
|
|
}
|
|
# Use MAPPED types for RLO generators
|
|
rlo_generators = {
|
|
"Contact",
|
|
"O",
|
|
"Eq",
|
|
"Ne",
|
|
"Gt",
|
|
"Lt",
|
|
"Ge",
|
|
"Le",
|
|
"And",
|
|
"Xor",
|
|
"P_TRIG",
|
|
"N_TRIG",
|
|
}
|
|
|
|
try:
|
|
sorted_uids = sorted(all_logic_steps.keys(), key=lambda x: int(x))
|
|
except ValueError:
|
|
sorted_uids = sorted(all_logic_steps.keys())
|
|
|
|
processed_for_en_inference = set()
|
|
current_logic_list_for_en = [all_logic_steps[uid] for uid in sorted_uids]
|
|
|
|
for i, instruction in enumerate(current_logic_list_for_en):
|
|
instr_uid = instruction["instruction_uid"]
|
|
instr_type = instruction["type"] # Use the mapped type
|
|
|
|
if (
|
|
instr_type in functional_block_types
|
|
and "en" not in instruction["inputs"]
|
|
and instr_uid not in processed_for_en_inference
|
|
):
|
|
inferred_en_source = None
|
|
if i > 0:
|
|
# Simple lookback to previous instruction
|
|
prev_instr = current_logic_list_for_en[i - 1]
|
|
prev_uid = prev_instr["instruction_uid"]
|
|
prev_type = prev_instr["type"]
|
|
# Check if previous instruction has a mappable 'out' or 'eno'
|
|
# We check source_connections map for actual wire existence
|
|
prev_has_out_wire = any(
|
|
dest[0] != "OPEN"
|
|
for dest in source_connections.get((prev_uid, "out"), [])
|
|
)
|
|
prev_has_eno_wire = any(
|
|
dest[0] != "OPEN"
|
|
for dest in source_connections.get((prev_uid, "eno"), [])
|
|
)
|
|
|
|
if prev_type in rlo_generators and prev_has_out_wire:
|
|
inferred_en_source = {
|
|
"type": "connection",
|
|
"source_instruction_uid": prev_uid,
|
|
"source_instruction_type": prev_type,
|
|
"source_pin": "out",
|
|
}
|
|
elif prev_type in functional_block_types and prev_has_eno_wire:
|
|
inferred_en_source = {
|
|
"type": "connection",
|
|
"source_instruction_uid": prev_uid,
|
|
"source_instruction_type": prev_type,
|
|
"source_pin": "eno",
|
|
}
|
|
|
|
if inferred_en_source:
|
|
all_logic_steps[instr_uid]["inputs"]["en"] = inferred_en_source
|
|
processed_for_en_inference.add(instr_uid)
|
|
|
|
# 5. Final Logic Ordering
|
|
network_logic = [all_logic_steps[uid] for uid in sorted_uids]
|
|
|
|
return {
|
|
"id": network_id,
|
|
"title": network_title,
|
|
"comment": network_comment,
|
|
"logic": network_logic,
|
|
}
|
|
|
|
|
|
# --- Main XML to JSON Conversion Function ---
|
|
# convert_xml_to_json - No significant changes needed from previous version
|
|
def convert_xml_to_json(xml_filepath, json_filepath):
|
|
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
|
|
if not os.path.exists(xml_filepath):
|
|
print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'")
|
|
return
|
|
try:
|
|
print("Paso 1: Parseando archivo XML...")
|
|
# Disable DTD loading for security and compatibility
|
|
parser = etree.XMLParser(
|
|
remove_blank_text=True, load_dtd=False, resolve_entities=False
|
|
)
|
|
tree = etree.parse(xml_filepath, parser)
|
|
root = tree.getroot()
|
|
print("Paso 1: Parseo XML completado.")
|
|
|
|
# Detect block type (FC or FB) - Look for SW.Blocks.FC or SW.Blocks.FB
|
|
block_node = None
|
|
block_xpath = ".//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB']"
|
|
block_list = root.xpath(block_xpath)
|
|
|
|
if not block_list:
|
|
print("Error Crítico: No se encontró <SW.Blocks.FC> o <SW.Blocks.FB>.")
|
|
return
|
|
block_node = block_list[0]
|
|
block_tag_name = etree.QName(
|
|
block_node.tag
|
|
).localname # SW.Blocks.FC or SW.Blocks.FB
|
|
print(
|
|
f"Paso 2: Bloque {block_tag_name} encontrado (ID={block_node.get('ID')})."
|
|
)
|
|
|
|
print("Paso 3: Extrayendo atributos del bloque...")
|
|
attribute_list_node = block_node.xpath("./*[local-name()='AttributeList']")
|
|
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
|
|
if attribute_list_node:
|
|
attr_list = attribute_list_node[0]
|
|
name_node = attr_list.xpath("./*[local-name()='Name']/text()")
|
|
block_name_val = name_node[0].strip() if name_node else block_name_val
|
|
num_node = attr_list.xpath("./*[local-name()='Number']/text()")
|
|
try:
|
|
block_number_val = int(num_node[0]) if num_node else None
|
|
except ValueError:
|
|
block_number_val = None # Handle non-integer Number
|
|
lang_node = attr_list.xpath(
|
|
"./*[local-name()='ProgrammingLanguage']/text()"
|
|
)
|
|
block_lang_val = lang_node[0].strip() if lang_node else block_lang_val
|
|
print(
|
|
f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje='{block_lang_val}'"
|
|
)
|
|
else:
|
|
print("Advertencia: No se encontró AttributeList para el bloque.")
|
|
|
|
# Get block comment
|
|
block_comment_val = ""
|
|
comment_node_list = block_node.xpath(
|
|
"./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']"
|
|
)
|
|
if comment_node_list:
|
|
block_comment_val = get_multilingual_text(comment_node_list[0])
|
|
|
|
# Initialize result dictionary
|
|
result = {
|
|
"block_name": block_name_val,
|
|
"block_number": block_number_val,
|
|
"language": block_lang_val,
|
|
"block_comment": block_comment_val,
|
|
"interface": {},
|
|
"networks": [],
|
|
}
|
|
|
|
print("Paso 4: Extrayendo la interfaz del bloque...")
|
|
if attribute_list_node:
|
|
interface_node = attribute_list_node[0].xpath(
|
|
"./*[local-name()='Interface']"
|
|
)
|
|
if interface_node:
|
|
print("Paso 4: Nodo Interface encontrado.")
|
|
# Iterate through sections using the correct namespace prefix 'iface'
|
|
for section in interface_node[0].xpath(
|
|
".//iface:Section", namespaces=ns
|
|
):
|
|
section_name = section.get(
|
|
"Name"
|
|
) # Input, Output, InOut, Temp, Constant, Return
|
|
members = []
|
|
for member in section.xpath("./iface:Member", namespaces=ns):
|
|
member_name = member.get("Name")
|
|
member_dtype = member.get("Datatype")
|
|
if member_name and member_dtype:
|
|
member_info = {
|
|
"name": member_name,
|
|
"datatype": member_dtype,
|
|
}
|
|
members.append(member_info)
|
|
if members:
|
|
result["interface"][section_name] = members
|
|
if not result["interface"]:
|
|
print("Advertencia: Interface sin secciones iface:Section válidas.")
|
|
else:
|
|
print(
|
|
"Advertencia: No se encontró <Interface> DENTRO de <AttributeList>."
|
|
)
|
|
if not result["interface"]:
|
|
print("Advertencia: No se pudo extraer información de la interfaz.")
|
|
|
|
print("Paso 5: Extrayendo y PROCESANDO lógica de redes (CompileUnits)...")
|
|
networks_processed_count = 0
|
|
object_list_node = block_node.xpath("./*[local-name()='ObjectList']")
|
|
if object_list_node:
|
|
compile_units = object_list_node[0].xpath(
|
|
"./*[local-name()='SW.Blocks.CompileUnit']"
|
|
)
|
|
print(
|
|
f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit."
|
|
)
|
|
for network_elem in compile_units:
|
|
networks_processed_count += 1
|
|
parsed_network = parse_network(network_elem)
|
|
if parsed_network and parsed_network.get("error") is None:
|
|
result["networks"].append(parsed_network)
|
|
elif parsed_network:
|
|
print(
|
|
f"Error: Falló parseo red ID={parsed_network.get('id')}: {parsed_network.get('error')}"
|
|
)
|
|
result["networks"].append(
|
|
parsed_network
|
|
) # Include network with error marker
|
|
else:
|
|
print(
|
|
f"Error Crítico: parse_network devolvió None para CompileUnit (ID={network_elem.get('ID')})."
|
|
)
|
|
|
|
if networks_processed_count == 0:
|
|
print("Advertencia: ObjectList sin SW.Blocks.CompileUnit.")
|
|
else:
|
|
print("Advertencia: No se encontró ObjectList.")
|
|
|
|
print("Paso 6: Escribiendo el resultado en el archivo JSON...")
|
|
if not result["interface"]:
|
|
print("ADVERTENCIA FINAL: 'interface' está vacía.")
|
|
if not result["networks"]:
|
|
print("ADVERTENCIA FINAL: 'networks' está vacía.")
|
|
|
|
try:
|
|
with open(json_filepath, "w", encoding="utf-8") as f:
|
|
json.dump(
|
|
result, f, indent=4, ensure_ascii=False
|
|
) # ensure_ascii=False is important
|
|
print(f"Paso 6: Escritura completada.")
|
|
print(f"Conversión finalizada. JSON guardado en: '{json_filepath}'")
|
|
except IOError as e:
|
|
print(
|
|
f"Error Crítico: No se pudo escribir JSON en '{json_filepath}'. Error: {e}"
|
|
)
|
|
except TypeError as e:
|
|
print(f"Error Crítico: Problema al serializar a JSON. Error: {e}")
|
|
|
|
except etree.XMLSyntaxError as e:
|
|
print(f"Error Crítico: Sintaxis XML en '{xml_filepath}'. Detalles: {e}")
|
|
except Exception as e:
|
|
print(f"Error Crítico: Error inesperado durante la conversión: {e}")
|
|
print("--- Traceback ---")
|
|
traceback.print_exc()
|
|
print("--- Fin Traceback ---")
|
|
|
|
|
|
# --- Punto de Entrada Principal ---
|
|
if __name__ == "__main__":
|
|
xml_file = "BlenderCtrl__Main.xml"
|
|
json_file = xml_file.replace(".xml", "_simplified.json")
|
|
convert_xml_to_json(xml_file, json_file)
|