#!/usr/bin/env python3 """ Complete PLC Generation Workflow Runs the entire pipeline from raw Excel to compiled ACD: 1. PLC Data Generator (raw Excel → DESC_IP_MERGED.xlsx) 2. Routines Generator (DESC_IP_MERGED.xlsx → L5X files) 3. L5X2ACD Compiler (L5X files → ACD files) """ from __future__ import annotations import sys import argparse import subprocess from pathlib import Path def get_project_paths(): """Get standardized paths for all project components.""" project_root = Path(__file__).parent.parent.resolve() return { 'project_root': project_root, 'data_generator': project_root / "PLC Data Generator", 'routines_generator': project_root / "Routines Generator", 'l5x2acd_compiler': project_root / "L5X2ACD Compiler", 'io_tree_generator': project_root / "IO Tree Configuration Generator" } def run_plc_data_generator(raw_excel_file: Path, paths: dict) -> bool: """Run the PLC Data Generator to create DESC_IP_MERGED.xlsx.""" data_gen_dir = paths['data_generator'] data_gen_script = data_gen_dir / "main.py" if not data_gen_script.exists(): print(f"ERROR: PLC Data Generator not found at {data_gen_script}") return False if not raw_excel_file.exists(): print(f"ERROR: Raw Excel file not found at {raw_excel_file}") return False print(f"=== Step 1: Processing Raw Excel Data ===") print(f"Input: {raw_excel_file}") print(f"Processing with PLC Data Generator...") try: # Run the PLC Data Generator with the Excel file path as argument result = subprocess.run([ sys.executable, str(data_gen_script), str(raw_excel_file.resolve()) # Pass the Excel file path as argument ], cwd=data_gen_dir, capture_output=True, text=True) # Check if core processing succeeded by looking for output files # Even if there's a permission error at the end, the processing might have completed source = data_gen_dir / "DESC_IP_MERGED.xlsx" success_indicators = [ "Processing complete!" in result.stdout, "New Excel file created:" in result.stdout, source.exists() ] # Consider it successful if the essential files were created, even with permission errors if result.returncode == 0 or (any(success_indicators) and "[Errno 1] Operation not permitted" in result.stdout): if result.returncode != 0: print("WARNING: Permission error at end of processing, but core processing completed successfully") else: print("SUCCESS: PLC Data Generator completed successfully") print(result.stdout) # Copy DESC_IP_MERGED.xlsx from data generator output (it already has safety sheets) dest = paths['routines_generator'] / "DESC_IP_MERGED.xlsx" if source.exists(): import shutil shutil.copy2(source, dest) print(f"SUCCESS: DESC_IP_MERGED.xlsx (with safety sheets) copied to {dest}") return True else: print("ERROR: DESC_IP_MERGED.xlsx not found after data generation") return False else: print("ERROR: PLC Data Generator failed") print("STDOUT:", result.stdout) print("STDERR:", result.stderr) return False except Exception as e: print(f"ERROR: Error running PLC Data Generator: {e}") return False def run_routines_generator(paths: dict, project_name: str = None, ignore_estop1ok: bool = False) -> bool: """Run the Routines Generator using generate_main_with_dpm.py script.""" print(f"\n=== Step 2: Generating L5X Programs ===") routines_dir = paths['routines_generator'] main_dpm_script = routines_dir / "generate_main_with_dpm.py" if not main_dpm_script.exists(): print(f"ERROR: generate_main_with_dpm.py script not found at {main_dpm_script}") return False try: # Build command arguments for generate_main_with_dpm.py script cmd_args = [ sys.executable, str(main_dpm_script), "--desc-ip-mode" # Use DESC_IP data extraction mode ] # Add project name if provided if project_name: cmd_args.extend(["--project-name", project_name]) print(f"INFO: Running with project name: {project_name}") # Add ignore-estop1ok flag if set if ignore_estop1ok: cmd_args.append("--ignore-estop1ok") print("INFO: Running with --ignore-estop1ok flag") # Run the main program generator script with DPM routines result = subprocess.run(cmd_args, cwd=routines_dir, capture_output=True, text=True) print(result.stdout) if result.stderr: print("STDERR:", result.stderr) if result.returncode == 0: print("SUCCESS: Routines generation completed successfully") return True else: print("ERROR: Routines generation failed") return False except Exception as e: print(f"ERROR: Error running Routines Generator: {e}") return False def run_io_tree_generator(paths: dict, project_name: str) -> bool: """Run the IO Tree Configuration Generator.""" print(f"\n=== Step 3: Generating Complete Project L5X ===") io_tree_dir = paths['io_tree_generator'] enhanced_mcm_script = io_tree_dir / "enhanced_mcm_generator.py" # Use the file directly from PLC Data Generator since we're skipping Routines Generator desc_ip_file = paths['data_generator'] / "DESC_IP_MERGED.xlsx" if not enhanced_mcm_script.exists(): print(f"ERROR: enhanced_mcm_generator.py not found at {enhanced_mcm_script}") return False # Load zones configuration for the project zones_json = None if project_name: try: import sys sys.path.append(str(paths['project_root'])) from zones_config import ZONES_CONFIGS, DEFAULT_ZONES # Determine project type from project name (MCM01, MCM04, MCM05, etc.) if 'MCM01' in project_name.upper(): zones_to_use = ZONES_CONFIGS.get("MCM01", DEFAULT_ZONES) elif 'MCM04' in project_name.upper(): zones_to_use = ZONES_CONFIGS.get("MCM04", DEFAULT_ZONES) elif 'MCM05' in project_name.upper(): zones_to_use = ZONES_CONFIGS.get("MCM05", DEFAULT_ZONES) else: zones_to_use = DEFAULT_ZONES import json zones_json = json.dumps(zones_to_use) print(f"Using zones configuration for IO Tree Generator: {project_name}") except Exception as e: print(f"WARNING: Could not load zones configuration for IO Tree: {e}") print("Proceeding without zones...") try: # Build command arguments cmd_args = [ sys.executable, str(enhanced_mcm_script), str(desc_ip_file), project_name ] # Add zones if available if zones_json: cmd_args.extend(["--zones", zones_json]) # Run the IO Tree Configuration Generator result = subprocess.run(cmd_args, cwd=io_tree_dir, capture_output=True, text=True) print(result.stdout) if result.stderr: print("STDERR:", result.stderr) if result.returncode == 0: print("SUCCESS: Complete project L5X generated successfully") return True else: print("ERROR: IO Tree generation failed") return False except Exception as e: print(f"ERROR: Error running IO Tree Generator: {e}") return False def run_l5x_to_acd_compiler(paths: dict, project_name: str) -> bool: """Prepare for L5X2ACD Compilation using dynamic compilation manager.""" print(f"\n=== Step 4: Preparing for L5X to ACD Compilation ===") # Find the generated complete project L5X file io_tree_dir = paths['io_tree_generator'] generated_projects_dir = io_tree_dir / "generated_projects" if not generated_projects_dir.exists(): print(f"ERROR: Generated projects directory not found at {generated_projects_dir}") return False # Look for L5X files that start with the project name l5x_files = list(generated_projects_dir.glob(f"{project_name}*.L5X")) if not l5x_files: print(f"ERROR: No L5X files found starting with '{project_name}' in {generated_projects_dir}") available_files = list(generated_projects_dir.glob("*.L5X")) if available_files: print(f"Available L5X files: {[f.name for f in available_files]}") return False if len(l5x_files) > 1: print(f"WARNING: Multiple L5X files found: {[f.name for f in l5x_files]}") print(f"Using the first one: {l5x_files[0].name}") complete_l5x = l5x_files[0] print(f"Found generated L5X file: {complete_l5x.name}") # Use the dynamic compilation manager l5x2acd_dir = paths['project_root'] / "L5X2ACD Compiler" try: # Import and use the compilation manager import sys sys.path.append(str(l5x2acd_dir)) from compilation_manager import CompilationManager # Create compilation manager manager = CompilationManager(l5x2acd_dir) # Determine project-specific options project_type = "UNKNOWN" options = {} if project_name: if "MCM01" in project_name.upper(): project_type = "MCM01" options['enable_safety_validation'] = True elif "MCM04" in project_name.upper(): project_type = "MCM04" options['enable_feeder_optimization'] = True print(f"Using dynamic compilation setup for project type: {project_type}") # Setup compilation with wipe and dynamic generation result = manager.setup_compilation( source_l5x=complete_l5x, project_name=project_name or complete_l5x.stem, compilation_options=options, wipe_existing=True ) print("SUCCESS: Dynamic compilation setup completed") print("=" * 60) print("⚠️ COMPILATION STEP REQUIRES WINDOWS:") print(f" L5X File: {result['l5x_file']}") print(f" Batch File: {result['batch_file']}") print() print("🪟 To compile on Windows:") print(f" 1. Run: {result['batch_file']}") print(" 2. Or double-click: {result['batch_file'].name}") print(" 3. Or manually run:") l5x2acd_windows_path = str(l5x2acd_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\') l5x_windows_path = str(result['l5x_file']).replace('/mnt/c/', 'C:\\').replace('/', '\\') print(f" cd \"{l5x2acd_windows_path}\"") print(f" python l5x_to_acd.py \"{l5x_windows_path}\"") print("=" * 60) return True except Exception as e: print(f"ERROR: Failed to setup dynamic compilation: {e}") import traceback traceback.print_exc() return False def main() -> None: """Main entry point for complete workflow.""" parser = argparse.ArgumentParser(description="Complete PLC generation workflow from raw Excel to ACD") parser.add_argument('--excel-file', type=Path, required=True, help='Raw Excel file to process') parser.add_argument('--project-name', help='Project name (for compatibility)') parser.add_argument('--ignore-estop1ok', action='store_true', help='Ignore ESTOP1OK tags in safety routines generation') args = parser.parse_args() # Get project paths paths = get_project_paths() print("Complete PLC Generation Workflow") print("=" * 60) print(f"Project Root: {paths['project_root']}") print(f"Input Excel: {args.excel_file}") print(f"Project: {args.project_name}") print() # Step 1: Process raw Excel data if not run_plc_data_generator(args.excel_file, paths): print("ERROR: Workflow failed at Step 1 (Data Processing)") sys.exit(1) # Step 2: Generate L5X programs (Routines Generator) if not run_routines_generator(paths, args.project_name, args.ignore_estop1ok): print("ERROR: Workflow failed at Step 2 (Routines Generation)") sys.exit(1) # Step 3: Generate complete project L5X (IO Tree Generator) if not run_io_tree_generator(paths, args.project_name): print("ERROR: Workflow failed at Step 3 (IO Tree Generation)") sys.exit(1) # Step 4: Compile L5X to ACD if not run_l5x_to_acd_compiler(paths, args.project_name): print("ERROR: Workflow failed at Step 4 (L5X2ACD Compilation)") sys.exit(1) print("\n" + "="*60) print("SUCCESS: Complete PLC Generation Workflow completed!") print(f"Generated L5X file: IO Tree Configuration Generator/generated_projects/{args.project_name}.L5X") print("🪟 Run the generated batch file on Windows to compile to ACD") print("="*60) if __name__ == '__main__': main()