344 lines
13 KiB
Python
344 lines
13 KiB
Python
#!/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() |