""" export_CAx_from_tia : Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown. """ import tkinter as tk from tkinter import filedialog 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 get_supported_filetypes(): """Returns the supported file types for TIA Portal projects.""" filetypes = [] for ext, version in SUPPORTED_TIA_VERSIONS.items(): version_major = version.split('.')[0] filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}")) # Add option to show all supported files all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()]) filetypes.insert(0, ("All TIA Portal Projects", all_extensions)) return filetypes 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 select_project_file(): """Opens a dialog to select a TIA Portal project file.""" root = tk.Tk() root.withdraw() file_path = filedialog.askopenfilename( title="Select TIA Portal Project File", filetypes=get_supported_filetypes(), ) root.destroy() if not file_path: print("No project file selected. Exiting.") sys.exit(0) return file_path def select_output_directory(): """Opens a dialog to select the output directory.""" root = tk.Tk() root.withdraw() dir_path = filedialog.askdirectory( title="Select Output Directory for AML and MD files" ) root.destroy() if not dir_path: print("No output directory selected. Exiting.") sys.exit(0) return dir_path 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 = "
".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() working_directory = configs.get("working_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) # 1. Select Project File, Output Directory comes from config project_file = select_project_file() output_dir = Path( working_directory ) # Use working directory from config, ensure it's a Path object print(f"\nSelected Project: {project_file}") print(f"Using Output Directory (Working Directory): {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 = output_dir / f"{project_base_name}_CAx_Export.aml" md_file = output_dir / f"{project_base_name}_CAx_Summary.md" log_file = ( 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 directory exists (Path.mkdir handles this implicitly if needed later, but good practice) output_dir.mkdir(parents=True, exist_ok=True) # 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 ts.TiaException as tia_ex: print(f"\nTIA Portal Openness Error: {tia_ex}") traceback.print_exc() 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.")