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) # --- Configuration --- TIA_PORTAL_VERSION = "18.0" # Target TIA Portal version # --- 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 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=[(f"TIA Portal V{TIA_PORTAL_VERSION} Projects", f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}")] ) 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__": print("--- TIA Portal Project CAx Exporter and Analyzer ---") # 1. Select Files/Folders project_file = select_project_file() output_dir = select_output_directory() print(f"\nSelected Project: {project_file}") print(f"Selected Output Directory: {output_dir}") # Define output file names project_base_name = os.path.splitext(os.path.basename(project_file))[0] aml_file = os.path.join(output_dir, f"{project_base_name}_CAx_Export.aml") md_file = os.path.join(output_dir, f"{project_base_name}_CAx_Summary.md") log_file = os.path.join(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: # 2. Connect to TIA Portal print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...") portal_instance = ts.open_portal( version=TIA_PORTAL_VERSION, portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface ) print("Connected.") # 3. Open Project print(f"Opening project: {os.path.basename(project_file)}...") project_object = portal_instance.open_project(project_file_path=project_file) if project_object is None: project_object = portal_instance.get_project() if project_object is None: raise Exception("Failed to open or get the specified project.") print("Project opened.") # 4. Export CAx Data (Project Level) print(f"Exporting CAx data for the project to {aml_file}...") # Ensure output directory exists for the log file as well os.makedirs(os.path.dirname(log_file), exist_ok=True) export_result = project_object.export_cax_data(export_file_path=aml_file, log_file_path=log_file) # [cite: 361] 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}") # 5. Parse AML and Generate Markdown (only if export was successful) if cax_export_successful: if os.path.exists(aml_file): 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.")