707 lines
28 KiB
Python
707 lines
28 KiB
Python
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
|
|
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)
|
|
|
|
# Get all wire elements
|
|
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)
|
|
|
|
# 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
|
|
|
|
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
|
|
|
|
# Debug all wires to find the one connected to this move
|
|
for wire_idx, wire in enumerate(wire_elements):
|
|
debug_print(f"Checking wire {wire_idx} for MOVE {move_uid}", debug)
|
|
|
|
# Check for powerrail
|
|
powerrail = None
|
|
for child in wire:
|
|
if child.tag.endswith("Powerrail"):
|
|
powerrail = child
|
|
debug_print(f" Found powerrail in wire {wire_idx}", debug)
|
|
break
|
|
|
|
if powerrail is None:
|
|
continue
|
|
|
|
# Check for connection to this move
|
|
name_con = None
|
|
for child in wire:
|
|
if child.tag.endswith("NameCon"):
|
|
debug_print(
|
|
f" Found NameCon: UId={child.get('UId')}, Name={child.get('Name')}",
|
|
debug,
|
|
)
|
|
if (
|
|
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(
|
|
f"MOVE with UId {move_uid} is directly enabled by powerrail in wire {wire_idx}",
|
|
debug,
|
|
)
|
|
break
|
|
|
|
if not is_enabled:
|
|
debug_print(
|
|
f"MOVE with UId {move_uid} is not directly enabled, trying alternative search",
|
|
debug,
|
|
)
|
|
|
|
# Try a more direct XML search
|
|
for wire in wire_elements:
|
|
wire_str = etree.tostring(wire).decode("utf-8")
|
|
if (
|
|
f'UId="{move_uid}"' in wire_str
|
|
and 'Name="en"' in wire_str
|
|
and "<Powerrail" in wire_str
|
|
):
|
|
is_enabled = True
|
|
debug_print(
|
|
f"Found MOVE {move_uid} enabled by powerrail using string search",
|
|
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 <xml_file>")
|