import sys import os from lxml import etree def debug_print(message, enabled=True): """Print debug messages if enabled.""" if enabled: print(f"DEBUG: {message}") def parse_siemens_lad_to_scl(xml_file, debug=True): """ Parse a Siemens LAD/FUP XML file and convert to SCL using lxml. Args: xml_file: Path to the Siemens XML file debug: Enable debug output Returns: String containing the SCL equivalent code """ try: # Parse the XML file with lxml parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(xml_file, parser) root = tree.getroot() # Extract namespaces nsmap = root.nsmap debug_print(f"Namespaces: {nsmap}", debug) # Create namespace dictionary for XPath queries ns = {} default_ns = nsmap.get(None, "") if default_ns: ns["d"] = default_ns # Add other namespaces for prefix, uri in nsmap.items(): if prefix is not None: ns[prefix] = uri debug_print(f"Namespace dictionary: {ns}", debug) # Find the block type (FC, FB, OB) block = None block_type = None for block_tag in ["SW.Blocks.FC", "SW.Blocks.FB", "SW.Blocks.OB"]: # Try with and without namespace block = root.find(f".//{block_tag}") if block is None and "d" in ns: block = root.find(f".//d:{block_tag}", namespaces=ns) if block is not None: block_type = block_tag.split(".")[-1] debug_print(f"Found block of type {block_type}", debug) break if block is None: return "Error: No supported block (FC, FB, OB) found in the XML file." # Extract block information block_name = block.find(".//Name").text block_number = block.find(".//Number").text programming_language = block.find(".//ProgrammingLanguage").text debug_print( f"Block name: {block_name}, Number: {block_number}, Language: {programming_language}", debug, ) # Start SCL code generation scl_code = [ f"// SCL equivalent of {programming_language} block: {block_name} ({block_type} {block_number})" ] # Extract block title title_element = block.find(".//MultilingualText[@CompositionName='Title']") if title_element is not None: # Try to find title in any available language for text_item in title_element.findall(".//MultilingualTextItem"): culture_elem = text_item.find(".//Culture") text_elem = text_item.find(".//Text") if text_elem is not None and text_elem.text: culture = ( culture_elem.text if culture_elem is not None else "unknown" ) debug_print(f"Found title in {culture}: {text_elem.text}", debug) scl_code.append(f"// Title: {text_elem.text}") break # Generate block header if block_type == "FC": scl_code.append(f'FUNCTION "{block_name}" : VOID') elif block_type == "FB": scl_code.append(f'FUNCTION_BLOCK "{block_name}"') elif block_type == "OB": scl_code.append(f'ORGANIZATION_BLOCK "{block_name}"') # Add BEGIN marker scl_code.append("BEGIN") # Process each network compile_units = block.findall(".//SW.Blocks.CompileUnit") debug_print(f"Found {len(compile_units)} networks", debug) for i, unit in enumerate(compile_units): debug_print(f"Processing network {i+1}", debug) # Get network title title_element = unit.find(".//MultilingualText[@CompositionName='Title']") network_title = f"Network {i+1}" if title_element is not None: for text_item in title_element.findall(".//MultilingualTextItem"): culture_elem = text_item.find(".//Culture") text_elem = text_item.find(".//Text") if text_elem is not None and text_elem.text: culture = ( culture_elem.text if culture_elem is not None else "unknown" ) network_title = f"Network {i+1}: {text_elem.text}" debug_print( f"Network title ({culture}): {network_title}", debug ) break scl_code.append(f" // {network_title}") # Get network source network_source_elem = unit.find(".//NetworkSource") if network_source_elem is None: debug_print(f"No NetworkSource found for network {i+1}", debug) continue # Look for FlgNet - the container for LAD/FUP network logic flg_net = None # Try direct search flg_net = network_source_elem.find("FlgNet") # Try with namespace if flg_net is None and "d" in ns: flg_net = network_source_elem.find("d:FlgNet", namespaces=ns) # Try using full XPath with any namespace if flg_net is None: for elem in network_source_elem: if elem.tag.endswith("FlgNet"): flg_net = elem break if flg_net is None: debug_print(f"No FlgNet found for network {i+1}", debug) continue # Get FlgNet namespace - THIS IS THE KEY FIX flgnet_ns = {} if flg_net.nsmap: debug_print(f"FlgNet has its own namespace: {flg_net.nsmap}", debug) # Add namespace with 'flg' prefix for prefix, uri in flg_net.nsmap.items(): if prefix is None: flgnet_ns["flg"] = uri else: flgnet_ns[prefix] = uri # Extract Parts and Wires sections - try with proper namespace handling parts = None wires = None # Try different methods to find Parts and Wires # Method 1: Direct access parts = flg_net.find("Parts") wires = flg_net.find("Wires") # Method 2: With FlgNet namespace if (parts is None or wires is None) and flgnet_ns: parts = parts or flg_net.find("flg:Parts", namespaces=flgnet_ns) wires = wires or flg_net.find("flg:Wires", namespaces=flgnet_ns) # Method 3: Try XPath with any namespace if parts is None: for child in flg_net: if child.tag.endswith("Parts"): parts = child break if wires is None: for child in flg_net: if child.tag.endswith("Wires"): wires = child break if parts is None: debug_print("No Parts element found - check XML namespace", debug) debug_print( f"FlgNet children: {[child.tag for child in flg_net]}", debug ) continue if wires is None: debug_print("No Wires element found - check XML namespace", debug) continue debug_print(f"Successfully found Parts and Wires elements", debug) # Process function calls (CALL instructions) call_elements = [] # Try both direct and with namespace call_elements.extend(parts.findall("Call")) if flgnet_ns: call_elements.extend(parts.findall("flg:Call", namespaces=flgnet_ns)) # Try XPath with any namespace if not call_elements: for child in parts: if child.tag.endswith("Call"): call_elements.append(child) debug_print(f"Found {len(call_elements)} function calls", debug) for call in call_elements: call_uid = call.get("UId") # Try both direct and with namespace for CallInfo call_info = call.find("CallInfo") or ( call.find("flg:CallInfo", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if still not found if call_info is None: for child in call: if child.tag.endswith("CallInfo"): call_info = child break if call_info is None: debug_print( f"No CallInfo found for call with UId {call_uid}", debug ) continue function_name = call_info.get("Name") block_type = call_info.get("BlockType") debug_print(f"Found call to {function_name} ({block_type})", debug) # Check if this call is connected to a powerrail (unconditional execution) is_enabled = False wire_elements = [] wire_elements.extend(wires.findall("Wire")) if flgnet_ns: wire_elements.extend( wires.findall("flg:Wire", namespaces=flgnet_ns) ) # Try XPath with any namespace if not wire_elements: for child in wires: if child.tag.endswith("Wire"): wire_elements.append(child) for wire in wire_elements: # Check if wire has a powerrail source powerrail = wire.find("Powerrail") or ( wire.find("flg:Powerrail", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if powerrail is None: for child in wire: if child.tag.endswith("Powerrail"): powerrail = child break if powerrail is None: continue # Check if wire connects to this call's enable input name_con = wire.find(f"NameCon[@UId='{call_uid}'][@Name='en']") or ( wire.find( f"flg:NameCon[@UId='{call_uid}'][@Name='en']", namespaces=flgnet_ns, ) if flgnet_ns else None ) # Try XPath with any namespace if name_con is None: for child in wire: if ( child.tag.endswith("NameCon") and child.get("UId") == call_uid and child.get("Name") == "en" ): name_con = child break if name_con is not None: is_enabled = True debug_print( f"Call to {function_name} is directly enabled by powerrail", debug, ) break if is_enabled: # Generate code for function call scl_code.append(f" {function_name}();") # Process MOVE operations move_elements = [] move_elements.extend(parts.findall("Part[@Name='Move']")) if flgnet_ns: move_elements.extend( parts.findall("flg:Part[@Name='Move']", namespaces=flgnet_ns) ) # Try XPath with any namespace if not move_elements: for child in parts: if child.tag.endswith("Part") and child.get("Name") == "Move": move_elements.append(child) debug_print(f"Found {len(move_elements)} MOVE operations", debug) for move in move_elements: move_uid = move.get("UId") debug_print(f"Processing MOVE with UId {move_uid}", debug) # Check if MOVE is directly enabled is_enabled = False for wire in wire_elements: powerrail = wire.find("Powerrail") or ( wire.find("flg:Powerrail", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if powerrail is None: for child in wire: if child.tag.endswith("Powerrail"): powerrail = child break if powerrail is None: continue name_con = wire.find(f"NameCon[@UId='{move_uid}'][@Name='en']") or ( wire.find( f"flg:NameCon[@UId='{move_uid}'][@Name='en']", namespaces=flgnet_ns, ) if flgnet_ns else None ) # Try XPath with any namespace if name_con is None: for child in wire: if ( child.tag.endswith("NameCon") and child.get("UId") == move_uid and child.get("Name") == "en" ): name_con = child break if name_con is not None: is_enabled = True debug_print("MOVE is directly enabled by powerrail", debug) break if not is_enabled: debug_print("MOVE is not directly enabled, skipping", debug) continue # Find source value for MOVE source_uid = None for wire in wire_elements: # Find wire that connects to this MOVE's input name_con = wire.find(f"NameCon[@UId='{move_uid}'][@Name='in']") or ( wire.find( f"flg:NameCon[@UId='{move_uid}'][@Name='in']", namespaces=flgnet_ns, ) if flgnet_ns else None ) # Try XPath with any namespace if name_con is None: for child in wire: if ( child.tag.endswith("NameCon") and child.get("UId") == move_uid and child.get("Name") == "in" ): name_con = child break if name_con is None: continue # Get source identifier from wire ident_con = wire.find("IdentCon") or ( wire.find("flg:IdentCon", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if ident_con is None: for child in wire: if child.tag.endswith("IdentCon"): ident_con = child break if ident_con is not None: source_uid = ident_con.get("UId") debug_print(f"MOVE input connected to UId {source_uid}", debug) break # Find source value source_value = None if source_uid: source_access = parts.find(f"Access[@UId='{source_uid}']") or ( parts.find( f"flg:Access[@UId='{source_uid}']", namespaces=flgnet_ns ) if flgnet_ns else None ) # Try XPath with any namespace if source_access is None: for child in parts: if ( child.tag.endswith("Access") and child.get("UId") == source_uid ): source_access = child break if source_access is not None: scope = source_access.get("Scope") debug_print(f"Source access scope: {scope}", debug) if scope == "LiteralConstant": # For literal constants - try with namespace constant = source_access.find(".//Constant") or ( source_access.find( ".//flg:Constant", namespaces=flgnet_ns ) if flgnet_ns else None ) # Try XPath with any namespace if constant is None: for elem in source_access.iter(): if elem.tag.endswith("Constant"): constant = elem break if constant is not None: # Try with namespace for child elements const_type = constant.find("ConstantType") or ( constant.find( "flg:ConstantType", namespaces=flgnet_ns ) if flgnet_ns else None ) const_value = constant.find("ConstantValue") or ( constant.find( "flg:ConstantValue", namespaces=flgnet_ns ) if flgnet_ns else None ) # Try XPath with any namespace if const_value is None: for child in constant: if child.tag.endswith("ConstantValue"): const_value = child break if const_value is not None: source_value = const_value.text debug_print( f"Source value is constant: {source_value}", debug, ) elif scope == "GlobalVariable": # For variables - try with namespace symbol = source_access.find("Symbol") or ( source_access.find("flg:Symbol", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if symbol is None: for child in source_access: if child.tag.endswith("Symbol"): symbol = child break if symbol is not None: # Try with namespace for components components = symbol.findall("Component") or ( symbol.findall( "flg:Component", namespaces=flgnet_ns ) if flgnet_ns else [] ) # Try XPath with any namespace if not components: for child in symbol: if child.tag.endswith("Component"): components.append(child) if components: source_value = ".".join( [comp.get("Name") for comp in components] ) debug_print( f"Source value is variable: {source_value}", debug, ) # Find destination for MOVE dest_uid = None for wire in wire_elements: # Find wire that connects from this MOVE's output name_con = wire.find( f"NameCon[@UId='{move_uid}'][@Name='out1']" ) or ( wire.find( f"flg:NameCon[@UId='{move_uid}'][@Name='out1']", namespaces=flgnet_ns, ) if flgnet_ns else None ) # Try XPath with any namespace if name_con is None: for child in wire: if ( child.tag.endswith("NameCon") and child.get("UId") == move_uid and child.get("Name") == "out1" ): name_con = child break if name_con is None: continue # Get destination identifier from wire ident_con = wire.find("IdentCon") or ( wire.find("flg:IdentCon", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if ident_con is None: for child in wire: if child.tag.endswith("IdentCon"): ident_con = child break if ident_con is not None: dest_uid = ident_con.get("UId") debug_print(f"MOVE output connected to UId {dest_uid}", debug) break # Find destination value dest_value = None if dest_uid: dest_access = parts.find(f"Access[@UId='{dest_uid}']") or ( parts.find( f"flg:Access[@UId='{dest_uid}']", namespaces=flgnet_ns ) if flgnet_ns else None ) # Try XPath with any namespace if dest_access is None: for child in parts: if ( child.tag.endswith("Access") and child.get("UId") == dest_uid ): dest_access = child break if ( dest_access is not None and dest_access.get("Scope") == "GlobalVariable" ): symbol = dest_access.find("Symbol") or ( dest_access.find("flg:Symbol", namespaces=flgnet_ns) if flgnet_ns else None ) # Try XPath with any namespace if symbol is None: for child in dest_access: if child.tag.endswith("Symbol"): symbol = child break if symbol is not None: components = symbol.findall("Component") or ( symbol.findall("flg:Component", namespaces=flgnet_ns) if flgnet_ns else [] ) # Try XPath with any namespace if not components: for child in symbol: if child.tag.endswith("Component"): components.append(child) if components: dest_value = ".".join( [comp.get("Name") for comp in components] ) debug_print( f"Destination value is: {dest_value}", debug ) # Generate SCL for assignment if source_value is not None and dest_value is not None: scl_code.append(f" {dest_value} := {source_value};") else: debug_print("Could not resolve complete MOVE operation", debug) # Close the block if block_type == "FC": scl_code.append("END_FUNCTION;") elif block_type == "FB": scl_code.append("END_FUNCTION_BLOCK;") elif block_type == "OB": scl_code.append("END_ORGANIZATION_BLOCK;") return "\n".join(scl_code) except Exception as e: import traceback debug_print(f"Exception occurred: {str(e)}", debug) debug_print(traceback.format_exc(), debug) return f"Error converting to SCL: {str(e)}" # Main execution if __name__ == "__main__": if len(sys.argv) > 1: input_file = sys.argv[1] if os.path.exists(input_file): scl = parse_siemens_lad_to_scl(input_file) print(scl) else: print(f"Error: File {input_file} not found") else: print("Usage: python script.py ")