PLC_Generation/batch_plc_generation.py
2025-09-02 11:13:29 +04:00

422 lines
16 KiB
Python

#!/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 <Controller and add/update LastModifiedDate attribute
controller_pattern = r'(<Controller[^>]*?)(?:\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'<!-- Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} by Batch PLC Generation -->\n'
if updated_content.startswith('<?xml'):
# Insert after XML declaration
xml_decl_end = updated_content.find('?>') + 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())