# -*- 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ó o .") 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ó DENTRO de ." ) 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)