422 lines
16 KiB
Python
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())
|