380 lines
15 KiB
Python
380 lines
15 KiB
Python
"""
|
|
export_CAx_from_tia : Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import xml.etree.ElementTree as ET # Library to parse XML (AML)
|
|
from pathlib import Path # Import Path
|
|
|
|
script_root = os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
)
|
|
sys.path.append(script_root)
|
|
from backend.script_utils import load_configuration
|
|
|
|
# --- Configuration ---
|
|
# Supported TIA Portal versions mapping (extension -> version)
|
|
SUPPORTED_TIA_VERSIONS = {".ap18": "18.0", ".ap19": "19.0", ".ap20": "20.0"}
|
|
|
|
# --- TIA Scripting Import Handling ---
|
|
# (Same import handling as the previous script)
|
|
if os.getenv("TIA_SCRIPTING"):
|
|
sys.path.append(os.getenv("TIA_SCRIPTING"))
|
|
else:
|
|
pass
|
|
|
|
try:
|
|
import siemens_tia_scripting as ts
|
|
except ImportError:
|
|
print("ERROR: Failed to import 'siemens_tia_scripting'.")
|
|
print("Ensure TIA Openness, the module, and Python 3.12.X are set up.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred during import: {e}")
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
# --- Functions ---
|
|
|
|
|
|
def find_project_file_in_dir(directory):
|
|
"""Finds a TIA project file in the given directory."""
|
|
project_files = []
|
|
supported_extensions = SUPPORTED_TIA_VERSIONS.keys()
|
|
for ext in supported_extensions:
|
|
project_files.extend(list(Path(directory).glob(f"*{ext}")))
|
|
|
|
if len(project_files) == 1:
|
|
return project_files[0]
|
|
elif len(project_files) == 0:
|
|
print(f"ERROR: No TIA Portal project file found in {directory}.")
|
|
print(f"Supported extensions: {list(supported_extensions)}")
|
|
return None
|
|
else:
|
|
print(f"ERROR: Multiple TIA Portal project files found in {directory}:")
|
|
for f in project_files:
|
|
print(f" - {f}")
|
|
print("Please ensure only one project file exists in the directory.")
|
|
return None
|
|
|
|
|
|
def detect_tia_version(project_file_path):
|
|
"""Detects TIA Portal version based on file extension."""
|
|
file_path = Path(project_file_path)
|
|
file_extension = file_path.suffix.lower()
|
|
|
|
if file_extension in SUPPORTED_TIA_VERSIONS:
|
|
detected_version = SUPPORTED_TIA_VERSIONS[file_extension]
|
|
print(
|
|
f"Detected TIA Portal version: {detected_version} (from extension {file_extension})"
|
|
)
|
|
return detected_version
|
|
else:
|
|
print(
|
|
f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}"
|
|
)
|
|
# Default to version 18.0 for backward compatibility
|
|
print("Defaulting to TIA Portal V18.0")
|
|
return "18.0"
|
|
|
|
|
|
def find_elements(element, path):
|
|
"""Helper to find elements using namespaces commonly found in AML."""
|
|
# AutomationML namespaces often vary slightly or might be default
|
|
# This basic approach tries common prefixes or no prefix
|
|
namespaces = {
|
|
"": (
|
|
element.tag.split("}")[0][1:] if "}" in element.tag else ""
|
|
), # Default namespace if present
|
|
"caex": "http://www.dke.de/CAEX", # Common CAEX namespace
|
|
# Add other potential namespaces if needed based on file inspection
|
|
}
|
|
# Try finding with common prefixes or the default namespace
|
|
for prefix, uri in namespaces.items():
|
|
# Construct path with namespace URI if prefix is defined
|
|
namespaced_path = path
|
|
if prefix:
|
|
parts = path.split("/")
|
|
namespaced_parts = [
|
|
f"{{{uri}}}{part}" if part != "." else part for part in parts
|
|
]
|
|
namespaced_path = "/".join(namespaced_parts)
|
|
|
|
# Try findall with the constructed path
|
|
found = element.findall(namespaced_path)
|
|
if found:
|
|
return found # Return first successful find
|
|
|
|
# Fallback: try finding without explicit namespace (might work if default ns is used throughout)
|
|
# This might require adjusting the path string itself depending on the XML structure
|
|
try:
|
|
# Simple attempt without namespace handling if the above fails
|
|
return element.findall(path)
|
|
except (
|
|
SyntaxError
|
|
): # Handle potential errors if path isn't valid without namespaces
|
|
return []
|
|
|
|
|
|
def parse_aml_to_markdown(aml_file_path, md_file_path):
|
|
"""Parses the AML file and generates a Markdown summary."""
|
|
print(f"Parsing AML file: {aml_file_path}")
|
|
try:
|
|
tree = ET.parse(aml_file_path)
|
|
root = tree.getroot()
|
|
|
|
markdown_lines = ["# Project CAx Data Summary (AutomationML)", ""]
|
|
|
|
# Find InstanceHierarchy - usually contains the project structure
|
|
# Note: Namespace handling in ElementTree can be tricky. Adjust '{...}' part if needed.
|
|
# We will use a helper function 'find_elements' to try common patterns
|
|
instance_hierarchies = find_elements(
|
|
root, ".//InstanceHierarchy"
|
|
) # Common CAEX tag
|
|
|
|
if not instance_hierarchies:
|
|
markdown_lines.append("Could not find InstanceHierarchy in the AML file.")
|
|
print("Warning: Could not find InstanceHierarchy element.")
|
|
else:
|
|
# Assuming the first InstanceHierarchy is the main one
|
|
ih = instance_hierarchies[0]
|
|
markdown_lines.append(f"## Instance Hierarchy: {ih.get('Name', 'N/A')}")
|
|
markdown_lines.append("")
|
|
|
|
# Look for InternalElements which represent devices/components
|
|
internal_elements = find_elements(
|
|
ih, ".//InternalElement"
|
|
) # Common CAEX tag
|
|
|
|
if not internal_elements:
|
|
markdown_lines.append(
|
|
"No devices (InternalElement) found in InstanceHierarchy."
|
|
)
|
|
print("Info: No InternalElement tags found under InstanceHierarchy.")
|
|
else:
|
|
markdown_lines.append(
|
|
f"Found {len(internal_elements)} device(s)/component(s):"
|
|
)
|
|
markdown_lines.append("")
|
|
markdown_lines.append(
|
|
"| Name | SystemUnitClass | RefBaseSystemUnitPath | Attributes |"
|
|
)
|
|
markdown_lines.append("|---|---|---|---|")
|
|
|
|
for elem in internal_elements:
|
|
name = elem.get("Name", "N/A")
|
|
ref_path = elem.get(
|
|
"RefBaseSystemUnitPath", "N/A"
|
|
) # Path to class definition
|
|
|
|
# Try to get the class name from the RefBaseSystemUnitPath or SystemUnitClassLib
|
|
su_class_path = find_elements(
|
|
elem, ".//SystemUnitClass"
|
|
) # Check direct child first
|
|
su_class = (
|
|
su_class_path[0].get("Path", "N/A")
|
|
if su_class_path
|
|
else ref_path.split("/")[-1]
|
|
) # Fallback to last part of path
|
|
|
|
attributes_md = ""
|
|
attributes = find_elements(elem, ".//Attribute") # Find attributes
|
|
attr_list = []
|
|
for attr in attributes:
|
|
attr_name = attr.get("Name", "")
|
|
attr_value_elem = find_elements(
|
|
attr, ".//Value"
|
|
) # Get Value element
|
|
attr_value = (
|
|
attr_value_elem[0].text
|
|
if attr_value_elem and attr_value_elem[0].text
|
|
else "N/A"
|
|
)
|
|
|
|
# Look for potential IP addresses (common attribute names)
|
|
if "Address" in attr_name or "IP" in attr_name:
|
|
attr_list.append(f"**{attr_name}**: {attr_value}")
|
|
else:
|
|
attr_list.append(f"{attr_name}: {attr_value}")
|
|
|
|
attributes_md = "<br>".join(attr_list) if attr_list else "None"
|
|
|
|
markdown_lines.append(
|
|
f"| {name} | {su_class} | `{ref_path}` | {attributes_md} |"
|
|
)
|
|
|
|
# Write to Markdown file
|
|
with open(md_file_path, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(markdown_lines))
|
|
print(f"Markdown summary written to: {md_file_path}")
|
|
|
|
except ET.ParseError as xml_err:
|
|
print(f"ERROR parsing XML file {aml_file_path}: {xml_err}")
|
|
with open(md_file_path, "w", encoding="utf-8") as f:
|
|
f.write(
|
|
f"# Error\n\nFailed to parse AML file: {os.path.basename(aml_file_path)}\n\nError: {xml_err}"
|
|
)
|
|
except Exception as e:
|
|
print(f"ERROR processing AML file {aml_file_path}: {e}")
|
|
traceback.print_exc()
|
|
with open(md_file_path, "w", encoding="utf-8") as f:
|
|
f.write(
|
|
f"# Error\n\nAn unexpected error occurred while processing AML file: {os.path.basename(aml_file_path)}\n\nError: {e}"
|
|
)
|
|
|
|
|
|
# --- Main Script ---
|
|
|
|
if __name__ == "__main__":
|
|
configs = load_configuration()
|
|
|
|
# Get parameters from configuration file
|
|
level3_configs = configs.get("level3", {})
|
|
level2_configs = configs.get("level2", {})
|
|
siemens_tia_project_dir = level3_configs.get("siemens_tia_project")
|
|
working_directory = configs.get("working_directory")
|
|
aml_exp_directory = level2_configs.get("aml_exp_directory")
|
|
resultados_exp_directory = level2_configs.get("resultados_exp_directory")
|
|
|
|
print("--- TIA Portal Project CAx Exporter and Analyzer ---")
|
|
|
|
# Validate working directory
|
|
if not working_directory or not os.path.isdir(working_directory):
|
|
print("ERROR: Working directory not set or invalid in configuration.")
|
|
print("Please configure the working directory using the main application.")
|
|
sys.exit(1)
|
|
|
|
if not aml_exp_directory:
|
|
print("ERROR: aml_exp_directory not set in level2 configuration.")
|
|
sys.exit(1)
|
|
|
|
if not resultados_exp_directory:
|
|
print("ERROR: resultados_exp_directory not set in level2 configuration.")
|
|
sys.exit(1)
|
|
|
|
# Validate TIA project directory from config
|
|
if not siemens_tia_project_dir or not os.path.isdir(siemens_tia_project_dir):
|
|
print("ERROR: TIA project directory is not configured or invalid.")
|
|
print(
|
|
'Please set the "siemens_tia_project" path in your configuration (work_dir.json).'
|
|
)
|
|
sys.exit(1)
|
|
|
|
# 1. Find Project File in the configured directory
|
|
project_file = find_project_file_in_dir(siemens_tia_project_dir)
|
|
if not project_file:
|
|
sys.exit(1) # Error message is printed inside the function
|
|
|
|
aml_output_dir = Path(working_directory) / aml_exp_directory
|
|
md_output_dir = Path(working_directory) / resultados_exp_directory
|
|
|
|
print(f"\nSelected Project: {project_file}")
|
|
print(f"Using AML Output Directory: {aml_output_dir}")
|
|
print(f"Using Results Output Directory: {md_output_dir}")
|
|
|
|
# 2. Detect TIA Portal version from project file
|
|
tia_version = detect_tia_version(project_file)
|
|
|
|
# Define output file names using Path object
|
|
project_path = Path(project_file)
|
|
project_base_name = project_path.stem # Get filename without extension
|
|
aml_file = aml_output_dir / f"{project_base_name}_CAx_Export.aml"
|
|
md_file = md_output_dir / f"{project_base_name}_CAx_Summary.md"
|
|
log_file = (
|
|
aml_output_dir / f"{project_base_name}_CAx_Export.log"
|
|
) # Log file for the export process
|
|
|
|
print(f"Will export CAx data to: {aml_file}")
|
|
print(f"Will generate summary to: {md_file}")
|
|
print(f"Export log file: {log_file}")
|
|
|
|
portal_instance = None
|
|
project_object = None
|
|
cax_export_successful = False
|
|
|
|
try:
|
|
# 3. Connect to TIA Portal with detected version
|
|
print(f"\nConnecting to TIA Portal V{tia_version}...")
|
|
portal_instance = ts.open_portal(
|
|
version=tia_version,
|
|
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
|
|
)
|
|
print("Connected.")
|
|
|
|
# 4. Open Project
|
|
print(
|
|
f"Opening project: {project_path.name}..."
|
|
) # Use Path object's name attribute
|
|
project_object = portal_instance.open_project(
|
|
project_file_path=str(project_path)
|
|
) # Pass path as string
|
|
if project_object is None:
|
|
print("Project might already be open, attempting to get handle...")
|
|
project_object = portal_instance.get_project()
|
|
if project_object is None:
|
|
raise Exception("Failed to open or get the specified project.")
|
|
print("Project opened.")
|
|
|
|
# 5. Export CAx Data (Project Level)
|
|
print(f"Exporting CAx data for the project to {aml_file}...")
|
|
# Ensure output directories exist
|
|
aml_output_dir.mkdir(parents=True, exist_ok=True)
|
|
md_output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Delete existing files to allow overwrite by TIA Portal
|
|
if aml_file.exists():
|
|
print(f"Deleting existing AML file to allow overwrite: {aml_file}")
|
|
aml_file.unlink()
|
|
if log_file.exists():
|
|
print(f"Deleting existing log file to allow overwrite: {log_file}")
|
|
log_file.unlink()
|
|
|
|
# Pass paths as strings to the TIA function
|
|
export_result = project_object.export_cax_data(
|
|
export_file_path=str(aml_file), log_file_path=str(log_file)
|
|
)
|
|
|
|
if export_result:
|
|
print("CAx data exported successfully.")
|
|
cax_export_successful = True
|
|
else:
|
|
print("CAx data export failed. Check the log file for details:")
|
|
print(f" Log file: {log_file}")
|
|
# Write basic error message to MD file if export fails
|
|
with open(md_file, "w", encoding="utf-8") as f:
|
|
f.write(
|
|
f"# Error\n\nCAx data export failed. Check log file: {log_file}"
|
|
)
|
|
|
|
except FileNotFoundError:
|
|
print(f"\nERROR: Project file not found at {project_file}")
|
|
except Exception as e:
|
|
print(f"\nAn unexpected error occurred during TIA interaction: {e}")
|
|
traceback.print_exc()
|
|
finally:
|
|
# Close TIA Portal before processing the file (or detach)
|
|
if portal_instance:
|
|
try:
|
|
print("\nClosing TIA Portal...")
|
|
portal_instance.close_portal()
|
|
print("TIA Portal closed.")
|
|
except Exception as close_ex:
|
|
print(f"Error during TIA Portal cleanup: {close_ex}")
|
|
|
|
# 6. Parse AML and Generate Markdown (only if export was successful)
|
|
if cax_export_successful:
|
|
if aml_file.exists(): # Use Path object's exists() method
|
|
parse_aml_to_markdown(aml_file, md_file)
|
|
else:
|
|
print(
|
|
f"ERROR: Export was reported successful, but AML file not found at {aml_file}"
|
|
)
|
|
with open(md_file, "w", encoding="utf-8") as f:
|
|
f.write(
|
|
f"# Error\n\nExport was reported successful, but AML file not found:\n{aml_file}"
|
|
)
|
|
|
|
print("\nScript finished.")
|