#!/usr/bin/env python3 """ Batch PLC Generation and Compilation Manager =========================================== Automatically discovers and processes all available Excel data files: 1. Scans PLC Data Generator/data for Excel files 2. Extracts project information from filenames 3. Runs complete workflow for each project 4. Timestamps generated L5X files 5. Sets up compilation for all generated projects Supports patterns: - IO Assignment_PROJECT_MCM##_COMPLETE.xlsm (MTN6 projects) - PROJECT_MCM##.xlsx (SAT9 projects) """ import re import sys import shutil import subprocess from pathlib import Path from datetime import datetime from typing import List, Dict, Tuple, Optional import xml.etree.ElementTree as ET def get_project_root() -> Path: """Get the project root directory.""" return Path(__file__).parent.resolve() def discover_excel_files(data_dir: Path) -> List[Dict]: """Discover all Excel files and extract project information. Returns: List of project dictionaries with keys: file_path, project_prefix, project_name, mcm_number """ if not data_dir.exists(): print(f"❌ Data directory not found: {data_dir}") return [] projects = [] # Pattern 1: IO Assignment_PROJECT_MCM##_COMPLETE.xlsx(m) (legacy format) pattern1 = re.compile(r'^IO Assignment_([A-Z0-9]+)_(MCM\d+)_COMPLETE\.(xlsx?m?)$', re.IGNORECASE) # Pattern 2: PROJECT_MCM##.xlsx/xlsm (current format) pattern2 = re.compile(r'^([A-Z0-9]+)_(MCM\d+)\.(xlsx?m?)$', re.IGNORECASE) for excel_file in data_dir.glob('*.xls*'): filename = excel_file.name # Try pattern 1 first (legacy format) match = pattern1.match(filename) if match: project_prefix = match.group(1).upper() mcm_number = match.group(2).upper() project_name = f"{project_prefix}_{mcm_number}" projects.append({ 'file_path': excel_file, 'project_prefix': project_prefix, 'project_name': project_name, 'mcm_number': mcm_number, 'pattern': 'IO Assignment (Legacy)' }) continue # Try pattern 2 (current format) match = pattern2.match(filename) if match: project_prefix = match.group(1).upper() mcm_number = match.group(2).upper() project_name = f"{project_prefix}_{mcm_number}" projects.append({ 'file_path': excel_file, 'project_prefix': project_prefix, 'project_name': project_name, 'mcm_number': mcm_number, 'pattern': 'Direct (Current)' }) continue print(f"⚠️ Unrecognized file pattern: {filename}") return projects def validate_project_configs(project_root: Path, projects: List[Dict]) -> List[Dict]: """Validate that required config files exist for each project. Returns: List of projects that have valid configurations """ valid_projects = [] for project in projects: prefix = project['project_prefix'] generator_config = project_root / f"{prefix}_generator_config.json" zones_config = project_root / f"{prefix}_zones.json" if generator_config.exists() and zones_config.exists(): project['generator_config'] = generator_config project['zones_config'] = zones_config valid_projects.append(project) print(f"✅ {project['project_name']:<15} - Config files found") else: missing = [] if not generator_config.exists(): missing.append(f"{prefix}_generator_config.json") if not zones_config.exists(): missing.append(f"{prefix}_zones.json") print(f"❌ {project['project_name']:<15} - Missing: {', '.join(missing)}") return valid_projects def timestamp_l5x_file(l5x_path: Path, timestamp: str = None) -> bool: """Add timestamp to L5X file in the Controller attributes. Args: l5x_path: Path to L5X file timestamp: Optional timestamp string, defaults to current time Returns: True if successful, False otherwise """ if not l5x_path.exists(): return False if timestamp is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") try: # Read the L5X file with open(l5x_path, 'r', encoding='utf-8') as f: content = f.read() # Add timestamp to Controller element using regex # Look for ]*?)(?:\s+LastModifiedDate="[^"]*")?\s*([^>]*>)' replacement = f'\\1 LastModifiedDate="{timestamp}" GeneratedBy="BatchPLCGeneration" \\2' updated_content = re.sub(controller_pattern, replacement, content, count=1) # Also add timestamp comment at the beginning timestamp_comment = f'\n' if updated_content.startswith('') + 2 updated_content = updated_content[:xml_decl_end] + '\n' + timestamp_comment + updated_content[xml_decl_end:] else: # Insert at beginning updated_content = timestamp_comment + updated_content # Write back with open(l5x_path, 'w', encoding='utf-8') as f: f.write(updated_content) return True except Exception as e: print(f"⚠️ Failed to timestamp {l5x_path.name}: {e}") return False def run_complete_workflow(project_root: Path, project: Dict, verbose: bool = False) -> bool: """Run the complete workflow for a single project. Returns: True if successful, False otherwise """ workflow_script = project_root / "Routines Generator" / "complete_workflow.py" if not workflow_script.exists(): print(f"❌ Workflow script not found: {workflow_script}") return False try: cmd = [ sys.executable, str(workflow_script), '--project', project['project_prefix'], '--project-name', project['project_name'], '--excel-file', str(project['file_path']), ] if verbose: cmd.append('--verbose') # Run the workflow result = subprocess.run( cmd, cwd=project_root, capture_output=True, text=True, timeout=600 # 10 minute timeout ) if result.returncode == 0: return True else: print(f"❌ Workflow failed for {project['project_name']}") if verbose: print(f" stdout: {result.stdout}") print(f" stderr: {result.stderr}") return False except subprocess.TimeoutExpired: print(f"❌ Workflow timeout for {project['project_name']} (exceeded 10 minutes)") return False except Exception as e: print(f"❌ Workflow error for {project['project_name']}: {e}") return False def setup_compilation_for_project(project_root: Path, project: Dict, timestamp: str) -> bool: """Setup compilation for a generated L5X project. Returns: True if successful, False otherwise """ # Find generated L5X file in project-specific folder generated_projects_dir = project_root / "IO Tree Configuration Generator" / "generated_projects" project_folder = generated_projects_dir / project['project_prefix'] # Check project-specific folder first if project_folder.exists(): l5x_files = list(project_folder.glob(f"{project['project_name']}*.L5X")) else: # Fallback to root directory for backward compatibility l5x_files = list(generated_projects_dir.glob(f"{project['project_name']}*.L5X")) if not l5x_files: print(f"❌ No L5X file found for {project['project_name']}") return False if len(l5x_files) > 1: print(f"⚠️ Multiple L5X files found for {project['project_name']}, using first: {l5x_files[0].name}") # Sort by modification time to get the most recent l5x_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) l5x_file = l5x_files[0] # Add timestamp to L5X file if not timestamp_l5x_file(l5x_file, timestamp): print(f"⚠️ Failed to timestamp L5X file: {l5x_file.name}") # Setup compilation using compilation manager try: compiler_dir = project_root / "L5X2ACD Compiler" sys.path.append(str(compiler_dir)) from compilation_manager import CompilationManager # Create compilation manager manager = CompilationManager(compiler_dir) # Setup compilation with smart replacement result = manager.setup_compilation( source_l5x=l5x_file, project_name=project['project_name'], compilation_options={}, wipe_existing=False, replace_mode=True ) return True except Exception as e: print(f"❌ Failed to setup compilation for {project['project_name']}: {e}") return False def main(): """Main entry point.""" import argparse parser = argparse.ArgumentParser( description="Batch PLC Generation and Compilation Manager", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Process all available projects python batch_plc_generation.py # Process specific projects only python batch_plc_generation.py --projects MTN6_MCM01 MTN6_MCM06 SAT9_MCM01 # Discover and list available projects only python batch_plc_generation.py --list-only # Verbose output python batch_plc_generation.py --verbose """ ) parser.add_argument('--projects', nargs='*', help='Specific projects to process (default: all available)') parser.add_argument('--list-only', action='store_true', help='Only list available projects, do not process') parser.add_argument('--setup-compilation', action='store_true', help='Also setup compilation (by default, only generates L5X files)') parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') args = parser.parse_args() # Get project root and paths project_root = get_project_root() data_dir = project_root / "PLC Data Generator" / "data" print("🚀 Batch PLC Generation and Compilation Manager") print("=" * 60) # Step 1: Discover Excel files print("\n📁 Discovering Excel data files...") discovered_projects = discover_excel_files(data_dir) if not discovered_projects: print("❌ No Excel files found in data directory") return 1 print(f" Found {len(discovered_projects)} Excel files") # Step 2: Validate project configurations print("\n🔧 Validating project configurations...") valid_projects = validate_project_configs(project_root, discovered_projects) if not valid_projects: print("❌ No projects with valid configurations found") return 1 print(f" {len(valid_projects)} projects have valid configurations") # Filter projects if specific ones requested if args.projects: requested_names = set(args.projects) valid_projects = [p for p in valid_projects if p['project_name'] in requested_names] print(f" Filtered to {len(valid_projects)} requested projects") # List projects if requested if args.list_only: print("\n📋 Available Projects:") for project in valid_projects: print(f" {project['project_name']:<15} - {project['file_path'].name}") return 0 # Step 3: Process each project timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") successful_workflows = [] failed_workflows = [] successful_compilations = [] failed_compilations = [] print(f"\n⚙️ Processing {len(valid_projects)} projects...") print(f" Timestamp: {timestamp}") for i, project in enumerate(valid_projects, 1): project_name = project['project_name'] print(f"\n[{i}/{len(valid_projects)}] Processing {project_name}") # Always run workflow to generate L5X files print(f" Running complete workflow...") workflow_success = run_complete_workflow(project_root, project, args.verbose) if workflow_success: print(f" ✅ L5X generation completed") successful_workflows.append(project_name) else: print(f" ❌ L5X generation failed") failed_workflows.append(project_name) # Setup compilation only if requested and workflow succeeded if workflow_success and args.setup_compilation: print(f" Setting up compilation...") compilation_success = setup_compilation_for_project(project_root, project, timestamp) if compilation_success: print(f" ✅ Compilation setup completed") successful_compilations.append(project_name) else: print(f" ❌ Compilation setup failed") failed_compilations.append(project_name) # Final summary print(f"\n📊 Processing Summary") print("=" * 60) print(f"Total projects processed: {len(valid_projects)}") print(f"Successful L5X generations: {len(successful_workflows)}") if successful_workflows: print(f" ✅ {', '.join(successful_workflows)}") print(f"Failed L5X generations: {len(failed_workflows)}") if failed_workflows: print(f" ❌ {', '.join(failed_workflows)}") print(f"Successful compilation setups: {len(successful_compilations)}") if successful_compilations: print(f" ✅ {', '.join(successful_compilations)}") print(f"Failed compilation setups: {len(failed_compilations)}") if failed_compilations: print(f" ❌ {', '.join(failed_compilations)}") # Show next steps if successful_workflows: print(f"\n🚀 Next Steps:") if args.setup_compilation and successful_compilations: print(f" Windows Compilation - Navigate to: L5X2ACD Compiler\\") print(f" Run batch files for compiled projects:") for project_name in successful_compilations: print(f" compile_{project_name}.bat") else: print(f" Compile all generated L5X files:") print(f" cd \"L5X2ACD Compiler\"") print(f" compile_all_projects.bat") print(f" run_all_compilations.bat") # Return appropriate exit code if failed_workflows or (args.setup_compilation and failed_compilations): return 1 if failed_workflows else 0 else: return 0 if __name__ == '__main__': sys.exit(main())