added config to routines generator

This commit is contained in:
ilia gu 2025-08-14 15:44:05 +04:00
parent f251cc7e25
commit e06a57dbc2
124 changed files with 9135 additions and 4783 deletions

View File

@ -19,6 +19,7 @@ import os
import re
import xml.etree.ElementTree as ET
from datetime import datetime
import copy
from typing import Optional
# Base controller & fixed slot models
@ -78,6 +79,9 @@ class ControllerBuilder:
def __init__(self, controller_name: str, skip_chassis_modules: bool = False):
self.controller_name = controller_name
self.skip_chassis_modules = skip_chassis_modules
# Raw base XML snippets to preserve CDATA on save
self._raw_base_aoi_xml: Optional[str] = None
self._raw_base_dtypes_xml: Optional[str] = None
# 1. Build base controller from boilerplate
controller_cfg = create_l83es_controller(controller_name)
@ -129,6 +133,86 @@ class ControllerBuilder:
# Indent & persist
self._save_project(self.tree, filename)
def import_base_sections_from_l5x(self, base_l5x_path: str):
"""Import AOIs and DataTypes from a base L5X and insert with required ordering.
- <AddOnInstructionDefinitions> is inserted immediately before <Tags>.
- <DataTypes> is inserted immediately before <Modules>.
Args:
base_l5x_path: Path to the L5X file containing base definitions
"""
base_xml_text: Optional[str] = None
try:
# Read raw text first so CDATA can be preserved verbatim
with open(base_l5x_path, "r", encoding="utf-8") as fh:
base_xml_text = fh.read()
base_tree = ET.fromstring(base_xml_text)
base_root = base_tree
except Exception as exc:
print(f"WARNING: Failed to parse base L5X at {base_l5x_path}: {exc}")
return
base_controller = base_root.find(".//Controller")
if base_controller is None:
print("WARNING: No <Controller> found in base L5X")
return
controller = self.root.find(".//Controller[@Use='Target']")
if controller is None:
print("ERROR: Target controller not found in builder")
return
# Ensure anchor elements exist
tags = controller.find("Tags")
if tags is None:
tags = ET.SubElement(controller, "Tags")
modules = controller.find("Modules")
if modules is None:
# Create Modules just after SafetyInfo if present, else append
safety_info = controller.find("SafetyInfo")
if safety_info is not None:
insert_idx = list(controller).index(safety_info) + 1
modules = ET.Element("Modules")
controller.insert(insert_idx, modules)
else:
modules = ET.SubElement(controller, "Modules")
# Import AOIs before Tags
base_aoi = base_controller.find("AddOnInstructionDefinitions")
if base_aoi is not None:
existing_aoi = controller.find("AddOnInstructionDefinitions")
if existing_aoi is not None:
controller.remove(existing_aoi)
try:
insert_idx = list(controller).index(tags)
except ValueError:
insert_idx = len(list(controller))
controller.insert(insert_idx, copy.deepcopy(base_aoi))
# Capture raw AOI XML to preserve CDATA on save
if base_xml_text is not None:
m = re.search(r"<AddOnInstructionDefinitions[\s\S]*?</AddOnInstructionDefinitions>", base_xml_text)
if m:
self._raw_base_aoi_xml = m.group(0)
# Import DataTypes before Modules
base_dts = base_controller.find("DataTypes")
if base_dts is not None:
existing_dts = controller.find("DataTypes")
if existing_dts is not None:
controller.remove(existing_dts)
try:
insert_idx = list(controller).index(modules)
except ValueError:
insert_idx = len(list(controller))
controller.insert(insert_idx, copy.deepcopy(base_dts))
# Capture raw DataTypes XML to preserve CDATA on save
if base_xml_text is not None:
m = re.search(r"<DataTypes[\s\S]*?</DataTypes>", base_xml_text)
if m:
self._raw_base_dtypes_xml = m.group(0)
def add_generated_tags(self, routines_generator_dir: str, zones_dict=None):
"""Add the generated tags from the Routines Generator into this controller.
@ -557,6 +641,22 @@ class ControllerBuilder:
# Apply CDATA wrapping to Text elements
full_xml = re.sub(text_pattern, lambda m: f"{m.group(1)}\n<![CDATA[{m.group(2)}]]>\n{m.group(3)}" if m.group(2).strip() else m.group(0), full_xml, flags=re.DOTALL)
# If raw base sections were imported, replace the generated sections with raw text
if self._raw_base_aoi_xml:
full_xml = re.sub(
r"<AddOnInstructionDefinitions[\s\S]*?</AddOnInstructionDefinitions>",
lambda _m: self._raw_base_aoi_xml,
full_xml,
count=1,
)
if self._raw_base_dtypes_xml:
full_xml = re.sub(
r"<DataTypes[\s\S]*?</DataTypes>",
lambda _m: self._raw_base_dtypes_xml,
full_xml,
count=1,
)
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w", encoding="utf-8") as fh:
fh.write(full_xml)

View File

@ -362,9 +362,7 @@ class EnhancedMCMGenerator:
# Print summary of modules found
if self.iolm_modules:
print(f"Found {len(self.iolm_modules)} IOLM modules:")
for iolm in self.iolm_modules:
print(f" {iolm['name']} (IP: {iolm.get('ip_address', 'N/A')})")
print(f"Found {len(self.iolm_modules)} IOLM modules")
if self.unknown_modules:
print(f"WARNING: {len(self.unknown_modules)} unknown modules found")
@ -391,22 +389,33 @@ class EnhancedMCMGenerator:
modules_section = builder.get_modules_section()
self._add_excel_modules(modules_section)
# 2b. Import AOIs and DataTypes from BaseProgram.L5X with required ordering
try:
base_l5x_path = os.path.join(os.path.dirname(__file__), "BaseProgram.L5X")
if os.path.exists(base_l5x_path):
print(f" Importing AOIs/DataTypes from base: {base_l5x_path}")
builder.import_base_sections_from_l5x(base_l5x_path)
else:
print(f" WARNING: BaseProgram.L5X not found at {base_l5x_path}")
except Exception as e:
print(f" WARNING: Failed importing base sections: {e}")
# 3. Embed generated programs from Routines Generator - TEMPORARILY DISABLED
# routines_generator_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Routines Generator")
# print(f"Looking for generated programs in: {routines_generator_dir}")
# builder.add_generated_programs(routines_generator_dir)
print(" Skipping generated programs embedding (temporarily disabled)")
# Skipping generated programs embedding (temporarily disabled)
# 4. Embed generated tags from Routines Generator - TEMPORARILY DISABLED
# print(f"Looking for generated tags in: {routines_generator_dir}")
# builder.add_generated_tags(routines_generator_dir, zones_dict=getattr(self, 'zones_dict', None))
print(" Skipping generated tags embedding (temporarily disabled)")
# Skipping generated tags embedding (temporarily disabled)
# 5. Finalise and save
output_file = os.path.join(self.generated_dir, f"{self.controller_name}.L5X")
builder.finalise_and_save(output_file)
print(f"Generated complete project: {output_file}")
print(f"OK: Generated project: {output_file}")
return [output_file]
def generate_split_projects(self) -> Tuple[str, str]:
@ -415,7 +424,7 @@ class EnhancedMCMGenerator:
Returns:
Tuple of (file1_path, file2_path)
"""
print("Generating split projects...")
print("Split projects generation")
# Validate parent-child relationships before splitting
self._validate_parent_child_relationships()
@ -440,9 +449,9 @@ class EnhancedMCMGenerator:
2
)
print(f"Split projects generated successfully:")
print(f" Part 1: {file1_path} ({self._count_modules_in_group(group1_modules)} modules)")
print(f" Part 2: {file2_path} ({self._count_modules_in_group(group2_modules)} modules)")
print("OK: Split projects generated")
print(f"- Part 1: {file1_path} ({self._count_modules_in_group(group1_modules)} modules)")
print(f"- Part 2: {file2_path} ({self._count_modules_in_group(group2_modules)} modules)")
return file1_path, file2_path
@ -452,7 +461,7 @@ class EnhancedMCMGenerator:
Returns:
Tuple of (group1_dict, group2_dict) where each dict contains module lists by type
"""
print(" Creating balanced module groups...")
# Creating balanced module groups
# Initialize empty groups
group1 = {
@ -591,9 +600,7 @@ class EnhancedMCMGenerator:
family_size += len(children)
# Log family assignment for debugging
if children:
child_names = [child.get('name', 'Unknown') for _, child in children]
print(f" Assigning family: {module_name} with {len(children)} children: {child_names}")
# Assignment details suppressed for concise output
# Assign to group with fewer modules
if group1_total <= group2_total:
@ -605,8 +612,7 @@ class EnhancedMCMGenerator:
group2_total += family_size
group_name = "Group 2"
if children:
print(f" -> {group_name} (family size: {family_size})")
# Assignment target suppressed for concise output
# Add parent module
target_group[module_type].append(module)
@ -645,12 +651,7 @@ class EnhancedMCMGenerator:
orphaned_modules.append(('solenoid', solenoid))
# Distribute orphaned modules
if orphaned_modules:
print(f" Found {len(orphaned_modules)} orphaned modules:")
for module_type, module in orphaned_modules:
module_name = module.get('name', 'Unknown')
parent = module.get('parent_module', 'None')
print(f" - {module_type}: {module_name} (expected parent: {parent})")
# Orphan details suppressed; only assignment occurs
for module_type, module in orphaned_modules:
if group1_total <= group2_total:
@ -662,8 +663,7 @@ class EnhancedMCMGenerator:
group2_total += 1
group_name = "Group 2"
module_name = module.get('name', 'Unknown')
print(f" -> Assigning orphaned {module_type} '{module_name}' to {group_name}")
# Assignment summary suppressed
assigned_modules.add(module_name)
def _generate_project_with_modules(self, project_name: str, module_groups: Dict, part_number: int) -> str:
@ -686,6 +686,17 @@ class EnhancedMCMGenerator:
# Add modules from the specific groups
self._add_excel_modules_from_groups(modules_section, module_groups)
# Import AOIs and DataTypes from BaseProgram.L5X
try:
base_l5x_path = os.path.join(os.path.dirname(__file__), "BaseProgram.L5X")
if os.path.exists(base_l5x_path):
print(f" Importing AOIs/DataTypes from base: {base_l5x_path}")
builder.import_base_sections_from_l5x(base_l5x_path)
else:
print(f" WARNING: BaseProgram.L5X not found at {base_l5x_path}")
except Exception as e:
print(f" WARNING: Failed importing base sections: {e}")
# Save the file
output_filename = f"{self.generated_dir}/{project_name}.L5X"
builder.finalise_and_save(output_filename)
@ -785,7 +796,7 @@ class EnhancedMCMGenerator:
def _validate_parent_child_relationships(self):
"""Validate parent-child relationships before splitting."""
print(" Validating parent-child relationships...")
# Validating parent-child relationships
issues = []
@ -844,15 +855,15 @@ class EnhancedMCMGenerator:
issues.append(f"Solenoid module '{solenoid.get('name')}' references non-existent IOLM parent '{parent}'")
if issues:
print(f" WARNING: Found {len(issues)} parent-child relationship issues:")
for issue in issues:
print(f" - {issue}")
print(f"Warning: {len(issues)} parent-child relationship issues detected")
for issue in issues[:5]:
print(f"- {issue}")
else:
print(" [SUCCESS] All parent-child relationships are valid")
print("OK: Parent-child relationships valid")
def _validate_split_integrity(self, group1: Dict, group2: Dict):
"""Validate that the split maintains parent-child relationships."""
print(" Validating split integrity...")
# Validating split integrity
# Build module name to group mapping
group1_modules = set()
@ -894,12 +905,12 @@ class EnhancedMCMGenerator:
violations.append(f"{child_type} '{child_name}' separated from parent '{parent_name}'")
if violations:
print(f" ERROR: Found {len(violations)} split integrity violations:")
for violation in violations:
print(f" - {violation}")
print(f"Error: {len(violations)} split integrity violations detected")
for violation in violations[:5]:
print(f"- {violation}")
raise ValueError("Split integrity validation failed - parent-child relationships would be broken")
else:
print(" [SUCCESS] Split maintains all parent-child relationships")
print("OK: Split integrity valid")
def _configure_controller_settings(self, root):
"""Configure controller-specific settings."""
@ -1166,11 +1177,10 @@ def main():
if len(sys.argv) > 2 and not sys.argv[2].startswith("--"):
project_name = sys.argv[2]
print(f"Enhanced MCM Generator")
print(f"Project: {project_name}")
print(f"Excel file: {excel_file}")
print(f"Mode: {'Split (2 files)' if split_mode else 'Single file'}")
print(f"Zones: {'Custom' if zones_dict else 'Default'}")
print("Enhanced MCM Generator")
print(f"- Project: {project_name}")
print(f"- Excel: {excel_file}")
print(f"- Mode: {'Split' if split_mode else 'Single file'}")
print("-" * 50)
# Create generator with zones
@ -1181,15 +1191,14 @@ def main():
if split_mode:
# Generate split projects
file1, file2 = generator.generate_complete_project(split_mode=True)
print(f"\nSplit generation complete!")
print(f"Generated files:")
print(f" {file1}")
print(f" {file2}")
print("Split generation complete")
print(f"- {file1}")
print(f"- {file2}")
else:
# Generate single project
output_file = generator.generate_complete_project(split_mode=False)
print(f"\nSingle file generation complete!")
print(f"Generated file: {output_file}")
print("Single file generation complete")
print(f"- {output_file}")
else:
print("ERROR: Failed to load/process Excel data")

View File

@ -1,191 +0,0 @@
#!/usr/bin/env python3
"""
Test script for SIO module implementation
========================================
Tests the SIO boilerplate model to ensure it works correctly with
IP address configuration and comment updates.
"""
import os
import sys
# Add the models directory to the path
sys.path.append(os.path.join(os.path.dirname(__file__), 'models'))
from models.sio_boilerplate_model import create_sio_module, SIOModuleGenerator
def test_sio_basic_functionality():
"""Test basic SIO module creation and configuration."""
print("Testing SIO module basic functionality...")
# Create a basic SIO configuration
config = create_sio_module(
name="SIO_TEST_1",
ip_address="192.168.1.100",
safety_input_names={
0: "Emergency Stop Line 1",
1: "Emergency Stop Line 2",
2: "Safety Gate Position",
3: "Light Curtain Active",
4: "Safety Mat Pressure",
5: "Reset Button Pressed",
6: "Enable Switch Active",
7: "Safety Scanner Zone"
},
safety_output_names={
0: "Main Safety Relay",
1: "Backup Safety Relay",
2: "Warning Light Red",
3: "Warning Light Amber",
4: "Safety Brake Release",
5: "Safety Brake Release",
6: "Safety Brake Release",
7: "Safety Brake Release"
}
)
# Create generator and apply updates
generator = SIOModuleGenerator(config)
try:
generator.load_boilerplate()
print(" ✓ Boilerplate loaded successfully")
generator.apply_updates()
print(" ✓ Updates applied successfully")
# Check that the IP address was updated
port = generator.root.find(".//Port[@Type='Ethernet']")
if port is not None and port.get("Address") == "192.168.1.100":
print(" ✓ IP address updated correctly")
else:
print(" ✗ IP address not updated correctly")
return False
# Check that module name was updated
module = generator.root.find(".//Module[@Use='Target']")
if module is not None and module.get("Name") == "SIO_TEST_1":
print(" ✓ Module name updated correctly")
else:
print(" ✗ Module name not updated correctly")
return False
# Check safety input comments
si_comments = generator.root.find(".//Connection[@Name='_200424962CC22C87']/InputTag/Comments")
if si_comments is not None:
comment_count = len(list(si_comments))
if comment_count == 8: # Should have 8 safety input comments
print(f" ✓ Safety input comments added ({comment_count} comments)")
else:
print(f" ✗ Expected 8 safety input comments, got {comment_count}")
return False
else:
print(" ✗ Safety input comments section not found")
return False
# Check safety output comments
so_comments = generator.root.find(".//Connection[@Name='_200424962C862CC2']/OutputTag/Comments")
if so_comments is not None:
comment_count = len(list(so_comments))
if comment_count == 5: # Should have 5 safety output comments
print(f" ✓ Safety output comments added ({comment_count} comments)")
else:
print(f" ✗ Expected 5 safety output comments, got {comment_count}")
return False
else:
print(" ✗ Safety output comments section not found")
return False
# Test saving the output
os.makedirs("generated", exist_ok=True)
output_path = "generated/SIO_TEST_1.L5X"
generator.save(output_path)
if os.path.exists(output_path):
print(f" ✓ SIO module saved successfully to {output_path}")
# Check file size to ensure it's not empty
file_size = os.path.getsize(output_path)
if file_size > 1000: # Reasonable size for XML file
print(f" ✓ Generated file has reasonable size ({file_size} bytes)")
else:
print(f" ✗ Generated file seems too small ({file_size} bytes)")
return False
else:
print(" ✗ Failed to save SIO module")
return False
return True
except FileNotFoundError as e:
print(f" ✗ Boilerplate file not found: {e}")
return False
except Exception as e:
print(f" ✗ Error during testing: {e}")
return False
def test_sio_minimal_config():
"""Test SIO module with minimal configuration."""
print("\nTesting SIO module with minimal configuration...")
# Create minimal configuration
config = create_sio_module(
name="SIO_MINIMAL",
ip_address="10.0.0.50"
)
generator = SIOModuleGenerator(config)
try:
generator.load_boilerplate()
generator.apply_updates()
# Verify basic properties
module = generator.root.find(".//Module[@Use='Target']")
port = generator.root.find(".//Port[@Type='Ethernet']")
if (module is not None and module.get("Name") == "SIO_MINIMAL" and
port is not None and port.get("Address") == "10.0.0.50"):
print(" ✓ Minimal configuration applied successfully")
return True
else:
print(" ✗ Minimal configuration failed")
return False
except Exception as e:
print(f" ✗ Error with minimal configuration: {e}")
return False
def main():
"""Run all SIO tests."""
print("Running SIO Module Implementation Tests")
print("=" * 40)
test_results = []
# Test basic functionality
test_results.append(test_sio_basic_functionality())
# Test minimal configuration
test_results.append(test_sio_minimal_config())
# Summary
print("\n" + "=" * 40)
passed = sum(test_results)
total = len(test_results)
if passed == total:
print(f"✓ All tests passed ({passed}/{total})")
print("SIO module implementation is working correctly!")
return 0
else:
print(f"✗ Some tests failed ({passed}/{total})")
print("Please check the SIO implementation.")
return 1
if __name__ == "__main__":
exit(main())

View File

@ -1,7 +1,7 @@
@echo off
echo ====================================
echo PLC Compilation: MTN6_MCM05_CHUTE_LOAD
echo Project Type: MCM05
echo PLC Compilation: mtn6
echo Project Type: UNKNOWN
echo ====================================
echo.
@ -9,8 +9,8 @@ cd /d "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler"
echo Working directory: %CD%
echo.
if not exist "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X" (
echo ERROR: L5X file not found: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X
if not exist "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X" (
echo ERROR: L5X file not found: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X
pause
exit /b 1
)
@ -61,28 +61,28 @@ if errorlevel 1 (
echo ✓ Logix Designer SDK found
echo.
echo Input L5X file: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X") do echo File size: %%~zF bytes
echo Input L5X file: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X") do echo File size: %%~zF bytes
echo.
echo Starting compilation...
echo Command: py -3.12 l5x_to_acd.py "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X"
echo Command: py -3.12 l5x_to_acd.py "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X"
echo.
py -3.12 l5x_to_acd.py "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X"
py -3.12 l5x_to_acd.py "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X"
if exist "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.ACD" (
if exist "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.ACD" (
echo.
echo ====================================
echo SUCCESS: Compilation completed!
echo Output: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.ACD
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.ACD") do echo ACD size: %%~zF bytes
echo Output: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.ACD
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.ACD") do echo ACD size: %%~zF bytes
echo ====================================
) else (
echo.
echo ====================================
echo ERROR: Compilation failed!
echo Expected output: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.ACD
echo Expected output: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.ACD
echo ====================================
)

Binary file not shown.

View File

@ -0,0 +1,410 @@
Name,Description,Subsystem
Local:5:I.Data.0,MCM02 S_PBLT START,MCM02
Local:5:I.Data.1,MCM02 ST_PB STOP,MCM02
Local:5:I.Data.10,SPARE,MCM02
Local:5:I.Data.11,SPARE,MCM02
Local:5:I.Data.12,SPARE,MCM02
Local:5:I.Data.13,SPARE,MCM02
Local:5:I.Data.14,SPARE,MCM02
Local:5:I.Data.15,SPARE,MCM02
Local:5:I.Data.2,MCM02 MF_PBLT MOTOR FAULT RESET,MCM02
Local:5:I.Data.3,MCM02 JR_PBLT JAM RESET,MCM02
Local:5:I.Data.4,MCM02 LAP_PBLT LOW AIR PRESSURE,MCM02
Local:5:I.Data.5,MCM02 PBFR_PBLT POWER BRANCH FAULT RESET,MCM02
Local:5:I.Data.6,MCM02 BATTERY_FAULT_UPS BATTERY FAULT,MCM02
Local:5:I.Data.7,MCM02 ON_BATTERY ON BATTERY,MCM02
Local:5:I.Data.8,MCM02 UPS_BATTERY_LOW UPS BATTERY LOW,MCM02
Local:5:I.Data.9,MCM02 NAT_SWITCH ALARM NAT SWITCH,MCM02
Local:7:I.Pt00.Data,MCM02 FIRE_ALARM FIRE ALARM,MCM02
Local:7:I.Pt01.Data,SPARE,MCM02
Local:7:I.Pt10.Data,SPARE,MCM02
Local:7:I.Pt11.Data,SPARE,MCM02
Local:7:I.Pt12.Data,SPARE,MCM02
Local:7:I.Pt13.Data,SPARE,MCM02
Local:7:I.Pt14.Data,SPARE,MCM02
Local:7:I.Pt15.Data,SPARE,MCM02
Local:7:I.Pt02.Data,MCM02 ES_PB ES_PB_CH1,MCM02
Local:7:I.Pt03.Data,MCM02 ES_PB ES_PB_CH2,MCM02
Local:7:I.Pt04.Data,SPARE,MCM02
Local:7:I.Pt05.Data,SPARE,MCM02
Local:7:I.Pt06.Data,SPARE,MCM02
Local:7:I.Pt07.Data,SPARE,MCM02
Local:7:I.Pt08.Data,SPARE,MCM02
Local:7:I.Pt09.Data,SPARE,MCM02
Local:6:O.Data.0,MCM02 ES_LT EMERGENCY STOP ACTUATED,MCM02
Local:6:O.Data.1,MCM02 S_PBLT START LIGHT,MCM02
Local:6:O.Data.10,SPARE,MCM02
Local:6:O.Data.11,SPARE,MCM02
Local:6:O.Data.12,SPARE,MCM02
Local:6:O.Data.13,SPARE,MCM02
Local:6:O.Data.14,SPARE,MCM02
Local:6:O.Data.15,SPARE,MCM02
Local:6:O.Data.2,MCM02 MF_PBLT MOTOR FAULT LIGHT,MCM02
Local:6:O.Data.3,MCM02 JR_PBLT JAM RESTART LIGHT,MCM02
Local:6:O.Data.4,MCM02 LAP_PBLT LOW AIR PRESSURE LIGHT,MCM02
Local:6:O.Data.5,MCM02 PBFR_PBLT POWER BRANCH FAULT RESET LIGHT,MCM02
Local:6:O.Data.6,SPARE,MCM02
Local:6:O.Data.7,SPARE,MCM02
Local:6:O.Data.8,SPARE,MCM02
Local:6:O.Data.9,SPARE,MCM02
PS2_3_VFD1:I.In_0,PS2_3_DISC AUXILIARY DISCONNECT,MCM02
PS2_3_VFD1:I.In_1,SPARE,MCM02
PS2_3_VFD1:I.In_2,PS2_3_TPE1 TRACKING PHOTOEYE,MCM02
PS2_3_VFD1:I.In_3,PS2_3_TPE2 TRACKING PHOTOEYE,MCM02
PS2_3_VFD1:I.IO_0,SPARE,MCM02
PS2_3_VFD1:I.IO_1,SPARE,MCM02
PS2_3_VFD1:SI.In00Data,SPARE,MCM02
PS2_3_VFD1:SI.In01Data,SPARE,MCM02
PS2_3_VFD1:SI.In02Data,SPARE,MCM02
PS2_3_VFD1:SI.In03Data,SPARE,MCM02
PS2_3_VFD1:SO.Out00Output,SPARE,MCM02
PS2_4A_VFD1:I.In_0,PS2_4A_DISC AUXILIARY DISCONNECT,MCM02
PS2_4A_VFD1:I.In_1,SPARE,MCM02
PS2_4A_VFD1:I.In_2,SPARE,MCM02
PS2_4A_VFD1:I.In_3,SPARE,MCM02
PS2_4A_VFD1:I.IO_0,SPARE,MCM02
PS2_4A_VFD1:I.IO_1,SPARE,MCM02
PS2_4A_VFD1:SI.In00Data,SPARE,MCM02
PS2_4A_VFD1:SI.In01Data,SPARE,MCM02
PS2_4A_VFD1:SI.In02Data,SPARE,MCM02
PS2_4A_VFD1:SI.In03Data,SPARE,MCM02
PS2_4A_VFD1:SO.Out00Output,SPARE,MCM02
PS2_4B_VFD1:I.In_0,PS2_4B_DISC AUXILIARY DISCONNECT,MCM02
PS2_4B_VFD1:I.In_1,SPARE,MCM02
PS2_4B_VFD1:I.In_2,SPARE,MCM02
PS2_4B_VFD1:I.In_3,SPARE,MCM02
PS2_4B_VFD1:O.IO_0,PS2_7_BCN2_R RED BEACON,MCM02
PS2_4B_VFD1:I.IO_1,SPARE,MCM02
PS2_4B_VFD1:SI.In00Data,SPARE,MCM02
PS2_4B_VFD1:SI.In01Data,SPARE,MCM02
PS2_4B_VFD1:SI.In02Data,SPARE,MCM02
PS2_4B_VFD1:SI.In03Data,SPARE,MCM02
PS2_4B_VFD1:SO.Out00Output,SPARE,MCM02
PS2_5_VFD1:I.In_0,PS2_5_DISC AUXILIARY DISCONNECT,MCM02
PS2_5_VFD1:I.In_1,PS2_5_ENSH1 SHAFT ENCODER,MCM02
PS2_5_VFD1:I.In_2,PS2_5_TPE1 TRACKING PHOTOEYE,MCM02
PS2_5_VFD1:I.In_3,PS2_5_TPE2 TRACKING PHOTOEYE,MCM02
PS2_5_VFD1:I.IO_0,PS2_7_S2_PB START PUSHBUTTON,MCM02
PS2_5_VFD1:O.IO_1,PS2_7_S2_PB_LT START PUSHBUTTON LIGHT,MCM02
PS2_5_VFD1:SI.In00Data,SPARE,MCM02
PS2_5_VFD1:SI.In01Data,SPARE,MCM02
PS2_5_VFD1:SI.In02Data,SPARE,MCM02
PS2_5_VFD1:SI.In03Data,SPARE,MCM02
PS2_5_VFD1:SO.Out00Output,SPARE,MCM02
PS2_6_VFD1:I.In_0,PS2_6_DISC AUXILIARY DISCONNECT,MCM02
PS2_6_VFD1:I.In_1,PS2_6_ENSH1 SHAFT ENCODER,MCM02
PS2_6_VFD1:I.In_2,PS2_6_TPE1 TRACKING PHOTOEYE,MCM02
PS2_6_VFD1:I.In_3,SPARE,MCM02
PS2_6_VFD1:O.IO_0,PS2_7_BCN1_R RED BEACON,MCM02
PS2_6_VFD1:I.IO_1,SPARE,MCM02
PS2_6_VFD1:SI.In00Data,SPARE,MCM02
PS2_6_VFD1:SI.In01Data,SPARE,MCM02
PS2_6_VFD1:SI.In02Data,SPARE,MCM02
PS2_6_VFD1:SI.In03Data,SPARE,MCM02
PS2_6_VFD1:SO.Out00Output,SPARE,MCM02
PS2_7_VFD1:I.In_0,PS2_7_DISC AUXILIARY DISCONNECT,MCM02
PS2_7_VFD1:I.In_1,PS2_7_ENSH1 SHAFT ENCODER,MCM02
PS2_7_VFD1:I.In_2,PS2_7_TPE1 TRACKING PHOTOEYE,MCM02
PS2_7_VFD1:I.In_3,SPARE,MCM02
PS2_7_VFD1:I.IO_0,PS2_7_S1_PB START PUSHBUTTON,MCM02
PS2_7_VFD1:O.IO_1,PS2_7_S1_PB_LT START PUSHBUTTON LIGHT,MCM02
PS2_7_VFD1:SI.In00Data,PS2_7_EPC1 EMERGENCY PULLCORD,MCM02
PS2_7_VFD1:SI.In01Data,PS2_7_EPC1_2 EMERGENCY PULLCORD,MCM02
PS2_7_VFD1:SI.In02Data,PS2_7_EPC2 EMERGENCY PULLCORD,MCM02
PS2_7_VFD1:SI.In03Data,PS2_7_EPC2_2 EMERGENCY PULLCORD,MCM02
PS2_7_VFD1:SO.Out00Output,SPARE,MCM02
UL4_5_VFD1:I.In_0,UL4_5_DISC AUXILIARY DISCONNECT,MCM02
UL4_5_VFD1:I.In_1,UL4_5_ENSH1 SHAFT ENCODER,MCM02
UL4_5_VFD1:I.In_2,UL4_5_TPE1 TRACKING PHOTOEYE,MCM02
UL4_5_VFD1:I.In_3,UL4_5_TPE2 TRACKING PHOTOEYE,MCM02
UL4_5_VFD1:O.IO_0,UL4_3_BCN1_A AMBER BEACON,MCM02
UL4_5_VFD1:I.IO_1,SPARE,MCM02
UL4_5_VFD1:SI.In00Data,SPARE,MCM02
UL4_5_VFD1:SI.In01Data,SPARE,MCM02
UL4_5_VFD1:SI.In02Data,SPARE,MCM02
UL4_5_VFD1:SI.In03Data,SPARE,MCM02
UL4_5_VFD1:SO.Out00Output,SPARE,MCM02
UL4_6_VFD1:I.In_0,UL4_6_DISC AUXILIARY DISCONNECT,MCM02
UL4_6_VFD1:I.In_1,UL4_6_ENSH1 SHAFT ENCODER,MCM02
UL4_6_VFD1:I.In_2,UL4_6_TPE1 TRACKING PHOTOEYE,MCM02
UL4_6_VFD1:I.In_3,SPARE,MCM02
UL4_6_VFD1:I.IO_0,SPARE,MCM02
UL4_6_VFD1:I.IO_1,SPARE,MCM02
UL4_6_VFD1:SI.In00Data,SPARE,MCM02
UL4_6_VFD1:SI.In01Data,SPARE,MCM02
UL4_6_VFD1:SI.In02Data,SPARE,MCM02
UL4_6_VFD1:SI.In03Data,SPARE,MCM02
UL4_6_VFD1:SO.Out00Output,SPARE,MCM02
UL4_7_VFD1:I.In_0,UL4_7_DISC AUXILIARY DISCONNECT,MCM02
UL4_7_VFD1:I.In_1,UL4_7_ENSH1 SHAFT ENCODER,MCM02
UL4_7_VFD1:I.In_2,UL4_7_TPE1 TRACKING PHOTOEYE,MCM02
UL4_7_VFD1:I.In_3,SPARE,MCM02
UL4_7_VFD1:I.IO_0,SPARE,MCM02
UL4_7_VFD1:I.IO_1,SPARE,MCM02
UL4_7_VFD1:SI.In00Data,SPARE,MCM02
UL4_7_VFD1:SI.In01Data,SPARE,MCM02
UL4_7_VFD1:SI.In02Data,SPARE,MCM02
UL4_7_VFD1:SI.In03Data,SPARE,MCM02
UL4_7_VFD1:SO.Out00Output,SPARE,MCM02
UL4_8_VFD1:I.In_0,UL4_8_DISC AUXILIARY DISCONNECT,MCM02
UL4_8_VFD1:I.In_1,UL4_8_ENSH1 SHAFT ENCODER,MCM02
UL4_8_VFD1:I.In_2,PS2_1_TPE1 TRACKING PHOTOEYE,MCM02
UL4_8_VFD1:I.In_3,SPARE,MCM02
UL4_8_VFD1:I.IO_0,SPARE,MCM02
UL4_8_VFD1:I.IO_1,SPARE,MCM02
UL4_8_VFD1:SI.In00Data,SPARE,MCM02
UL4_8_VFD1:SI.In01Data,SPARE,MCM02
UL4_8_VFD1:SI.In02Data,SPARE,MCM02
UL4_8_VFD1:SI.In03Data,SPARE,MCM02
UL4_8_VFD1:SO.Out00Output,SPARE,MCM02
UL5_5_VFD1:I.In_0,UL5_5_DISC AUXILIARY DISCONNECT,MCM02
UL5_5_VFD1:I.In_1,UL5_5_ENSH1 SHAFT ENCODER,MCM02
UL5_5_VFD1:I.In_2,UL5_5_TPE1 TRACKING PHOTOEYE,MCM02
UL5_5_VFD1:I.In_3,UL5_5_TPE2 TRACKING PHOTOEYE,MCM02
UL5_5_VFD1:I.IO_0,SPARE,MCM02
UL5_5_VFD1:I.IO_1,SPARE,MCM02
UL5_5_VFD1:SI.In00Data,SPARE,MCM02
UL5_5_VFD1:SI.In01Data,SPARE,MCM02
UL5_5_VFD1:SI.In02Data,SPARE,MCM02
UL5_5_VFD1:SI.In03Data,SPARE,MCM02
UL5_5_VFD1:SO.Out00Output,SPARE,MCM02
UL5_6_VFD1:I.In_0,UL5_6_DISC AUXILIARY DISCONNECT,MCM02
UL5_6_VFD1:I.In_1,UL5_6_ENSH1 SHAFT ENCODER,MCM02
UL5_6_VFD1:I.In_2,UL5_6_TPE1 TRACKING PHOTOEYE,MCM02
UL5_6_VFD1:I.In_3,SPARE,MCM02
UL5_6_VFD1:I.IO_0,SPARE,MCM02
UL5_6_VFD1:I.IO_1,SPARE,MCM02
UL5_6_VFD1:SI.In00Data,SPARE,MCM02
UL5_6_VFD1:SI.In01Data,SPARE,MCM02
UL5_6_VFD1:SI.In02Data,SPARE,MCM02
UL5_6_VFD1:SI.In03Data,SPARE,MCM02
UL5_6_VFD1:SO.Out00Output,SPARE,MCM02
UL5_7_VFD1:I.In_0,UL5_7_DISC AUXILIARY DISCONNECT,MCM02
UL5_7_VFD1:I.In_1,UL5_7_ENSH1 SHAFT ENCODER,MCM02
UL5_7_VFD1:I.In_2,UL5_7_TPE1 TRACKING PHOTOEYE,MCM02
UL5_7_VFD1:I.In_3,SPARE,MCM02
UL5_7_VFD1:I.IO_0,SPARE,MCM02
UL5_7_VFD1:I.IO_1,SPARE,MCM02
UL5_7_VFD1:SI.In00Data,SPARE,MCM02
UL5_7_VFD1:SI.In01Data,SPARE,MCM02
UL5_7_VFD1:SI.In02Data,SPARE,MCM02
UL5_7_VFD1:SI.In03Data,SPARE,MCM02
UL5_7_VFD1:SO.Out00Output,SPARE,MCM02
UL5_8_VFD1:I.In_0,UL5_8_DISC AUXILIARY DISCONNECT,MCM02
UL5_8_VFD1:I.In_1,UL5_8_ENSH1 SHAFT ENCODER,MCM02
UL5_8_VFD1:I.In_2,UL5_8_TPE1 TRACKING PHOTOEYE,MCM02
UL5_8_VFD1:I.In_3,SPARE,MCM02
UL5_8_VFD1:O.IO_0,PS2_1_BCN1_A AMBER BEACON,MCM02
UL5_8_VFD1:I.IO_1,SPARE,MCM02
UL5_8_VFD1:SI.In00Data,SPARE,MCM02
UL5_8_VFD1:SI.In01Data,SPARE,MCM02
UL5_8_VFD1:SI.In02Data,SPARE,MCM02
UL5_8_VFD1:SI.In03Data,SPARE,MCM02
UL5_8_VFD1:SO.Out00Output,SPARE,MCM02
UL5_9_VFD1:I.In_0,UL5_9_DISC AUXILIARY DISCONNECT,MCM02
UL5_9_VFD1:I.In_1,UL5_9_ENSH1 SHAFT ENCODER,MCM02
UL5_9_VFD1:I.In_2,PS2_1_TPE2 TRACKING PHOTOEYE,MCM02
UL5_9_VFD1:I.In_3,SPARE,MCM02
UL5_9_VFD1:I.IO_0,PS2_1_JR1_PB JAM RESET PUSHBUTTON,MCM02
UL5_9_VFD1:O.IO_1,PS2_1_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
UL5_9_VFD1:SI.In00Data,SPARE,MCM02
UL5_9_VFD1:SI.In01Data,SPARE,MCM02
UL5_9_VFD1:SI.In02Data,SPARE,MCM02
UL5_9_VFD1:SI.In03Data,SPARE,MCM02
UL5_9_VFD1:SO.Out00Output,SPARE,MCM02
UL6_1_VFD1:I.In_0,UL6_1_DISC AUXILIARY DISCONNECT,MCM02
UL6_1_VFD1:I.In_1,SPARE,MCM02
UL6_1_VFD1:I.In_2,UL6_1_TPE1 TRACKING PHOTOEYE,MCM02
UL6_1_VFD1:I.In_3,SPARE,MCM02
UL6_1_VFD1:I.IO_0,SPARE,MCM02
UL6_1_VFD1:I.IO_1,SPARE,MCM02
UL6_1_VFD1:SI.In00Data,UL6_1_EPC1 EMERGENCY PULLCORD,MCM02
UL6_1_VFD1:SI.In01Data,UL6_1_EPC1_2 EMERGENCY PULLCORD,MCM02
UL6_1_VFD1:SI.In02Data,UL6_1_EPC2 EMERGENCY PULLCORD,MCM02
UL6_1_VFD1:SI.In03Data,UL6_1_EPC2_2 EMERGENCY PULLCORD,MCM02
UL6_1_VFD1:SO.Out00Output,SPARE,MCM02
UL6_4_VFD1:I.In_0,UL6_4_DISC AUXILIARY DISCONNECT,MCM02
UL6_4_VFD1:I.In_1,UL6_4_ENW1 WHEEL ENCODER,MCM02
UL6_4_VFD1:I.In_2,UL6_4_TPE1 TRACKING PHOTOEYE,MCM02
UL6_4_VFD1:I.In_3,UL6_4_TPE2 TRACKING PHOTOEYE,MCM02
UL6_4_VFD1:O.IO_0,UL6_2_BCN3_A AMBER BEACON,MCM02
UL6_4_VFD1:I.IO_1,SPARE,MCM02
UL6_4_VFD1:SI.In00Data,SPARE,MCM02
UL6_4_VFD1:SI.In01Data,SPARE,MCM02
UL6_4_VFD1:SI.In02Data,SPARE,MCM02
UL6_4_VFD1:SI.In03Data,SPARE,MCM02
UL6_4_VFD1:SO.Out00Output,SPARE,MCM02
UL6_5_VFD1:I.In_0,UL6_5_DISC AUXILIARY DISCONNECT,MCM02
UL6_5_VFD1:I.In_1,UL6_5_ENSH1 SHAFT ENCODER,MCM02
UL6_5_VFD1:I.In_2,UL6_5_TPE1 TRACKING PHOTOEYE,MCM02
UL6_5_VFD1:I.In_3,SPARE,MCM02
UL6_5_VFD1:I.IO_0,SPARE,MCM02
UL6_5_VFD1:I.IO_1,SPARE,MCM02
UL6_5_VFD1:SI.In00Data,SPARE,MCM02
UL6_5_VFD1:SI.In01Data,SPARE,MCM02
UL6_5_VFD1:SI.In02Data,SPARE,MCM02
UL6_5_VFD1:SI.In03Data,SPARE,MCM02
UL6_5_VFD1:SO.Out00Output,SPARE,MCM02
UL6_6_VFD1:I.In_0,UL6_6_DISC AUXILIARY DISCONNECT,MCM02
UL6_6_VFD1:I.In_1,UL6_6_ENSH1 SHAFT ENCODER,MCM02
UL6_6_VFD1:I.In_2,UL6_6_TPE1 TRACKING PHOTOEYE,MCM02
UL6_6_VFD1:I.In_3,SPARE,MCM02
UL6_6_VFD1:I.IO_0,SPARE,MCM02
UL6_6_VFD1:I.IO_1,SPARE,MCM02
UL6_6_VFD1:SI.In00Data,SPARE,MCM02
UL6_6_VFD1:SI.In01Data,SPARE,MCM02
UL6_6_VFD1:SI.In02Data,SPARE,MCM02
UL6_6_VFD1:SI.In03Data,SPARE,MCM02
UL6_6_VFD1:SO.Out00Output,SPARE,MCM02
UL6_7_VFD1:I.In_0,UL6_7_DISC AUXILIARY DISCONNECT,MCM02
UL6_7_VFD1:I.In_1,UL6_7_ENSH1 SHAFT ENCODER,MCM02
UL6_7_VFD1:I.In_2,UL6_7_TPE1 TRACKING PHOTOEYE,MCM02
UL6_7_VFD1:I.In_3,SPARE,MCM02
UL6_7_VFD1:O.IO_0,PS2_1_BCN2_A AMBER BEACON,MCM02
UL6_7_VFD1:I.IO_1,SPARE,MCM02
UL6_7_VFD1:SI.In00Data,SPARE,MCM02
UL6_7_VFD1:SI.In01Data,SPARE,MCM02
UL6_7_VFD1:SI.In02Data,SPARE,MCM02
UL6_7_VFD1:SI.In03Data,SPARE,MCM02
UL6_7_VFD1:SO.Out00Output,SPARE,MCM02
UL6_8_VFD1:I.In_0,UL6_8_DISC AUXILIARY DISCONNECT,MCM02
UL6_8_VFD1:I.In_1,UL6_8_ENSH1 SHAFT ENCODER,MCM02
UL6_8_VFD1:I.In_2,PS2_1_TPE3 TRACKING PHOTOEYE,MCM02
UL6_8_VFD1:I.In_3,SPARE,MCM02
UL6_8_VFD1:I.IO_0,PS2_1_JR2_PB JAM RESET PUSHBUTTON,MCM02
UL6_8_VFD1:O.IO_1,PS2_1_JR2_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
UL6_8_VFD1:SI.In00Data,SPARE,MCM02
UL6_8_VFD1:SI.In01Data,SPARE,MCM02
UL6_8_VFD1:SI.In02Data,SPARE,MCM02
UL6_8_VFD1:SI.In03Data,SPARE,MCM02
UL6_8_VFD1:SO.Out00Output,SPARE,MCM02
PS2_2_VFD1:I.In_0,PS2_2_DISC AUXILIARY DISCONNECT,MCM02
PS2_2_VFD1:I.In_1,PS2_2_ENSH1 SHAFT ENCODER,MCM02
PS2_2_VFD1:I.In_2,SPARE,MCM02
PS2_2_VFD1:I.In_3,SPARE,MCM02
PS2_2_VFD1:I.IO_0,PS2_1_BCN3,MCM02
PS2_2_VFD1:O.IO_1,PS2_1_BCN3_A AMBER BEACON,MCM02
PS2_2_VFD1:SI.In00Data,SPARE,MCM02
PS2_2_VFD1:SI.In01Data,SPARE,MCM02
PS2_2_VFD1:SI.In02Data,SPARE,MCM02
PS2_2_VFD1:SI.In03Data,SPARE,MCM02
PS2_2_VFD1:SO.Out00Output,SPARE,MCM02
UL4_4_VFD1:I.In_0,UL4_4_DISC AUXILIARY DISCONNECT,MCM02
UL4_4_VFD1:I.In_1,UL4_4_ENSH1 SHAFT ENCODER,MCM02
UL4_4_VFD1:I.In_2,UL4_3_TPE3 TRACKING PHOTOEYE,MCM02
UL4_4_VFD1:I.In_3,SPARE,MCM02
UL4_4_VFD1:I.IO_0,UL4_3_JR1_PB JAM RESET PUSHBUTTON,MCM02
UL4_4_VFD1:O.IO_1,UL4_3_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
UL4_4_VFD1:SI.In00Data,SPARE,MCM02
UL4_4_VFD1:SI.In01Data,SPARE,MCM02
UL4_4_VFD1:SI.In02Data,SPARE,MCM02
UL4_4_VFD1:SI.In03Data,SPARE,MCM02
UL4_4_VFD1:SO.Out00Output,SPARE,MCM02
UL5_4_VFD1:I.In_0,UL5_4_DISC AUXILIARY DISCONNECT,MCM02
UL5_4_VFD1:I.In_1,UL5_4_ENSH1 SHAFT ENCODER,MCM02
UL5_4_VFD1:I.In_2,SPARE,MCM02
UL5_4_VFD1:I.In_3,SPARE,MCM02
UL5_4_VFD1:I.IO_0,SPARE,MCM02
UL5_4_VFD1:I.IO_1,SPARE,MCM02
UL5_4_VFD1:SI.In00Data,SPARE,MCM02
UL5_4_VFD1:SI.In01Data,SPARE,MCM02
UL5_4_VFD1:SI.In02Data,SPARE,MCM02
UL5_4_VFD1:SI.In03Data,SPARE,MCM02
UL5_4_VFD1:SO.Out00Output,SPARE,MCM02
UL6_3_VFD1:I.In_0,UL6_3_DISC AUXILIARY DISCONNECT,MCM02
UL6_3_VFD1:I.In_1,UL6_3_ENSH1 SHAFT ENCODER,MCM02
UL6_3_VFD1:I.In_2,UL6_2_TPE2 TRACKING PHOTOEYE,MCM02
UL6_3_VFD1:I.In_3,SPARE,MCM02
UL6_3_VFD1:I.IO_0,UL6_2_JR1_PB JAM RESET PUSHBUTTON,MCM02
UL6_3_VFD1:O.IO_1,UL6_2_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
UL6_3_VFD1:SI.In00Data,SPARE,MCM02
UL6_3_VFD1:SI.In01Data,SPARE,MCM02
UL6_3_VFD1:SI.In02Data,SPARE,MCM02
UL6_3_VFD1:SI.In03Data,SPARE,MCM02
UL6_3_VFD1:SO.Out00Output,SPARE,MCM02
UL4_3_VFD1:I.In_0,UL4_3_DISC AUXILIARY DISCONNECT,MCM02
UL4_3_VFD1:I.In_1,UL4_3_ENW1 WHEEL ENCODER,MCM02
UL4_3_VFD1:I.In_2,SPARE,MCM02
UL4_3_VFD1:I.In_3,SPARE,MCM02
UL4_3_VFD1:I.IO_0,SPARE,MCM02
UL4_3_VFD1:I.IO_1,SPARE,MCM02
UL4_3_VFD1:SI.In00Data,SPARE,MCM02
UL4_3_VFD1:SI.In01Data,SPARE,MCM02
UL4_3_VFD1:SI.In02Data,SPARE,MCM02
UL4_3_VFD1:SI.In03Data,SPARE,MCM02
UL4_3_VFD1:SO.Out00Output,SPARE,MCM02
UL5_3_VFD1:I.In_0,UL5_3_DISC AUXILIARY DISCONNECT,MCM02
UL5_3_VFD1:I.In_1,UL5_3_ENW1 WHEEL ENCODER,MCM02
UL5_3_VFD1:I.In_2,UL5_3_TPE3 TRACKING PHOTOEYE,MCM02
UL5_3_VFD1:I.In_3,SPARE,MCM02
UL5_3_VFD1:I.IO_0,UL5_3_JR1_PB JAM RESET PUSHBUTTON,MCM02
UL5_3_VFD1:O.IO_1,UL5_3_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
UL5_3_VFD1:SI.In00Data,SPARE,MCM02
UL5_3_VFD1:SI.In01Data,SPARE,MCM02
UL5_3_VFD1:SI.In02Data,SPARE,MCM02
UL5_3_VFD1:SI.In03Data,SPARE,MCM02
UL5_3_VFD1:SO.Out00Output,SPARE,MCM02
UL6_2_VFD1:I.In_0,UL6_2_DISC AUXILIARY DISCONNECT,MCM02
UL6_2_VFD1:I.In_1,UL6_2_ENW1 WHEEL ENCODER,MCM02
UL6_2_VFD1:I.In_2,UL6_2_TPE1 TRACKING PHOTOEYE,MCM02
UL6_2_VFD1:I.In_3,SPARE,MCM02
UL6_2_VFD1:I.IO_0,SPARE,MCM02
UL6_2_VFD1:I.IO_1,SPARE,MCM02
UL6_2_VFD1:SI.In00Data,SPARE,MCM02
UL6_2_VFD1:SI.In01Data,SPARE,MCM02
UL6_2_VFD1:SI.In02Data,SPARE,MCM02
UL6_2_VFD1:SI.In03Data,SPARE,MCM02
UL6_2_VFD1:SO.Out00Output,SPARE,MCM02
PS2_1_VFD1:I.In_0,PS2_1_DISC AUXILIARY DISCONNECT,MCM02
PS2_1_VFD1:I.In_1,PS2_1_ENW1 WHEEL ENCODER,MCM02
PS2_1_VFD1:I.In_2,PS2_1_TPE4 TRACKING PHOTOEYE,MCM02
PS2_1_VFD1:I.In_3,SPARE,MCM02
PS2_1_VFD1:I.IO_0,PS2_1_JR3_PB JAM RESET PUSHBUTTON,MCM02
PS2_1_VFD1:O.IO_1,PS2_1_JR3_PB_LT JAM RESET PUSHBUTTON LIGHT,MCM02
PS2_1_VFD1:SI.In00Data,SPARE,MCM02
PS2_1_VFD1:SI.In01Data,SPARE,MCM02
PS2_1_VFD1:SI.In02Data,SPARE,MCM02
PS2_1_VFD1:SI.In03Data,SPARE,MCM02
PS2_1_VFD1:SO.Out00Output,SPARE,MCM02
UL4_3_FIO1:I.Pt01.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt03.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt04.Data,UL4_3_TPE1 TRACKING PHOTOEYE,MCM02
UL4_3_FIO1:O.Pt05.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt06.Data,UL4_3_TPE2 TRACKING PHOTOEYE,MCM02
UL4_3_FIO1:O.Pt07.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt08.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt09.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt10.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt11.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt12.Data,SPARE,MCM02
UL4_3_FIO1:O.Pt13.Data,SPARE,MCM02
UL4_3_FIO1:I.Pt14.Data,SPARE,MCM02
UL4_3_FIO1:O.Pt15.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt01.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt03.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt04.Data,UL5_3_TPE1 TRACKING PHOTOEYE,MCM02
UL5_3_FIO1:O.Pt05.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt06.Data,UL5_3_TPE2 TRACKING PHOTOEYE,MCM02
UL5_3_FIO1:O.Pt07.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt08.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt09.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt10.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt11.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt12.Data,SPARE,MCM02
UL5_3_FIO1:O.Pt13.Data,SPARE,MCM02
UL5_3_FIO1:I.Pt14.Data,SPARE,MCM02
UL5_3_FIO1:O.Pt15.Data,SPARE,MCM02
UL6_2_FIO1:I.Pt00.Data,UL6_1_SS1_SPB STOP PUSHBUTTON,MCM02
UL6_2_FIO1:O.Pt01.Data,UL6_1_SS1_STPB_LT START PUSHBUTTON LIGHT,MCM02
UL6_2_FIO1:I.Pt02.Data,UL6_1_SS1_STPB START PUSHBUTTON,MCM02
UL6_2_FIO1:I.Pt03.Data,SPARE,MCM02
UL6_2_FIO1:O.Pt04.Data,UL6_2_BCN1_R RED BEACON,MCM02
UL6_2_FIO1:O.Pt05.Data,SPARE,MCM02
UL6_2_FIO1:O.Pt06.Data,UL6_2_BCN2_R RED BEACON,MCM02
UL6_2_FIO1:O.Pt07.Data,SPARE,MCM02
UL6_2_FIO1:I.Pt09.Data,SPARE,MCM02
UL6_2_FIO1:I.Pt11.Data,SPARE,MCM02
UL6_2_FIO1:I.Pt12.Data,SPARE,MCM02
UL6_2_FIO1:O.Pt13.Data,SPARE,MCM02
UL6_2_FIO1:I.Pt14.Data,SPARE,MCM02
UL6_2_FIO1:O.Pt15.Data,SPARE,MCM02
1 Name Description Subsystem
2 Local:5:I.Data.0 MCM02 S_PBLT START MCM02
3 Local:5:I.Data.1 MCM02 ST_PB STOP MCM02
4 Local:5:I.Data.10 SPARE MCM02
5 Local:5:I.Data.11 SPARE MCM02
6 Local:5:I.Data.12 SPARE MCM02
7 Local:5:I.Data.13 SPARE MCM02
8 Local:5:I.Data.14 SPARE MCM02
9 Local:5:I.Data.15 SPARE MCM02
10 Local:5:I.Data.2 MCM02 MF_PBLT MOTOR FAULT RESET MCM02
11 Local:5:I.Data.3 MCM02 JR_PBLT JAM RESET MCM02
12 Local:5:I.Data.4 MCM02 LAP_PBLT LOW AIR PRESSURE MCM02
13 Local:5:I.Data.5 MCM02 PBFR_PBLT POWER BRANCH FAULT RESET MCM02
14 Local:5:I.Data.6 MCM02 BATTERY_FAULT_UPS BATTERY FAULT MCM02
15 Local:5:I.Data.7 MCM02 ON_BATTERY ON BATTERY MCM02
16 Local:5:I.Data.8 MCM02 UPS_BATTERY_LOW UPS BATTERY LOW MCM02
17 Local:5:I.Data.9 MCM02 NAT_SWITCH ALARM NAT SWITCH MCM02
18 Local:7:I.Pt00.Data MCM02 FIRE_ALARM FIRE ALARM MCM02
19 Local:7:I.Pt01.Data SPARE MCM02
20 Local:7:I.Pt10.Data SPARE MCM02
21 Local:7:I.Pt11.Data SPARE MCM02
22 Local:7:I.Pt12.Data SPARE MCM02
23 Local:7:I.Pt13.Data SPARE MCM02
24 Local:7:I.Pt14.Data SPARE MCM02
25 Local:7:I.Pt15.Data SPARE MCM02
26 Local:7:I.Pt02.Data MCM02 ES_PB ES_PB_CH1 MCM02
27 Local:7:I.Pt03.Data MCM02 ES_PB ES_PB_CH2 MCM02
28 Local:7:I.Pt04.Data SPARE MCM02
29 Local:7:I.Pt05.Data SPARE MCM02
30 Local:7:I.Pt06.Data SPARE MCM02
31 Local:7:I.Pt07.Data SPARE MCM02
32 Local:7:I.Pt08.Data SPARE MCM02
33 Local:7:I.Pt09.Data SPARE MCM02
34 Local:6:O.Data.0 MCM02 ES_LT EMERGENCY STOP ACTUATED MCM02
35 Local:6:O.Data.1 MCM02 S_PBLT START LIGHT MCM02
36 Local:6:O.Data.10 SPARE MCM02
37 Local:6:O.Data.11 SPARE MCM02
38 Local:6:O.Data.12 SPARE MCM02
39 Local:6:O.Data.13 SPARE MCM02
40 Local:6:O.Data.14 SPARE MCM02
41 Local:6:O.Data.15 SPARE MCM02
42 Local:6:O.Data.2 MCM02 MF_PBLT MOTOR FAULT LIGHT MCM02
43 Local:6:O.Data.3 MCM02 JR_PBLT JAM RESTART LIGHT MCM02
44 Local:6:O.Data.4 MCM02 LAP_PBLT LOW AIR PRESSURE LIGHT MCM02
45 Local:6:O.Data.5 MCM02 PBFR_PBLT POWER BRANCH FAULT RESET LIGHT MCM02
46 Local:6:O.Data.6 SPARE MCM02
47 Local:6:O.Data.7 SPARE MCM02
48 Local:6:O.Data.8 SPARE MCM02
49 Local:6:O.Data.9 SPARE MCM02
50 PS2_3_VFD1:I.In_0 PS2_3_DISC AUXILIARY DISCONNECT MCM02
51 PS2_3_VFD1:I.In_1 SPARE MCM02
52 PS2_3_VFD1:I.In_2 PS2_3_TPE1 TRACKING PHOTOEYE MCM02
53 PS2_3_VFD1:I.In_3 PS2_3_TPE2 TRACKING PHOTOEYE MCM02
54 PS2_3_VFD1:I.IO_0 SPARE MCM02
55 PS2_3_VFD1:I.IO_1 SPARE MCM02
56 PS2_3_VFD1:SI.In00Data SPARE MCM02
57 PS2_3_VFD1:SI.In01Data SPARE MCM02
58 PS2_3_VFD1:SI.In02Data SPARE MCM02
59 PS2_3_VFD1:SI.In03Data SPARE MCM02
60 PS2_3_VFD1:SO.Out00Output SPARE MCM02
61 PS2_4A_VFD1:I.In_0 PS2_4A_DISC AUXILIARY DISCONNECT MCM02
62 PS2_4A_VFD1:I.In_1 SPARE MCM02
63 PS2_4A_VFD1:I.In_2 SPARE MCM02
64 PS2_4A_VFD1:I.In_3 SPARE MCM02
65 PS2_4A_VFD1:I.IO_0 SPARE MCM02
66 PS2_4A_VFD1:I.IO_1 SPARE MCM02
67 PS2_4A_VFD1:SI.In00Data SPARE MCM02
68 PS2_4A_VFD1:SI.In01Data SPARE MCM02
69 PS2_4A_VFD1:SI.In02Data SPARE MCM02
70 PS2_4A_VFD1:SI.In03Data SPARE MCM02
71 PS2_4A_VFD1:SO.Out00Output SPARE MCM02
72 PS2_4B_VFD1:I.In_0 PS2_4B_DISC AUXILIARY DISCONNECT MCM02
73 PS2_4B_VFD1:I.In_1 SPARE MCM02
74 PS2_4B_VFD1:I.In_2 SPARE MCM02
75 PS2_4B_VFD1:I.In_3 SPARE MCM02
76 PS2_4B_VFD1:O.IO_0 PS2_7_BCN2_R RED BEACON MCM02
77 PS2_4B_VFD1:I.IO_1 SPARE MCM02
78 PS2_4B_VFD1:SI.In00Data SPARE MCM02
79 PS2_4B_VFD1:SI.In01Data SPARE MCM02
80 PS2_4B_VFD1:SI.In02Data SPARE MCM02
81 PS2_4B_VFD1:SI.In03Data SPARE MCM02
82 PS2_4B_VFD1:SO.Out00Output SPARE MCM02
83 PS2_5_VFD1:I.In_0 PS2_5_DISC AUXILIARY DISCONNECT MCM02
84 PS2_5_VFD1:I.In_1 PS2_5_ENSH1 SHAFT ENCODER MCM02
85 PS2_5_VFD1:I.In_2 PS2_5_TPE1 TRACKING PHOTOEYE MCM02
86 PS2_5_VFD1:I.In_3 PS2_5_TPE2 TRACKING PHOTOEYE MCM02
87 PS2_5_VFD1:I.IO_0 PS2_7_S2_PB START PUSHBUTTON MCM02
88 PS2_5_VFD1:O.IO_1 PS2_7_S2_PB_LT START PUSHBUTTON LIGHT MCM02
89 PS2_5_VFD1:SI.In00Data SPARE MCM02
90 PS2_5_VFD1:SI.In01Data SPARE MCM02
91 PS2_5_VFD1:SI.In02Data SPARE MCM02
92 PS2_5_VFD1:SI.In03Data SPARE MCM02
93 PS2_5_VFD1:SO.Out00Output SPARE MCM02
94 PS2_6_VFD1:I.In_0 PS2_6_DISC AUXILIARY DISCONNECT MCM02
95 PS2_6_VFD1:I.In_1 PS2_6_ENSH1 SHAFT ENCODER MCM02
96 PS2_6_VFD1:I.In_2 PS2_6_TPE1 TRACKING PHOTOEYE MCM02
97 PS2_6_VFD1:I.In_3 SPARE MCM02
98 PS2_6_VFD1:O.IO_0 PS2_7_BCN1_R RED BEACON MCM02
99 PS2_6_VFD1:I.IO_1 SPARE MCM02
100 PS2_6_VFD1:SI.In00Data SPARE MCM02
101 PS2_6_VFD1:SI.In01Data SPARE MCM02
102 PS2_6_VFD1:SI.In02Data SPARE MCM02
103 PS2_6_VFD1:SI.In03Data SPARE MCM02
104 PS2_6_VFD1:SO.Out00Output SPARE MCM02
105 PS2_7_VFD1:I.In_0 PS2_7_DISC AUXILIARY DISCONNECT MCM02
106 PS2_7_VFD1:I.In_1 PS2_7_ENSH1 SHAFT ENCODER MCM02
107 PS2_7_VFD1:I.In_2 PS2_7_TPE1 TRACKING PHOTOEYE MCM02
108 PS2_7_VFD1:I.In_3 SPARE MCM02
109 PS2_7_VFD1:I.IO_0 PS2_7_S1_PB START PUSHBUTTON MCM02
110 PS2_7_VFD1:O.IO_1 PS2_7_S1_PB_LT START PUSHBUTTON LIGHT MCM02
111 PS2_7_VFD1:SI.In00Data PS2_7_EPC1 EMERGENCY PULLCORD MCM02
112 PS2_7_VFD1:SI.In01Data PS2_7_EPC1_2 EMERGENCY PULLCORD MCM02
113 PS2_7_VFD1:SI.In02Data PS2_7_EPC2 EMERGENCY PULLCORD MCM02
114 PS2_7_VFD1:SI.In03Data PS2_7_EPC2_2 EMERGENCY PULLCORD MCM02
115 PS2_7_VFD1:SO.Out00Output SPARE MCM02
116 UL4_5_VFD1:I.In_0 UL4_5_DISC AUXILIARY DISCONNECT MCM02
117 UL4_5_VFD1:I.In_1 UL4_5_ENSH1 SHAFT ENCODER MCM02
118 UL4_5_VFD1:I.In_2 UL4_5_TPE1 TRACKING PHOTOEYE MCM02
119 UL4_5_VFD1:I.In_3 UL4_5_TPE2 TRACKING PHOTOEYE MCM02
120 UL4_5_VFD1:O.IO_0 UL4_3_BCN1_A AMBER BEACON MCM02
121 UL4_5_VFD1:I.IO_1 SPARE MCM02
122 UL4_5_VFD1:SI.In00Data SPARE MCM02
123 UL4_5_VFD1:SI.In01Data SPARE MCM02
124 UL4_5_VFD1:SI.In02Data SPARE MCM02
125 UL4_5_VFD1:SI.In03Data SPARE MCM02
126 UL4_5_VFD1:SO.Out00Output SPARE MCM02
127 UL4_6_VFD1:I.In_0 UL4_6_DISC AUXILIARY DISCONNECT MCM02
128 UL4_6_VFD1:I.In_1 UL4_6_ENSH1 SHAFT ENCODER MCM02
129 UL4_6_VFD1:I.In_2 UL4_6_TPE1 TRACKING PHOTOEYE MCM02
130 UL4_6_VFD1:I.In_3 SPARE MCM02
131 UL4_6_VFD1:I.IO_0 SPARE MCM02
132 UL4_6_VFD1:I.IO_1 SPARE MCM02
133 UL4_6_VFD1:SI.In00Data SPARE MCM02
134 UL4_6_VFD1:SI.In01Data SPARE MCM02
135 UL4_6_VFD1:SI.In02Data SPARE MCM02
136 UL4_6_VFD1:SI.In03Data SPARE MCM02
137 UL4_6_VFD1:SO.Out00Output SPARE MCM02
138 UL4_7_VFD1:I.In_0 UL4_7_DISC AUXILIARY DISCONNECT MCM02
139 UL4_7_VFD1:I.In_1 UL4_7_ENSH1 SHAFT ENCODER MCM02
140 UL4_7_VFD1:I.In_2 UL4_7_TPE1 TRACKING PHOTOEYE MCM02
141 UL4_7_VFD1:I.In_3 SPARE MCM02
142 UL4_7_VFD1:I.IO_0 SPARE MCM02
143 UL4_7_VFD1:I.IO_1 SPARE MCM02
144 UL4_7_VFD1:SI.In00Data SPARE MCM02
145 UL4_7_VFD1:SI.In01Data SPARE MCM02
146 UL4_7_VFD1:SI.In02Data SPARE MCM02
147 UL4_7_VFD1:SI.In03Data SPARE MCM02
148 UL4_7_VFD1:SO.Out00Output SPARE MCM02
149 UL4_8_VFD1:I.In_0 UL4_8_DISC AUXILIARY DISCONNECT MCM02
150 UL4_8_VFD1:I.In_1 UL4_8_ENSH1 SHAFT ENCODER MCM02
151 UL4_8_VFD1:I.In_2 PS2_1_TPE1 TRACKING PHOTOEYE MCM02
152 UL4_8_VFD1:I.In_3 SPARE MCM02
153 UL4_8_VFD1:I.IO_0 SPARE MCM02
154 UL4_8_VFD1:I.IO_1 SPARE MCM02
155 UL4_8_VFD1:SI.In00Data SPARE MCM02
156 UL4_8_VFD1:SI.In01Data SPARE MCM02
157 UL4_8_VFD1:SI.In02Data SPARE MCM02
158 UL4_8_VFD1:SI.In03Data SPARE MCM02
159 UL4_8_VFD1:SO.Out00Output SPARE MCM02
160 UL5_5_VFD1:I.In_0 UL5_5_DISC AUXILIARY DISCONNECT MCM02
161 UL5_5_VFD1:I.In_1 UL5_5_ENSH1 SHAFT ENCODER MCM02
162 UL5_5_VFD1:I.In_2 UL5_5_TPE1 TRACKING PHOTOEYE MCM02
163 UL5_5_VFD1:I.In_3 UL5_5_TPE2 TRACKING PHOTOEYE MCM02
164 UL5_5_VFD1:I.IO_0 SPARE MCM02
165 UL5_5_VFD1:I.IO_1 SPARE MCM02
166 UL5_5_VFD1:SI.In00Data SPARE MCM02
167 UL5_5_VFD1:SI.In01Data SPARE MCM02
168 UL5_5_VFD1:SI.In02Data SPARE MCM02
169 UL5_5_VFD1:SI.In03Data SPARE MCM02
170 UL5_5_VFD1:SO.Out00Output SPARE MCM02
171 UL5_6_VFD1:I.In_0 UL5_6_DISC AUXILIARY DISCONNECT MCM02
172 UL5_6_VFD1:I.In_1 UL5_6_ENSH1 SHAFT ENCODER MCM02
173 UL5_6_VFD1:I.In_2 UL5_6_TPE1 TRACKING PHOTOEYE MCM02
174 UL5_6_VFD1:I.In_3 SPARE MCM02
175 UL5_6_VFD1:I.IO_0 SPARE MCM02
176 UL5_6_VFD1:I.IO_1 SPARE MCM02
177 UL5_6_VFD1:SI.In00Data SPARE MCM02
178 UL5_6_VFD1:SI.In01Data SPARE MCM02
179 UL5_6_VFD1:SI.In02Data SPARE MCM02
180 UL5_6_VFD1:SI.In03Data SPARE MCM02
181 UL5_6_VFD1:SO.Out00Output SPARE MCM02
182 UL5_7_VFD1:I.In_0 UL5_7_DISC AUXILIARY DISCONNECT MCM02
183 UL5_7_VFD1:I.In_1 UL5_7_ENSH1 SHAFT ENCODER MCM02
184 UL5_7_VFD1:I.In_2 UL5_7_TPE1 TRACKING PHOTOEYE MCM02
185 UL5_7_VFD1:I.In_3 SPARE MCM02
186 UL5_7_VFD1:I.IO_0 SPARE MCM02
187 UL5_7_VFD1:I.IO_1 SPARE MCM02
188 UL5_7_VFD1:SI.In00Data SPARE MCM02
189 UL5_7_VFD1:SI.In01Data SPARE MCM02
190 UL5_7_VFD1:SI.In02Data SPARE MCM02
191 UL5_7_VFD1:SI.In03Data SPARE MCM02
192 UL5_7_VFD1:SO.Out00Output SPARE MCM02
193 UL5_8_VFD1:I.In_0 UL5_8_DISC AUXILIARY DISCONNECT MCM02
194 UL5_8_VFD1:I.In_1 UL5_8_ENSH1 SHAFT ENCODER MCM02
195 UL5_8_VFD1:I.In_2 UL5_8_TPE1 TRACKING PHOTOEYE MCM02
196 UL5_8_VFD1:I.In_3 SPARE MCM02
197 UL5_8_VFD1:O.IO_0 PS2_1_BCN1_A AMBER BEACON MCM02
198 UL5_8_VFD1:I.IO_1 SPARE MCM02
199 UL5_8_VFD1:SI.In00Data SPARE MCM02
200 UL5_8_VFD1:SI.In01Data SPARE MCM02
201 UL5_8_VFD1:SI.In02Data SPARE MCM02
202 UL5_8_VFD1:SI.In03Data SPARE MCM02
203 UL5_8_VFD1:SO.Out00Output SPARE MCM02
204 UL5_9_VFD1:I.In_0 UL5_9_DISC AUXILIARY DISCONNECT MCM02
205 UL5_9_VFD1:I.In_1 UL5_9_ENSH1 SHAFT ENCODER MCM02
206 UL5_9_VFD1:I.In_2 PS2_1_TPE2 TRACKING PHOTOEYE MCM02
207 UL5_9_VFD1:I.In_3 SPARE MCM02
208 UL5_9_VFD1:I.IO_0 PS2_1_JR1_PB JAM RESET PUSHBUTTON MCM02
209 UL5_9_VFD1:O.IO_1 PS2_1_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
210 UL5_9_VFD1:SI.In00Data SPARE MCM02
211 UL5_9_VFD1:SI.In01Data SPARE MCM02
212 UL5_9_VFD1:SI.In02Data SPARE MCM02
213 UL5_9_VFD1:SI.In03Data SPARE MCM02
214 UL5_9_VFD1:SO.Out00Output SPARE MCM02
215 UL6_1_VFD1:I.In_0 UL6_1_DISC AUXILIARY DISCONNECT MCM02
216 UL6_1_VFD1:I.In_1 SPARE MCM02
217 UL6_1_VFD1:I.In_2 UL6_1_TPE1 TRACKING PHOTOEYE MCM02
218 UL6_1_VFD1:I.In_3 SPARE MCM02
219 UL6_1_VFD1:I.IO_0 SPARE MCM02
220 UL6_1_VFD1:I.IO_1 SPARE MCM02
221 UL6_1_VFD1:SI.In00Data UL6_1_EPC1 EMERGENCY PULLCORD MCM02
222 UL6_1_VFD1:SI.In01Data UL6_1_EPC1_2 EMERGENCY PULLCORD MCM02
223 UL6_1_VFD1:SI.In02Data UL6_1_EPC2 EMERGENCY PULLCORD MCM02
224 UL6_1_VFD1:SI.In03Data UL6_1_EPC2_2 EMERGENCY PULLCORD MCM02
225 UL6_1_VFD1:SO.Out00Output SPARE MCM02
226 UL6_4_VFD1:I.In_0 UL6_4_DISC AUXILIARY DISCONNECT MCM02
227 UL6_4_VFD1:I.In_1 UL6_4_ENW1 WHEEL ENCODER MCM02
228 UL6_4_VFD1:I.In_2 UL6_4_TPE1 TRACKING PHOTOEYE MCM02
229 UL6_4_VFD1:I.In_3 UL6_4_TPE2 TRACKING PHOTOEYE MCM02
230 UL6_4_VFD1:O.IO_0 UL6_2_BCN3_A AMBER BEACON MCM02
231 UL6_4_VFD1:I.IO_1 SPARE MCM02
232 UL6_4_VFD1:SI.In00Data SPARE MCM02
233 UL6_4_VFD1:SI.In01Data SPARE MCM02
234 UL6_4_VFD1:SI.In02Data SPARE MCM02
235 UL6_4_VFD1:SI.In03Data SPARE MCM02
236 UL6_4_VFD1:SO.Out00Output SPARE MCM02
237 UL6_5_VFD1:I.In_0 UL6_5_DISC AUXILIARY DISCONNECT MCM02
238 UL6_5_VFD1:I.In_1 UL6_5_ENSH1 SHAFT ENCODER MCM02
239 UL6_5_VFD1:I.In_2 UL6_5_TPE1 TRACKING PHOTOEYE MCM02
240 UL6_5_VFD1:I.In_3 SPARE MCM02
241 UL6_5_VFD1:I.IO_0 SPARE MCM02
242 UL6_5_VFD1:I.IO_1 SPARE MCM02
243 UL6_5_VFD1:SI.In00Data SPARE MCM02
244 UL6_5_VFD1:SI.In01Data SPARE MCM02
245 UL6_5_VFD1:SI.In02Data SPARE MCM02
246 UL6_5_VFD1:SI.In03Data SPARE MCM02
247 UL6_5_VFD1:SO.Out00Output SPARE MCM02
248 UL6_6_VFD1:I.In_0 UL6_6_DISC AUXILIARY DISCONNECT MCM02
249 UL6_6_VFD1:I.In_1 UL6_6_ENSH1 SHAFT ENCODER MCM02
250 UL6_6_VFD1:I.In_2 UL6_6_TPE1 TRACKING PHOTOEYE MCM02
251 UL6_6_VFD1:I.In_3 SPARE MCM02
252 UL6_6_VFD1:I.IO_0 SPARE MCM02
253 UL6_6_VFD1:I.IO_1 SPARE MCM02
254 UL6_6_VFD1:SI.In00Data SPARE MCM02
255 UL6_6_VFD1:SI.In01Data SPARE MCM02
256 UL6_6_VFD1:SI.In02Data SPARE MCM02
257 UL6_6_VFD1:SI.In03Data SPARE MCM02
258 UL6_6_VFD1:SO.Out00Output SPARE MCM02
259 UL6_7_VFD1:I.In_0 UL6_7_DISC AUXILIARY DISCONNECT MCM02
260 UL6_7_VFD1:I.In_1 UL6_7_ENSH1 SHAFT ENCODER MCM02
261 UL6_7_VFD1:I.In_2 UL6_7_TPE1 TRACKING PHOTOEYE MCM02
262 UL6_7_VFD1:I.In_3 SPARE MCM02
263 UL6_7_VFD1:O.IO_0 PS2_1_BCN2_A AMBER BEACON MCM02
264 UL6_7_VFD1:I.IO_1 SPARE MCM02
265 UL6_7_VFD1:SI.In00Data SPARE MCM02
266 UL6_7_VFD1:SI.In01Data SPARE MCM02
267 UL6_7_VFD1:SI.In02Data SPARE MCM02
268 UL6_7_VFD1:SI.In03Data SPARE MCM02
269 UL6_7_VFD1:SO.Out00Output SPARE MCM02
270 UL6_8_VFD1:I.In_0 UL6_8_DISC AUXILIARY DISCONNECT MCM02
271 UL6_8_VFD1:I.In_1 UL6_8_ENSH1 SHAFT ENCODER MCM02
272 UL6_8_VFD1:I.In_2 PS2_1_TPE3 TRACKING PHOTOEYE MCM02
273 UL6_8_VFD1:I.In_3 SPARE MCM02
274 UL6_8_VFD1:I.IO_0 PS2_1_JR2_PB JAM RESET PUSHBUTTON MCM02
275 UL6_8_VFD1:O.IO_1 PS2_1_JR2_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
276 UL6_8_VFD1:SI.In00Data SPARE MCM02
277 UL6_8_VFD1:SI.In01Data SPARE MCM02
278 UL6_8_VFD1:SI.In02Data SPARE MCM02
279 UL6_8_VFD1:SI.In03Data SPARE MCM02
280 UL6_8_VFD1:SO.Out00Output SPARE MCM02
281 PS2_2_VFD1:I.In_0 PS2_2_DISC AUXILIARY DISCONNECT MCM02
282 PS2_2_VFD1:I.In_1 PS2_2_ENSH1 SHAFT ENCODER MCM02
283 PS2_2_VFD1:I.In_2 SPARE MCM02
284 PS2_2_VFD1:I.In_3 SPARE MCM02
285 PS2_2_VFD1:I.IO_0 PS2_1_BCN3 MCM02
286 PS2_2_VFD1:O.IO_1 PS2_1_BCN3_A AMBER BEACON MCM02
287 PS2_2_VFD1:SI.In00Data SPARE MCM02
288 PS2_2_VFD1:SI.In01Data SPARE MCM02
289 PS2_2_VFD1:SI.In02Data SPARE MCM02
290 PS2_2_VFD1:SI.In03Data SPARE MCM02
291 PS2_2_VFD1:SO.Out00Output SPARE MCM02
292 UL4_4_VFD1:I.In_0 UL4_4_DISC AUXILIARY DISCONNECT MCM02
293 UL4_4_VFD1:I.In_1 UL4_4_ENSH1 SHAFT ENCODER MCM02
294 UL4_4_VFD1:I.In_2 UL4_3_TPE3 TRACKING PHOTOEYE MCM02
295 UL4_4_VFD1:I.In_3 SPARE MCM02
296 UL4_4_VFD1:I.IO_0 UL4_3_JR1_PB JAM RESET PUSHBUTTON MCM02
297 UL4_4_VFD1:O.IO_1 UL4_3_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
298 UL4_4_VFD1:SI.In00Data SPARE MCM02
299 UL4_4_VFD1:SI.In01Data SPARE MCM02
300 UL4_4_VFD1:SI.In02Data SPARE MCM02
301 UL4_4_VFD1:SI.In03Data SPARE MCM02
302 UL4_4_VFD1:SO.Out00Output SPARE MCM02
303 UL5_4_VFD1:I.In_0 UL5_4_DISC AUXILIARY DISCONNECT MCM02
304 UL5_4_VFD1:I.In_1 UL5_4_ENSH1 SHAFT ENCODER MCM02
305 UL5_4_VFD1:I.In_2 SPARE MCM02
306 UL5_4_VFD1:I.In_3 SPARE MCM02
307 UL5_4_VFD1:I.IO_0 SPARE MCM02
308 UL5_4_VFD1:I.IO_1 SPARE MCM02
309 UL5_4_VFD1:SI.In00Data SPARE MCM02
310 UL5_4_VFD1:SI.In01Data SPARE MCM02
311 UL5_4_VFD1:SI.In02Data SPARE MCM02
312 UL5_4_VFD1:SI.In03Data SPARE MCM02
313 UL5_4_VFD1:SO.Out00Output SPARE MCM02
314 UL6_3_VFD1:I.In_0 UL6_3_DISC AUXILIARY DISCONNECT MCM02
315 UL6_3_VFD1:I.In_1 UL6_3_ENSH1 SHAFT ENCODER MCM02
316 UL6_3_VFD1:I.In_2 UL6_2_TPE2 TRACKING PHOTOEYE MCM02
317 UL6_3_VFD1:I.In_3 SPARE MCM02
318 UL6_3_VFD1:I.IO_0 UL6_2_JR1_PB JAM RESET PUSHBUTTON MCM02
319 UL6_3_VFD1:O.IO_1 UL6_2_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
320 UL6_3_VFD1:SI.In00Data SPARE MCM02
321 UL6_3_VFD1:SI.In01Data SPARE MCM02
322 UL6_3_VFD1:SI.In02Data SPARE MCM02
323 UL6_3_VFD1:SI.In03Data SPARE MCM02
324 UL6_3_VFD1:SO.Out00Output SPARE MCM02
325 UL4_3_VFD1:I.In_0 UL4_3_DISC AUXILIARY DISCONNECT MCM02
326 UL4_3_VFD1:I.In_1 UL4_3_ENW1 WHEEL ENCODER MCM02
327 UL4_3_VFD1:I.In_2 SPARE MCM02
328 UL4_3_VFD1:I.In_3 SPARE MCM02
329 UL4_3_VFD1:I.IO_0 SPARE MCM02
330 UL4_3_VFD1:I.IO_1 SPARE MCM02
331 UL4_3_VFD1:SI.In00Data SPARE MCM02
332 UL4_3_VFD1:SI.In01Data SPARE MCM02
333 UL4_3_VFD1:SI.In02Data SPARE MCM02
334 UL4_3_VFD1:SI.In03Data SPARE MCM02
335 UL4_3_VFD1:SO.Out00Output SPARE MCM02
336 UL5_3_VFD1:I.In_0 UL5_3_DISC AUXILIARY DISCONNECT MCM02
337 UL5_3_VFD1:I.In_1 UL5_3_ENW1 WHEEL ENCODER MCM02
338 UL5_3_VFD1:I.In_2 UL5_3_TPE3 TRACKING PHOTOEYE MCM02
339 UL5_3_VFD1:I.In_3 SPARE MCM02
340 UL5_3_VFD1:I.IO_0 UL5_3_JR1_PB JAM RESET PUSHBUTTON MCM02
341 UL5_3_VFD1:O.IO_1 UL5_3_JR1_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
342 UL5_3_VFD1:SI.In00Data SPARE MCM02
343 UL5_3_VFD1:SI.In01Data SPARE MCM02
344 UL5_3_VFD1:SI.In02Data SPARE MCM02
345 UL5_3_VFD1:SI.In03Data SPARE MCM02
346 UL5_3_VFD1:SO.Out00Output SPARE MCM02
347 UL6_2_VFD1:I.In_0 UL6_2_DISC AUXILIARY DISCONNECT MCM02
348 UL6_2_VFD1:I.In_1 UL6_2_ENW1 WHEEL ENCODER MCM02
349 UL6_2_VFD1:I.In_2 UL6_2_TPE1 TRACKING PHOTOEYE MCM02
350 UL6_2_VFD1:I.In_3 SPARE MCM02
351 UL6_2_VFD1:I.IO_0 SPARE MCM02
352 UL6_2_VFD1:I.IO_1 SPARE MCM02
353 UL6_2_VFD1:SI.In00Data SPARE MCM02
354 UL6_2_VFD1:SI.In01Data SPARE MCM02
355 UL6_2_VFD1:SI.In02Data SPARE MCM02
356 UL6_2_VFD1:SI.In03Data SPARE MCM02
357 UL6_2_VFD1:SO.Out00Output SPARE MCM02
358 PS2_1_VFD1:I.In_0 PS2_1_DISC AUXILIARY DISCONNECT MCM02
359 PS2_1_VFD1:I.In_1 PS2_1_ENW1 WHEEL ENCODER MCM02
360 PS2_1_VFD1:I.In_2 PS2_1_TPE4 TRACKING PHOTOEYE MCM02
361 PS2_1_VFD1:I.In_3 SPARE MCM02
362 PS2_1_VFD1:I.IO_0 PS2_1_JR3_PB JAM RESET PUSHBUTTON MCM02
363 PS2_1_VFD1:O.IO_1 PS2_1_JR3_PB_LT JAM RESET PUSHBUTTON LIGHT MCM02
364 PS2_1_VFD1:SI.In00Data SPARE MCM02
365 PS2_1_VFD1:SI.In01Data SPARE MCM02
366 PS2_1_VFD1:SI.In02Data SPARE MCM02
367 PS2_1_VFD1:SI.In03Data SPARE MCM02
368 PS2_1_VFD1:SO.Out00Output SPARE MCM02
369 UL4_3_FIO1:I.Pt01.Data SPARE MCM02
370 UL4_3_FIO1:I.Pt03.Data SPARE MCM02
371 UL4_3_FIO1:I.Pt04.Data UL4_3_TPE1 TRACKING PHOTOEYE MCM02
372 UL4_3_FIO1:O.Pt05.Data SPARE MCM02
373 UL4_3_FIO1:I.Pt06.Data UL4_3_TPE2 TRACKING PHOTOEYE MCM02
374 UL4_3_FIO1:O.Pt07.Data SPARE MCM02
375 UL4_3_FIO1:I.Pt08.Data SPARE MCM02
376 UL4_3_FIO1:I.Pt09.Data SPARE MCM02
377 UL4_3_FIO1:I.Pt10.Data SPARE MCM02
378 UL4_3_FIO1:I.Pt11.Data SPARE MCM02
379 UL4_3_FIO1:I.Pt12.Data SPARE MCM02
380 UL4_3_FIO1:O.Pt13.Data SPARE MCM02
381 UL4_3_FIO1:I.Pt14.Data SPARE MCM02
382 UL4_3_FIO1:O.Pt15.Data SPARE MCM02
383 UL5_3_FIO1:I.Pt01.Data SPARE MCM02
384 UL5_3_FIO1:I.Pt03.Data SPARE MCM02
385 UL5_3_FIO1:I.Pt04.Data UL5_3_TPE1 TRACKING PHOTOEYE MCM02
386 UL5_3_FIO1:O.Pt05.Data SPARE MCM02
387 UL5_3_FIO1:I.Pt06.Data UL5_3_TPE2 TRACKING PHOTOEYE MCM02
388 UL5_3_FIO1:O.Pt07.Data SPARE MCM02
389 UL5_3_FIO1:I.Pt08.Data SPARE MCM02
390 UL5_3_FIO1:I.Pt09.Data SPARE MCM02
391 UL5_3_FIO1:I.Pt10.Data SPARE MCM02
392 UL5_3_FIO1:I.Pt11.Data SPARE MCM02
393 UL5_3_FIO1:I.Pt12.Data SPARE MCM02
394 UL5_3_FIO1:O.Pt13.Data SPARE MCM02
395 UL5_3_FIO1:I.Pt14.Data SPARE MCM02
396 UL5_3_FIO1:O.Pt15.Data SPARE MCM02
397 UL6_2_FIO1:I.Pt00.Data UL6_1_SS1_SPB STOP PUSHBUTTON MCM02
398 UL6_2_FIO1:O.Pt01.Data UL6_1_SS1_STPB_LT START PUSHBUTTON LIGHT MCM02
399 UL6_2_FIO1:I.Pt02.Data UL6_1_SS1_STPB START PUSHBUTTON MCM02
400 UL6_2_FIO1:I.Pt03.Data SPARE MCM02
401 UL6_2_FIO1:O.Pt04.Data UL6_2_BCN1_R RED BEACON MCM02
402 UL6_2_FIO1:O.Pt05.Data SPARE MCM02
403 UL6_2_FIO1:O.Pt06.Data UL6_2_BCN2_R RED BEACON MCM02
404 UL6_2_FIO1:O.Pt07.Data SPARE MCM02
405 UL6_2_FIO1:I.Pt09.Data SPARE MCM02
406 UL6_2_FIO1:I.Pt11.Data SPARE MCM02
407 UL6_2_FIO1:I.Pt12.Data SPARE MCM02
408 UL6_2_FIO1:O.Pt13.Data SPARE MCM02
409 UL6_2_FIO1:I.Pt14.Data SPARE MCM02
410 UL6_2_FIO1:O.Pt15.Data SPARE MCM02

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,8 @@ def classify_signal(desca, tagname, descb=None):
return 'O'
elif re.search(r'BCN', desca_str) and 'FIOH' in tagname_str:
return 'O'
elif re.search(r'_H', desca_str):
return 'O'
# IOLink patterns (check FIOH before FIO due to priority)
elif re.search(r'SOL', desca_str) and re.search(r'DIVERT', descb_str):
@ -52,7 +54,7 @@ def classify_signal(desca, tagname, descb=None):
return 'I'
elif re.search(r'FPE', desca_str):
return 'I'
elif re.search(r'ENC', desca_str):
elif re.search(r'EN', desca_str):
return 'I'
elif re.search(r'PS', desca_str):
return 'I'

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
MCM01_S_PB=SFT_MCM01_S_PB, PS1_1_S1_PB=SFT_PS1_1_S1_PB, PS1_1_S2_PB=SFT_PS1_1_S2_PB, PS1_5_S1_PB=SFT_PS1_5_S1_PB, PS1_5_S2_PB=SFT_PS1_5_S2_PB, PS2_1_S1_PB=SFT_PS2_1_S1_PB, PS2_1_S2_PB=SFT_PS2_1_S2_PB, PS2_6_S1_PB=SFT_PS2_6_S1_PB, PS2_6_S2_PB=SFT_PS2_6_S2_PB, PS3_12_S1_PB=SFT_PS3_12_S1_PB, PS3_12_S2_PB=SFT_PS3_12_S2_PB, PS3_1_S1_PB=SFT_PS3_1_S1_PB, PS3_1_S2_PB=SFT_PS3_1_S2_PB, PS4_14_S1_PB=SFT_PS4_14_S1_PB, PS4_14_S2_PB=SFT_PS4_14_S2_PB, PS4_1_S1_PB=SFT_PS4_1_S1_PB, PS4_1_S2_PB=SFT_PS4_1_S2_PB, UL10_1_SS1_SPB=SFT_UL10_1_SS1_SPB, UL10_3_S1_PB=SFT_UL10_3_S1_PB, UL10_3_S2_PB=SFT_UL10_3_S2_PB, UL11_3_S1_PB=SFT_UL11_3_S1_PB, UL11_3_S2_PB=SFT_UL11_3_S2_PB, UL11_4_S1_PB=SFT_UL11_4_S1_PB, UL11_4_S2_PB=SFT_UL11_4_S2_PB, UL11_9_S1_PB=SFT_UL11_9_S1_PB, UL11_9_S2_PB=SFT_UL11_9_S2_PB, UL12_3_S1_PB=SFT_UL12_3_S1_PB, UL12_3_S2_PB=SFT_UL12_3_S2_PB, UL12_4_S1_PB=SFT_UL12_4_S1_PB, UL12_4_S2_PB=SFT_UL12_4_S2_PB, UL1_3_S1_PB=SFT_UL1_3_S1_PB, UL1_3_S2_PB=SFT_UL1_3_S2_PB, UL1_4_S1_PB=SFT_UL1_4_S1_PB, UL1_4_S2_PB=SFT_UL1_4_S2_PB, UL1_9_S1_PB=SFT_UL1_9_S1_PB, UL1_9_S2_PB=SFT_UL1_9_S2_PB, UL2_3_S1_PB=SFT_UL2_3_S1_PB, UL2_3_S2_PB=SFT_UL2_3_S2_PB, UL2_4_S1_PB=SFT_UL2_4_S1_PB, UL2_4_S2_PB=SFT_UL2_4_S2_PB, UL3_1_SS1_SPB=SFT_UL3_1_SS1_SPB, UL3_2_S1_PB=SFT_UL3_2_S1_PB, UL3_2_S2_PB=SFT_UL3_2_S2_PB, UL4_3_S1_PB=SFT_UL4_3_S1_PB, UL4_3_S2_PB=SFT_UL4_3_S2_PB, UL4_4_S1_PB=SFT_UL4_4_S1_PB, UL4_4_S2_PB=SFT_UL4_4_S2_PB, UL4_9_S1_PB=SFT_UL4_9_S1_PB, UL4_9_S2_PB=SFT_UL4_9_S2_PB, UL5_3_S1_PB=SFT_UL5_3_S1_PB, UL5_3_S2_PB=SFT_UL5_3_S2_PB, UL5_4_S1_PB=SFT_UL5_4_S1_PB, UL5_4_S2_PB=SFT_UL5_4_S2_PB, UL6_1_SS1_SPB=SFT_UL6_1_SS1_SPB, UL6_2_S1_PB=SFT_UL6_2_S1_PB, UL6_2_S2_PB=SFT_UL6_2_S2_PB, UL7_3_S1_PB=SFT_UL7_3_S1_PB, UL7_3_S2_PB=SFT_UL7_3_S2_PB, UL7_4_S1_PB=SFT_UL7_4_S1_PB, UL7_4_S2_PB=SFT_UL7_4_S2_PB, UL7_9_S1_PB=SFT_UL7_9_S1_PB, UL7_9_S2_PB=SFT_UL7_9_S2_PB, UL8_1_SS1_SPB=SFT_UL8_1_SS1_SPB, UL8_3_S1_PB=SFT_UL8_3_S1_PB, UL8_3_S2_PB=SFT_UL8_3_S2_PB, UL9_3_S1_PB=SFT_UL9_3_S1_PB, UL9_3_S2_PB=SFT_UL9_3_S2_PB, UL9_4_S1_PB=SFT_UL9_4_S1_PB, UL9_4_S2_PB=SFT_UL9_4_S2_PB
MCM_S_PB=SFT_MCM_S_PB, PS1_1_S1_PB=SFT_PS1_1_S1_PB, PS1_1_S2_PB=SFT_PS1_1_S2_PB, PS1_5_S1_PB=SFT_PS1_5_S1_PB, PS1_5_S2_PB=SFT_PS1_5_S2_PB, PS2_1_S1_PB=SFT_PS2_1_S1_PB, PS2_1_S2_PB=SFT_PS2_1_S2_PB, PS2_6_S1_PB=SFT_PS2_6_S1_PB, PS2_6_S2_PB=SFT_PS2_6_S2_PB, PS3_12_S1_PB=SFT_PS3_12_S1_PB, PS3_12_S2_PB=SFT_PS3_12_S2_PB, PS3_1_S1_PB=SFT_PS3_1_S1_PB, PS3_1_S2_PB=SFT_PS3_1_S2_PB, PS4_14_S1_PB=SFT_PS4_14_S1_PB, PS4_14_S2_PB=SFT_PS4_14_S2_PB, PS4_1_S1_PB=SFT_PS4_1_S1_PB, PS4_1_S2_PB=SFT_PS4_1_S2_PB, UL10_1_SS1_SPB=SFT_UL10_1_SS1_SPB, UL10_3_S1_PB=SFT_UL10_3_S1_PB, UL10_3_S2_PB=SFT_UL10_3_S2_PB, UL11_3_S1_PB=SFT_UL11_3_S1_PB, UL11_3_S2_PB=SFT_UL11_3_S2_PB, UL11_4_S1_PB=SFT_UL11_4_S1_PB, UL11_4_S2_PB=SFT_UL11_4_S2_PB, UL11_9_S1_PB=SFT_UL11_9_S1_PB, UL11_9_S2_PB=SFT_UL11_9_S2_PB, UL12_3_S1_PB=SFT_UL12_3_S1_PB, UL12_3_S2_PB=SFT_UL12_3_S2_PB, UL12_4_S1_PB=SFT_UL12_4_S1_PB, UL12_4_S2_PB=SFT_UL12_4_S2_PB, UL1_3_S1_PB=SFT_UL1_3_S1_PB, UL1_3_S2_PB=SFT_UL1_3_S2_PB, UL1_4_S1_PB=SFT_UL1_4_S1_PB, UL1_4_S2_PB=SFT_UL1_4_S2_PB, UL1_9_S1_PB=SFT_UL1_9_S1_PB, UL1_9_S2_PB=SFT_UL1_9_S2_PB, UL2_3_S1_PB=SFT_UL2_3_S1_PB, UL2_3_S2_PB=SFT_UL2_3_S2_PB, UL2_4_S1_PB=SFT_UL2_4_S1_PB, UL2_4_S2_PB=SFT_UL2_4_S2_PB, UL3_1_SS1_SPB=SFT_UL3_1_SS1_SPB, UL3_2_S1_PB=SFT_UL3_2_S1_PB, UL3_2_S2_PB=SFT_UL3_2_S2_PB, UL4_3_S1_PB=SFT_UL4_3_S1_PB, UL4_3_S2_PB=SFT_UL4_3_S2_PB, UL4_4_S1_PB=SFT_UL4_4_S1_PB, UL4_4_S2_PB=SFT_UL4_4_S2_PB, UL4_9_S1_PB=SFT_UL4_9_S1_PB, UL4_9_S2_PB=SFT_UL4_9_S2_PB, UL5_3_S1_PB=SFT_UL5_3_S1_PB, UL5_3_S2_PB=SFT_UL5_3_S2_PB, UL5_4_S1_PB=SFT_UL5_4_S1_PB, UL5_4_S2_PB=SFT_UL5_4_S2_PB, UL6_1_SS1_SPB=SFT_UL6_1_SS1_SPB, UL6_2_S1_PB=SFT_UL6_2_S1_PB, UL6_2_S2_PB=SFT_UL6_2_S2_PB, UL7_3_S1_PB=SFT_UL7_3_S1_PB, UL7_3_S2_PB=SFT_UL7_3_S2_PB, UL7_4_S1_PB=SFT_UL7_4_S1_PB, UL7_4_S2_PB=SFT_UL7_4_S2_PB, UL7_9_S1_PB=SFT_UL7_9_S1_PB, UL7_9_S2_PB=SFT_UL7_9_S2_PB, UL8_1_SS1_SPB=SFT_UL8_1_SS1_SPB, UL8_3_S1_PB=SFT_UL8_3_S1_PB, UL8_3_S2_PB=SFT_UL8_3_S2_PB, UL9_3_S1_PB=SFT_UL9_3_S1_PB, UL9_3_S2_PB=SFT_UL9_3_S2_PB, UL9_4_S1_PB=SFT_UL9_4_S1_PB, UL9_4_S2_PB=SFT_UL9_4_S2_PB

View File

@ -1 +0,0 @@
MCM01_S_PB=SFT_MCM01_S_PB

View File

@ -0,0 +1,105 @@
<Tag Name="PDP1_CB_MONITOR" Class="Standard" TagType="Base" DataType="UDT_AOI_CB_MONITOR" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1],[[0,0,0,0]],[[0],[0,0,0,0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_CB_MONITOR">
<StructureMember Name="AOI" DataType="AOI_CB_MONITOR">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="IO_Block_Comm_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB1_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB2_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB3_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB4_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB5_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB6_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB7_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB8_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB9_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB10_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB11_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB12_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB13_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB14_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB15_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB16_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB17_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB18_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB19_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB20_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB21_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB22_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB23_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB24_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB25_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB26_I" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_CB_MONITOR">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_CB_MONITOR">
<DataValueMember Name="Power_Branch_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB1_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB2_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB3_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB4_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB5_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB6_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB7_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB8_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB9_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB10_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB11_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB12_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB13_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB14_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB15_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB16_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB17_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB18_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB19_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB20_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB21_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB22_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB23_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB24_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB25_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB26_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_CB_MONITOR">
<StructureMember Name="CMD" DataType="UDT_CTRL_CB_MONITOR_CMD">
<DataValueMember Name="Power_Branch_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_CB_MONITOR_STS">
<DataValueMember Name="Power_Branch_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB1_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB2_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB3_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB4_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB5_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB6_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB7_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB8_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB9_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB10_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB11_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB12_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB13_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB14_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB15_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB16_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB17_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB18_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB19_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB20_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB21_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB22_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB23_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB24_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB25_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="CB26_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,54 @@
<Tag Name="S011003_D2C_CHUTE" Class="Standard" TagType="Base" DataType="UDT_AOI_D2C_CHUTE" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[4194305,0,0,0,0,[1,0,0,[0,0,0],[0,0,0]],[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0
,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[0,0,0,0,0,0,0,0]],0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],1,[1,0,0,0,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,3000,0],[0,5000,0],[0,0]],[0,0,0],[0,0,0]],[0,[0],0],[[0],[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_D2C_CHUTE">
<StructureMember Name="AOI" DataType="AOI_D2C_CHUTE">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="GS_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="GS_PB_Light_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="G_Beacon_Segment_Color_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="G_Beacon_Segment_Animation_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="B_Beacon_Segment_Color_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="B_Beacon_Segment_Animation_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Busy" DataType="BOOL" Value="0"/>
<DataValueMember Name="Done" DataType="BOOL" Value="0"/>
<DataValueMember Name="Bin_Error" DataType="BOOL" Value="0"/>
<DataValueMember Name="Cyclic_Error" DataType="BOOL" Value="0"/>
<DataValueMember Name="Comms_Error" DataType="BOOL" Value="0"/>
<DataValueMember Name="Cart_Present" DataType="BOOL" Value="0"/>
<DataValueMember Name="Short_Shuttle_Present" DataType="BOOL" Value="0"/>
<DataValueMember Name="Tall_Shuttle_Present" DataType="BOOL" Value="0"/>
<DataValueMember Name="Ready" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_D2C_CHUTE">
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<StructureMember Name="Alarms" DataType="UDT_ALARMS_D2C_CHUTE">
<DataValueMember Name="GS_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="No_Container" DataType="BOOL" Value="0"/>
<DataValueMember Name="Disabled" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="GS_PB" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_D2C_CHUTE">
<StructureMember Name="CMD" DataType="UDT_CTRL_D2C_CHUTE_CMD">
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_D2C_CHUTE_STS">
<DataValueMember Name="Enabled" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Ready" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,60 @@
<Tag Name="UL11_1_EX1" Class="Standard" TagType="Base" DataType="UDT_AOI_EXTENDO" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],0,0,[10,' LOST COMM$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[6,' FULL$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],[0]],[[0],0,0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_EXTENDO">
<StructureMember Name="AOI" DataType="AOI_EXTENDO">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="DPM_Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Extendo_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Extendo_Faulted_Reset_Required" DataType="BOOL" Value="0"/>
<DataValueMember Name="Estopped" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Saving_Mode" DataType="BOOL" Value="0"/>
<DataValueMember Name="No_Interlock" DataType="BOOL" Value="0"/>
<DataValueMember Name="MCM_Not_Started" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_EXTENDO">
<StructureMember Name="CMD" DataType="UDT_CTRL_EXTENDO_CMD">
<DataValueMember Name="Interlock" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_EXTENDO_STS">
<DataValueMember Name="Interlock" DataType="BOOL" Value="0"/>
<DataValueMember Name="Allow_Loading" DataType="BOOL" Value="0"/>
<DataValueMember Name="Running" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_EXTENDO">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_EXTENDO">
<DataValueMember Name="Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Extendo_Faulted" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Loading_Mode" DataType="BOOL" Value="0"/>
<DataValueMember Name="Unloading_Mode" DataType="BOOL" Value="0"/>
<DataValueMember Name="Aopen_Active" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Saving_Mode" DataType="BOOL" Value="0"/>
<DataValueMember Name="Fully_Retracted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Fully_Extended" DataType="BOOL" Value="0"/>
<DataValueMember Name="PEC_Override_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Extendo_Faults_Reset_PB" DataType="BOOL" Value="0"/>
<StructureMember Name="Name" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,45 @@
<Tag Name="FL1018_3CH_PE1" Class="Standard" TagType="Base" DataType="UDT_AOI_FPE" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,[0,0,0],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[5,' FULL$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],0,[14,'FL1018_3CH_PE1$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_FPE">
<StructureMember Name="AOI" DataType="AOI_FPE">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Parent_Comm_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Clear_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Blocked" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_PE">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_PE">
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="TimeOut" DataType="DINT" Radix="Decimal" Value="0"/>
<StructureMember Name="Name" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="14"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_PE">
<StructureMember Name="CMD" DataType="UDT_CTRL_PE_CMD">
<DataValueMember Name="Dynamic_Jam" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_PE_STS">
<DataValueMember Name="Blocked" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Chute_Roller_PE" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,47 @@
<Tag Name="FL1014_2_PE1" Class="Standard" TagType="Base" DataType="UDT_AOI_JPE" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,[0,0,0],[1,0,0,[0,0,0],[0,0,0]],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[4,' Jam$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[0,0,0],[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],0,[12,'FL1014_2_PE1$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_JPE">
<StructureMember Name="AOI" DataType="AOI_JPE">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Parent_Comm_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Clear_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Blocked" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Run_Up_PE" DataType="BOOL" Value="0"/>
<DataValueMember Name="Run_Up" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_PE">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_PE">
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="TimeOut" DataType="DINT" Radix="Decimal" Value="0"/>
<StructureMember Name="Name" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="12"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA['FL1014_2_PE1']]>
</DataValueMember>
</StructureMember>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_PE">
<StructureMember Name="CMD" DataType="UDT_CTRL_PE_CMD">
<DataValueMember Name="Dynamic_Jam" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_PE_STS">
<DataValueMember Name="Blocked" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Chute_Roller_PE" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,142 @@
<Tag Name="MCM" Class="Standard" TagType="Base" DataType="UDT_AOI_MCM" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,0,[1,0,0,[0,0,0],[0,0,0]],[1,0,0,[0,0,0],[0,0,0]],[1,0,0,[0,0,0],[0,0,0]],[1,0,0,[0,0,0],[0,0,0]],[1,0,0,[0,0,0],[0,0,0]]],[[0,0,0],0,0,0,0,0,0,0,0],[[[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],0,0,0],[[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],0,0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_MCM">
<StructureMember Name="AOI" DataType="AOI_MCM">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Restart_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_PB_I_CH1" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_PB_I_CH2" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stop_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Fire_Relay_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="On_UPS_Battery_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Low_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Fault_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="NAT_Switch_Fault_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Actuated_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Restart_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Was_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Was_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Fault_Was_Present" DataType="BOOL" Value="0"/>
<DataValueMember Name="Encoder_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Was_Actuated" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStopped_Locally" DataType="BOOL" Value="0"/>
<DataValueMember Name="System_Started" DataType="BOOL" Value="0"/>
<DataValueMember Name="Fire_Relay_De_Energized" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="On_UPS_Battery" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Low" DataType="BOOL" Value="0"/>
<DataValueMember Name="NAT_Switch_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_MCM">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_MCM">
<DataValueMember Name="EStop_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_On" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Low" DataType="BOOL" Value="0"/>
<DataValueMember Name="Fire_Relay_De_Energized" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Encoder_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStopped" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stop_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stopped" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Restart_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB_Pressed" DataType="BOOL" Value="0"/>
<DataValueMember Name="UPS_Battery_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="On_UPS_Battery" DataType="BOOL" Value="0"/>
<DataValueMember Name="NAT_Switch_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStopped_Locally" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Restart_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stop_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Actuated_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Restart_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Local_Estop_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Setup_Motor_Speeds" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Fault_Reset_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Reset_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Start_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Jam_Restart_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_MCM">
<StructureMember Name="CMD" DataType="UDT_CTRL_MCM_CMD">
<StructureMember Name="Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
<DataValueMember Name="Log_Quantity" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Acknowledge_Log" DataType="BOOL" Value="0"/>
<DataValueMember Name="Unacknowledge_All_Logs" DataType="BOOL" Value="0"/>
<DataValueMember Name="VFD_Was_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="VFD_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Was_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Fault_Was_Present" DataType="BOOL" Value="0"/>
<DataValueMember Name="Encoder_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Actuated" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Was_Actuated" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_MCM_STS">
<StructureMember Name="Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
<DataValueMember Name="Log_Quantity" DataType="DINT" Radix="Decimal" Value="0"/>
<StructureMember Name="Acknowledge_Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
<DataValueMember Name="Unacknowledge_All_Logs" DataType="BOOL" Value="0"/>
<DataValueMember Name="Motor_Was_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="VFD_Fault_Reset_Requested" DataType="BOOL" Value="0"/>
<DataValueMember Name="Power_Branch_Fault_Reset_Requested" DataType="BOOL" Value="0"/>
<DataValueMember Name="Low_Air_Pressure_Fault_Reset_Requested" DataType="BOOL" Value="0"/>
<DataValueMember Name="System_Started" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Reset_Requested" DataType="BOOL" Value="0"/>
<DataValueMember Name="Communication_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Setup_Motor_Speeds" DataType="BOOL" Value="0"/>
<DataValueMember Name="System_Start_Requested" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,62 @@
<Tag Name="S011004_PB_Chute" Class="Standard" TagType="Base" DataType="UDT_AOI_PB_CHUTE" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,0,0,0,0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],[0,0,0],[0,0,0],[1,0,0,[0,0,0],[0,0,0]]],[[0],0,0,0,0,0],[[0],[0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_PB_CHUTE">
<StructureMember Name="AOI" DataType="AOI_PB_CHUTE">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="IO_Block_Communication_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="PR_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Disabled" DataType="BOOL" Value="0"/>
<DataValueMember Name="Solenoid_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="G_Beacon_Segment_Color" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="G_Beacon_Segment_Animation" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="B_Beacon_Segment_Color" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="B_Beacon_Segment_Animation" DataType="DINT" Radix="Decimal" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_PB_CHUTE">
<StructureMember Name="Alarms" DataType="UDT_ALARMS_PB_CHUTE">
<DataValueMember Name="Disabled" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam" DataType="BOOL" Value="0"/>
<DataValueMember Name="PR_PB_Pressed" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Enable_Chute_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="TimeOut" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="PR_PB_STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Chute_Type" DataType="DINT" Radix="Decimal" Value="0"/>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_PB_CHUTE">
<StructureMember Name="CMD" DataType="UDT_CTRL_PB_CHUTE_CMD">
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_PB_CHUTE_STS">
<DataValueMember Name="Enabled" DataType="BOOL" Value="0"/>
<DataValueMember Name="Half_Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full" DataType="BOOL" Value="0"/>
<DataValueMember Name="Full_PE_Blocked" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Ready" DataType="BOOL" Value="0"/>
<StructureMember Name="Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,26 @@
<Tag Name="PDP1_PMM1" Class="Standard" TagType="Base" DataType="UDT_AOI_PMM" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,[0,0.00000000e+000,0.00000000e+000,0.00000000e+000,[0]]],[0,0.00000000e+000,0.00000000e+000,0.00000000e+000
,[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_PMM">
<StructureMember Name="AOI" DataType="AOI_PMM">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Parent_Comm_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="PMM_Fault_I" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_PMM">
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="PMM_Total_Power" DataType="REAL" Radix="Float" Value="0.0"/>
<DataValueMember Name="PMM_KWH_Consumed" DataType="REAL" Radix="Float" Value="0.0"/>
<DataValueMember Name="PMM_Max_Total_Power" DataType="REAL" Radix="Float" Value="0.0"/>
<StructureMember Name="Alarm" DataType="UDT_ALARMS_PMM">
<DataValueMember Name="PMM_Communication_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="PMM_Fault" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,25 @@
<Tag Name="Rack" Class="Standard" TagType="Base" DataType="UDT_AOI_RACK" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,0,0,0,0,0],[[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_RACK">
<StructureMember Name="AOI" DataType="AOI_RACK">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot2_EN4TR_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot5_IB16_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot6_OB16E_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot7_IB16S_Faulted" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_RACK">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_RACK">
<DataValueMember Name="Slot5_IB16_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot6_OB16E_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot7_IB16S_Faulted" DataType="BOOL" Value="0"/>
<DataValueMember Name="Slot2_EN4TR_Faulted" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,35 @@
<Tag Name="S011003_JR" Class="Standard" TagType="Base" DataType="UDT_AOI_STATION_JR_CHUTE" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,0,0,[1,0,0,[0,0,0],[0,0,0]]],[[0],0,0],[[0],[0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_STATION_JR_CHUTE">
<StructureMember Name="AOI" DataType="AOI_STATION_JR_CHUTE">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="JR_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="JR_PB_LT_O" DataType="BOOL" Value="0"/>
<DataValueMember Name="A_Beacon_Segment_Color_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="A_Beacon_Segment_Animation_O" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_STATION_JR_CHUTE">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_STATION_JR_CHUTE">
<DataValueMember Name="Pressed" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="JR_PB" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_STATION_JR_CHUTE">
<StructureMember Name="CMD" DataType="UDT_CTRL_STATION_JR_CHUTE_CMD">
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="Sorter_JPE_Blocked" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_STATION_JR_CHUTE_STS">
<DataValueMember Name="Jam_Reset_Requested" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -0,0 +1,62 @@
<Tag Name="FL1014_2_JR2_PB" Class="Standard" TagType="Base" DataType="UDT_AOI_STATION_JR_PB" Constant="false" ExternalAccess="Read/Write" OpcUaAccess="None">
<Data Format="L5K">
<![CDATA[[[1,0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0],0,0,[15,'FL1014_2_JR2_PB$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[[0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
]],[0,[0,'$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00$00'
],0]]]]]>
</Data>
<Data Format="Decorated">
<Structure DataType="UDT_AOI_STATION_JR_PB">
<StructureMember Name="AOI" DataType="AOI_STATION_JR_PB">
<DataValueMember Name="EnableIn" DataType="BOOL" Value="1"/>
<DataValueMember Name="EnableOut" DataType="BOOL" Value="0"/>
<DataValueMember Name="JR_PB_I" DataType="BOOL" Value="0"/>
<DataValueMember Name="JR_PB_LT_O" DataType="BOOL" Value="0"/>
</StructureMember>
<StructureMember Name="HMI" DataType="UDT_HMI_STATION">
<StructureMember Name="Alarm" DataType="UDT_ALARMS_STATION">
<DataValueMember Name="EStopped" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stopped" DataType="BOOL" Value="0"/>
<DataValueMember Name="Pressed" DataType="BOOL" Value="0"/>
</StructureMember>
<DataValueMember Name="STATE" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="JR_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Start_PB" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stop_PB" DataType="BOOL" Value="0"/>
<StructureMember Name="Name" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="15"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
</StructureMember>
<StructureMember Name="CTRL" DataType="UDT_CTRL_STATION">
<StructureMember Name="CMD" DataType="UDT_CTRL_STATION_CMD">
<DataValueMember Name="Jammed" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Actuated" DataType="BOOL" Value="0"/>
<StructureMember Name="Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
</StructureMember>
<StructureMember Name="STS" DataType="UDT_CTRL_STATION_STS">
<DataValueMember Name="Start" DataType="BOOL" Value="0"/>
<DataValueMember Name="Stop" DataType="BOOL" Value="0"/>
<DataValueMember Name="EStop_Was_Actuated" DataType="BOOL" Value="0"/>
<StructureMember Name="Log" DataType="STRING">
<DataValueMember Name="LEN" DataType="DINT" Radix="Decimal" Value="0"/>
<DataValueMember Name="DATA" DataType="STRING" Radix="ASCII">
<![CDATA[]]>
</DataValueMember>
</StructureMember>
<DataValueMember Name="JR_PB_LT" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Fault" DataType="BOOL" Value="0"/>
<DataValueMember Name="Jam_Reset" DataType="BOOL" Value="0"/>
</StructureMember>
</StructureMember>
</Structure>
</Data>
</Tag>

View File

@ -14,6 +14,10 @@ import sys
import argparse
import subprocess
from pathlib import Path
import io
import contextlib
import xml.etree.ElementTree as ET
import re
def get_project_paths():
"""Get standardized paths for all project components."""
@ -27,7 +31,7 @@ def get_project_paths():
'io_tree_generator': project_root / "IO Tree Configuration Generator"
}
def run_plc_data_generator(raw_excel_file: Path, paths: dict) -> bool:
def run_plc_data_generator(raw_excel_file: Path, paths: dict, verbose: bool = False) -> 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"
@ -40,10 +44,6 @@ def run_plc_data_generator(raw_excel_file: Path, paths: dict) -> bool:
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([
@ -63,11 +63,10 @@ def run_plc_data_generator(raw_excel_file: Path, paths: dict) -> bool:
# 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)
if verbose and result.returncode != 0:
print("Warning: Permission error at end of processing, core processing completed")
if verbose:
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"
@ -75,71 +74,72 @@ def run_plc_data_generator(raw_excel_file: Path, paths: dict) -> bool:
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)
if verbose:
print("Error: Data processing failed")
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
return False
except Exception as e:
print(f"ERROR: Error running PLC Data Generator: {e}")
if verbose:
print(f"Error: Exception in data processing: {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 ===")
def run_routines_generator(paths: dict, project_name: str = None, ignore_estop1ok: bool = False, safety_only: bool = False, verbose: bool = False) -> bool:
"""Run the Routines Generator.
When safety_only is True, runs safety-only generation (inputs, outputs, resets,
estops, zones, estop_check, safety tag map). Otherwise runs the standard
generator with DPM and other routines.
"""
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
# Build command arguments to use unified, config-driven CLI
config_path = paths['project_root'] / 'generator_config.json'
excel_path = routines_dir / 'DESC_IP_MERGED.xlsx'
subcmd = 'safety' if safety_only else 'all'
# Build args with global flags BEFORE the subcommand
cmd_args = [
sys.executable,
str(main_dpm_script),
"--desc-ip-mode" # Use DESC_IP data extraction mode
sys.executable,
'-m', 'src.unified_cli',
'--config', str(config_path),
'--excel-file', str(excel_path),
]
# 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")
if verbose:
cmd_args.extend(['--log-level', 'DEBUG'])
cmd_args.append(subcmd)
# Note: routine inclusion/exclusion is driven by config; project_name and ignore-estop1ok are configured in JSON
# Run the main program generator script with DPM routines
# Run the unified CLI
result = subprocess.run(cmd_args, cwd=routines_dir, capture_output=True, text=True)
print(result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
if verbose:
print(result.stdout)
if result.stderr:
print("[generator 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}")
if verbose:
print(f"Error: Exception in routine generation: {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 ===")
def run_io_tree_generator(paths: dict, project_name: str, safety_only: bool = False, verbose: bool = False) -> bool:
"""Run the IO Tree Configuration Generator.
If safety_only is True, skip this step to avoid generating non-safety routines.
"""
if safety_only:
return True
io_tree_dir = paths['io_tree_generator']
enhanced_mcm_script = io_tree_dir / "enhanced_mcm_generator.py"
@ -147,34 +147,10 @@ def run_io_tree_generator(paths: dict, project_name: str) -> bool:
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 fully removed: do not attempt to load or pass zones
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
@ -185,56 +161,70 @@ def run_io_tree_generator(paths: dict, project_name: str) -> bool:
project_name
]
# Add zones if available
if zones_json:
cmd_args.extend(["--zones", zones_json])
# Zones removed; no additional args
# 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 verbose:
print(result.stdout)
if result.stderr:
print("[io-tree 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}")
if verbose:
print(f"Error: Exception in IO tree generation: {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 ===")
def run_l5x_to_acd_compiler(paths: dict, project_name: str, safety_only: bool = False, verbose: bool = False) -> bool:
"""Prepare for L5X2ACD Compilation using dynamic compilation manager.
If safety_only is True, skip this step since a full project L5X wasn't generated.
"""
if safety_only:
return True
# 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]}")
if verbose:
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}")
if len(l5x_files) > 1 and verbose:
print(f"Warning: Multiple L5X files found, using first: {l5x_files[0].name}")
complete_l5x = l5x_files[0]
print(f"Found generated L5X file: {complete_l5x.name}")
if verbose:
print(f"Found generated L5X file: {complete_l5x.name}")
# Inject SafetyTagMap from SafetyTagMapping.txt before compilation (if available)
try:
mapping_file = paths['routines_generator'] / 'SafetyTagMapping.txt'
if mapping_file.exists():
if verbose:
print("Injecting SafetyTagMap from SafetyTagMapping.txt into L5X ...")
_inject_safety_tag_map_into_l5x(complete_l5x, mapping_file, verbose)
elif verbose:
print("SafetyTagMapping.txt not found; skipping SafetyTagMap injection")
except Exception as e:
if verbose:
print(f"Warning: Failed to inject SafetyTagMap: {e}")
# Use the dynamic compilation manager
l5x2acd_dir = paths['project_root'] / "L5X2ACD Compiler"
@ -260,40 +250,121 @@ def run_l5x_to_acd_compiler(paths: dict, project_name: str) -> bool:
project_type = "MCM04"
options['enable_feeder_optimization'] = True
print(f"Using dynamic compilation setup for project type: {project_type}")
if verbose:
print(f"- 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
)
if verbose:
result = manager.setup_compilation(
source_l5x=complete_l5x,
project_name=project_name or complete_l5x.stem,
compilation_options=options,
wipe_existing=True
)
else:
_buf = io.StringIO()
with contextlib.redirect_stdout(_buf), contextlib.redirect_stderr(_buf):
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)
if verbose:
print("OK: Compilation setup completed")
l5x2acd_windows_path = str(l5x2acd_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\')
l5x_windows_path = str(result['l5x_file']).replace('/mnt/c/', 'C:\\').replace('/', '\\')
print("To compile on Windows:")
print(f"- cd \"{l5x2acd_windows_path}\"")
print(f"- python l5x_to_acd.py \"{l5x_windows_path}\"")
return True
except Exception as e:
print(f"ERROR: Failed to setup dynamic compilation: {e}")
import traceback
traceback.print_exc()
if verbose:
print(f"Error: Exception in compilation setup: {e}")
import traceback
traceback.print_exc()
return False
def _inject_safety_tag_map_into_l5x(l5x_path: Path, mapping_file: Path, verbose: bool = False) -> None:
"""Inject or replace <SafetyTagMap> inside the existing Controller/SafetyInfo using text edits.
- Preserves the original XML header exactly
- Does not create additional SafetyInfo blocks
- Formats SafetyTagMap on its own line between SafetyInfo open/close tags
"""
mapping_text = mapping_file.read_text(encoding='utf-8').strip()
if not mapping_text:
if verbose:
print("SafetyTagMapping.txt is empty; skipping injection")
return
xml_text = l5x_path.read_text(encoding='utf-8')
# Find Controller block
ctrl_match = re.search(r"<Controller\b[\s\S]*?</Controller>", xml_text)
if not ctrl_match:
if verbose:
print("No <Controller> found; skipping injection")
return
ctrl_start, ctrl_end = ctrl_match.span()
ctrl_text = xml_text[ctrl_start:ctrl_end]
# Locate first SafetyInfo (body or self-closing)
m_body = re.search(r"<SafetyInfo\b([^>]*)>([\s\S]*?)</SafetyInfo>", ctrl_text)
m_self = re.search(r"<SafetyInfo\b([^>]*)/>", ctrl_text)
if not m_body and not m_self:
if verbose:
print("No <SafetyInfo> under <Controller>; skipping injection")
return
# Determine indentation based on the SafetyInfo line
first_match = m_body if (m_body and (not m_self or m_body.start() < m_self.start())) else m_self
safety_start = first_match.start()
line_start = ctrl_text.rfind('\n', 0, safety_start)
indent = ctrl_text[line_start+1:safety_start] if line_start != -1 else ''
map_line = f"\n{indent} <SafetyTagMap> {mapping_text} </SafetyTagMap>\n"
def dedup_safety_infos(text: str) -> str:
seen = False
def repl(match: re.Match) -> str:
nonlocal seen
if seen:
return ''
seen = True
return match.group(0)
pat = re.compile(r"(<SafetyInfo\b[^>]*/>)|(\n?\s*<SafetyInfo\b[^>]*>[\s\S]*?</SafetyInfo>)")
return pat.sub(repl, text)
if m_body and (not m_self or m_body.start() < m_self.start()):
# Replace or insert SafetyTagMap inside existing body
attrs = m_body.group(1)
inner = m_body.group(2)
# Replace existing map if present
if re.search(r"<SafetyTagMap>[\s\S]*?</SafetyTagMap>", inner):
new_inner = re.sub(r"<SafetyTagMap>[\s\S]*?</SafetyTagMap>", map_line.strip('\n'), inner, count=1)
# Remove any additional maps
new_inner = re.sub(r"<SafetyTagMap>[\s\S]*?</SafetyTagMap>", '', new_inner)
else:
new_inner = map_line + inner
new_block = f"<SafetyInfo{attrs}>{new_inner}</SafetyInfo>"
new_ctrl_text = ctrl_text[:m_body.start()] + new_block + ctrl_text[m_body.end():]
else:
# Convert self-closing to body with map
attrs = m_self.group(1)
new_block = f"<SafetyInfo{attrs}>{map_line}</SafetyInfo>"
new_ctrl_text = ctrl_text[:m_self.start()] + new_block + ctrl_text[m_self.end():]
new_ctrl_text = dedup_safety_infos(new_ctrl_text)
new_xml = xml_text[:ctrl_start] + new_ctrl_text + xml_text[ctrl_end:]
l5x_path.write_text(new_xml, encoding='utf-8')
if verbose:
print("SafetyTagMap injection OK")
def main() -> None:
"""Main entry point for complete workflow."""
@ -301,44 +372,61 @@ def main() -> None:
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')
parser.add_argument('--safety-only', action='store_true', help='Generate only safety routines and safety checks')
parser.add_argument('--verbose', action='store_true', help='Print detailed logs for each step')
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()
print("PLC Generation Workflow")
# 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)")
print("Step 1: Data processing ...", end=" ")
ok = run_plc_data_generator(args.excel_file, paths, verbose=args.verbose)
print("OK" if ok else "FAIL")
if not ok:
if not args.verbose:
print("(details suppressed; re-run with --verbose)")
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)")
print("Step 2: Routine generation ...", end=" ")
ok = run_routines_generator(paths, args.project_name, args.ignore_estop1ok, args.safety_only, verbose=args.verbose)
print("OK" if ok else "FAIL")
if not ok:
if not args.verbose:
print("(details suppressed; re-run with --verbose)")
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)
if args.safety_only:
print("Step 3: IO tree generation ... SKIPPED")
else:
print("Step 3: IO tree generation ...", end=" ")
ok = run_io_tree_generator(paths, args.project_name, args.safety_only, verbose=args.verbose)
print("OK" if ok else "FAIL")
if not ok:
if not args.verbose:
print("(details suppressed; re-run with --verbose)")
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)
if args.safety_only:
print("Step 4: Prepare compilation ... SKIPPED")
else:
print("Step 4: Prepare compilation ...", end=" ")
ok = run_l5x_to_acd_compiler(paths, args.project_name, args.safety_only, verbose=args.verbose)
print("OK" if ok else "FAIL")
if not ok:
if not args.verbose:
print("(details suppressed; re-run with --verbose)")
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)
print("Workflow complete")
if args.verbose and not args.safety_only and args.project_name:
print(f"L5X: IO Tree Configuration Generator/generated_projects/{args.project_name}.L5X")
if __name__ == '__main__':
main()

View File

@ -1,170 +1,8 @@
#!/usr/bin/env python3
"""generate_all.py build SafetyProgram L5X, MainProgram L5X, full tag CSV, and safety-tag mapping.
This is the updated version using the new configuration and logging systems.
Run from repository root:
python generate_all.py [options]
"""
from __future__ import annotations
from pathlib import Path
import sys
import argparse
# If repo root is current dir, add nested "src" folder to sys.path
pkg_dir = Path(__file__).resolve().parent / "src"
if pkg_dir.is_dir():
sys.path.insert(0, str(pkg_dir))
# Import new configuration and logging systems
from src.config import GeneratorConfig, get_config, set_config
from src.logging_config import setup_logging, get_logger, FileOperationContext
# Import existing generators and writers (these will be updated in later phases)
from src.generators.main_program import LimitedMainProgramGenerator as MainProgramGenerator
from src.generators.safety_program import LimitedSafetyProgramGenerator as SafetyProgramGenerator
from src.data_loader import DataLoader
from src.writers.csv_writer import create_limited_csv_with_tags
from src.writers.mapping_writer import create_safety_tag_mapping
def main() -> None:
parser = argparse.ArgumentParser(description="Generate safety-focused PLC program")
parser.add_argument(
'--config',
type=Path,
default=Path(__file__).parent.parent / 'generator_config.json',
help='Configuration file path'
)
parser.add_argument(
'--excel-file',
type=Path,
help='Override Excel file path from config'
)
parser.add_argument(
'--output-dir',
type=Path,
help='Override output directory from config'
)
parser.add_argument(
'--log-level',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO',
help='Set logging level'
)
parser.add_argument(
'--log-file',
type=Path,
help='Optional log file path'
)
parser.add_argument(
'--desc-ip-mode',
action='store_true',
help='Legacy flag - DESC_IP mode is now the default'
)
args = parser.parse_args()
# Setup logging first
setup_logging(
level=args.log_level,
log_file=args.log_file,
use_colors=True
)
logger = get_logger(__name__)
try:
logger.info("=== Starting PLC Generation Process ===")
# Load configuration
logger.debug("Loading configuration", config_file=str(args.config))
config = GeneratorConfig.from_file(args.config)
# Override config with command line arguments
if args.excel_file:
config.files.excel_file = args.excel_file
logger.debug("Overridden Excel file", file_path=str(args.excel_file))
if args.output_dir:
config.files.output_dir = args.output_dir
logger.debug("Overridden output directory", output_dir=str(args.output_dir))
# Set global config
set_config(config)
# Validate Excel file exists
if not config.files.excel_file.exists():
logger.error("Excel file not found", file_path=str(config.files.excel_file))
sys.exit(1)
logger.info("Configuration loaded",
excel_file=str(config.files.excel_file),
controller_name=config.xml.controller_name)
# Show deprecation warning for desc-ip-mode flag
if args.desc_ip_mode:
logger.warning("--desc-ip-mode flag is deprecated. DESC_IP extraction is now the default mode.")
# Safety program generation
with FileOperationContext(logger, "SafetyProgram generation", config.files.safety_l5x):
safety_gen = SafetyProgramGenerator(config.files.excel_file)
safety_gen.write(config.files.safety_l5x)
logger.info("SafetyProgram generation completed", output_file=config.files.safety_l5x)
# Main program generation
logger.info("Starting MainProgram generation", stage="main_generation")
with FileOperationContext(logger, "MainProgram generation", config.files.main_l5x):
main_gen = MainProgramGenerator(config.files.excel_file)
main_gen.write(config.files.main_l5x)
logger.info("MainProgram generation completed", output_file=config.files.main_l5x)
# Generate CSV tags
logger.progress("csv_generation", "Starting CSV tags generation")
if not config.files.original_csv.exists():
logger.warning("Original CSV file not found, skipping CSV generation",
file_path=str(config.files.original_csv))
else:
with FileOperationContext(logger, "CSV generation", config.files.complete_csv):
beacon_so_tags = create_limited_csv_with_tags(
config.files.excel_file,
config.files.original_csv,
config.files.complete_csv
)
logger.info("CSV generation completed",
output_file=config.files.complete_csv,
beacon_tags=len(beacon_so_tags) if beacon_so_tags else 0)
# Generate Safety Tag Mapping
logger.progress("mapping_generation", "Starting safety tag mapping generation")
with FileOperationContext(logger, "Safety mapping generation", config.files.mapping_txt):
# Load data for safety tag extraction
loader = DataLoader.from_excel(config.files.excel_file)
safety_tags = loader.safety_tags_from_pb
logger.data_stats("safety_tags", len(safety_tags))
create_safety_tag_mapping(
safety_tags,
set(), # No beacon SO tags in limited mode
set(), # No beacon SFT tags in limited mode
config.files.mapping_txt
)
logger.info("Safety tag mapping completed",
output_file=config.files.mapping_txt,
safety_tag_count=len(safety_tags))
logger.info("=== PLC Generation Process Completed Successfully ===")
logger.info("Generated files:",
safety_l5x=config.files.safety_l5x,
main_l5x=config.files.main_l5x,
csv_file=config.files.complete_csv,
mapping_file=config.files.mapping_txt)
except Exception as e:
logger.error("Generation process failed", exception=str(e))
if args.log_level == 'DEBUG':
logger.exception("Full traceback")
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
This script is deprecated. Use the unified CLI:
python -m src.unified_cli --config ../generator_config.json --excel-file DESC_IP_MERGED.xlsx all
"""
import sys
print("generate_all.py is deprecated. Please use the unified CLI (src.unified_cli).", file=sys.stderr)
sys.exit(1)

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
"""generate_fiom.py FIOM routine generation (DEPRECATED).
This script is no longer functional after the removal of legacy routines.
FIOM routine generation was part of the legacy system that has been removed
in favor of safety-focused generation.
For safety-only generation, use:
python generate_safety_only.py
"""
from __future__ import annotations
from pathlib import Path
import sys
print("ERROR: FIOM Generation No Longer Available")
print("=" * 50)
print("The FIOM routine generation feature has been removed as part of the")
print("legacy system cleanup. Only safety-focused routines are now supported.")
print()
print("Available alternatives:")
print(" • Safety-only generation: python generate_safety_only.py")
print(" • Full safety generation: python generate_all.py")
print()
print("These scripts generate:")
print(" • Safety routines (inputs, outputs, estops, zones, resets)")
print(" • Safety tag mappings")
print(" • Essential main program routines (safety_tag_map, estop_check)")
print()
print("If you need FIOM functionality, you would need to:")
print("1. Recreate the FIOM routine generator")
print("2. Add it to the src/routines/ directory")
print("3. Update the generators to include it")
sys.exit(1)

View File

@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""
Generate MainProgram with DPM routines from DESC_IP data.
This script creates a complete MainProgram.L5X file with:
- R000_SAFETY_TAG_MAP routine
- R020_DPM routine (with AOI_DPM calls)
- R100_ESTOP_CHECK routine
- Complete controller tags
Usage:
python generate_main_with_dpm.py --desc-ip-mode
python generate_main_with_dpm.py --desc-ip-mode --project-name MTN6_MCM05_CHUTE_LOAD
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
# If repo root is current dir, add nested "src" folder to sys.path
pkg_dir = Path(__file__).resolve().parent / "src"
if pkg_dir.is_dir():
sys.path.insert(0, str(pkg_dir))
from src.generators import FullMainProgramGenerator, LimitedSafetyProgramGenerator
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Generate MainProgram L5X with DPM routines from DESC_IP data"
)
parser.add_argument(
'--desc-ip-mode',
action='store_true',
help='Use DESC_IP extraction mode (default: true for this script)'
)
parser.add_argument(
'--project-name',
type=str,
help='Project name for output files (e.g., MTN6_MCM05_CHUTE_LOAD)'
)
parser.add_argument(
'--ignore-estop1ok',
action='store_true',
help='Ignore ESTOP1_OK validation errors'
)
return parser.parse_args()
def main():
"""Generate MainProgram and SafetyProgram L5X files with DPM routines."""
args = parse_args()
print("=== MainProgram Generator with DPM Routines ===")
print("Using DESC_IP extraction mode for device data")
# Use DESC_IP_MERGED.xlsx from current directory
excel_file = Path("DESC_IP_MERGED.xlsx")
if not excel_file.exists():
print(f"ERROR: {excel_file} not found in current directory")
print("Please ensure DESC_IP_MERGED.xlsx is available")
sys.exit(1)
print(f"Using Excel file: {excel_file}")
# Load zones configuration based on project type
zones_dict = None
try:
# Add parent directory to sys.path to find zones_config.py
sys.path.insert(0, str(Path(__file__).parent.parent))
from zones_config import ZONES_CONFIGS, DEFAULT_ZONES
# Detect project type from project name argument
import re
if args.project_name:
mcm_match = re.search(r"(MCM\d+)", args.project_name, re.IGNORECASE)
if mcm_match:
mcm_type = mcm_match.group(1).upper()
zones_dict = ZONES_CONFIGS.get(mcm_type, DEFAULT_ZONES)
print(f"Detected {mcm_type} project from '{args.project_name}', loaded {len(zones_dict)} zones from zones_config.py")
else:
zones_dict = DEFAULT_ZONES
print(f"No MCM type detected in '{args.project_name}', loaded {len(zones_dict)} default zones from zones_config.py")
else:
zones_dict = DEFAULT_ZONES
print(f"No project name provided, loaded {len(zones_dict)} default zones from zones_config.py")
except ImportError as e:
print(f"Warning: Could not load zones_config.py: {e}")
print("Proceeding without zones configuration...")
try:
# Generate MainProgram with DPM routines
print("\n--- Generating MainProgram L5X ---")
main_generator = FullMainProgramGenerator(excel_path=excel_file, zones_dict=zones_dict)
# Determine output filename
if args.project_name:
main_output = f"MainProgram_{args.project_name}.L5X"
safety_output = f"SafetyProgram_{args.project_name}.L5X"
else:
main_output = "MainProgram_Generated.L5X"
safety_output = "SafetyProgram_Generated.L5X"
main_generator.write(main_output)
print(f"SUCCESS: MainProgram written to {main_output}")
# Generate SafetyProgram
print("\n--- Generating SafetyProgram L5X ---")
safety_generator = LimitedSafetyProgramGenerator(excel_path=excel_file, zones_dict=zones_dict, ignore_estop1ok=args.ignore_estop1ok)
safety_generator.write(safety_output)
print(f"SUCCESS: SafetyProgram written to {safety_output}")
print("\n=== Generation Complete ===")
print(f"Generated files:")
print(f"{main_output}")
print(f"{safety_output}")
# Summary of generated content
print(f"\nMainProgram includes:")
print(f" • R000_SAFETY_TAG_MAP routine")
print(f" • R020_DPM routine with AOI_DPM calls")
print(f" • R100_ESTOP_CHECK routine")
print(f" • Complete controller tags")
except Exception as e:
print(f"ERROR: Generation failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,136 +0,0 @@
#!/usr/bin/env python3
"""generate_safety_only.py build limited SafetyProgram and MainProgram L5X with essential safety routines only.
This mode only generates:
- Safety routines: inputs, outputs, estops, zones, resets
- Main routines: safety_tag_map, estop_check (safety checkout)
- Controller tags embedded in MainProgram L5X
- Safety tag mapping
Extracts safety data (RST, STO, EPC) from DESC_IP sheet and uses zones_config.py for zone configuration.
Run from repository root:
python generate_safety_only.py [--desc-ip-mode]
The outputs are written to *_Limited.* filenames in the current directory.
"""
from __future__ import annotations
from pathlib import Path
import sys
import argparse
# If repo root is current dir, add nested "src" folder to sys.path
pkg_dir = Path(__file__).resolve().parent / "src"
if pkg_dir.is_dir():
sys.path.insert(0, str(pkg_dir))
from src.generators import LimitedSafetyProgramGenerator, LimitedMainProgramGenerator
from src.data_loader import DataLoader
from src.writers.mapping_writer import create_safety_tag_mapping
EXCEL_FILE = Path('MCM01_UL1_UL3.xlsx')
DESC_IP_FILE = Path('DESC_IP_MERGED.xlsx')
SAFETY_L5X = 'SafetyProgram_Limited.L5X'
MAIN_L5X = 'MainProgram_Limited.L5X'
MAPPING_TXT = 'SafetyTagMapping_Limited.txt'
def main() -> None:
parser = argparse.ArgumentParser(description="Generate safety-only routines")
parser.add_argument('--desc-ip-mode', action='store_true',
help='Extract safety devices from DESC_IP data instead of separate sheets')
parser.add_argument('--ignore-estop1ok', action='store_true',
help='Ignore ESTOP1OK tags in inputs, estops, and zones routines')
parser.add_argument('--project-name', type=str,
help='Project name to detect MCM type for zones configuration')
args = parser.parse_args()
print("Safety-Only Mode")
print("================")
# Determine which Excel file to use
excel_file = DESC_IP_FILE if args.desc_ip_mode and DESC_IP_FILE.exists() else EXCEL_FILE
if args.desc_ip_mode:
print("Using DESC_IP data extraction mode")
print(f"Using Excel file: {excel_file}")
print("Extracting safety devices from DESC_IP sheet using safety rules\n")
else:
print("Using separate safety sheets mode")
print(f"Using Excel file: {excel_file}")
print("Generating essential safety routines using DESC_IP data extraction and zones_config.py\n")
if args.ignore_estop1ok:
print("INFO: Ignoring ESTOP1OK tags in safety routines generation\n")
# Load zones configuration based on project type
zones_dict = None
try:
# Add parent directory to sys.path to find zones_config.py
sys.path.insert(0, str(Path(__file__).parent.parent))
from zones_config import ZONES_CONFIGS, DEFAULT_ZONES
# Detect project type from project name argument
import re
if args.project_name:
mcm_match = re.search(r"(MCM\d+)", args.project_name, re.IGNORECASE)
if mcm_match:
mcm_type = mcm_match.group(1).upper()
zones_dict = ZONES_CONFIGS.get(mcm_type, DEFAULT_ZONES)
print(f"Detected {mcm_type} project from '{args.project_name}', loaded {len(zones_dict)} zones from zones_config.py")
else:
zones_dict = DEFAULT_ZONES
print(f"No MCM type detected in '{args.project_name}', loaded {len(zones_dict)} default zones from zones_config.py")
else:
zones_dict = DEFAULT_ZONES
print(f"No project name provided, loaded {len(zones_dict)} default zones from zones_config.py")
except ImportError as e:
print(f"Warning: Could not load zones_config.py: {e}")
print("Proceeding without zones configuration...")
# Limited Safety program (inputs, outputs, estops, zones, resets)
print("1. Generating SafetyProgram...")
LimitedSafetyProgramGenerator(excel_file, zones_dict=zones_dict, ignore_estop1ok=args.ignore_estop1ok).write(SAFETY_L5X)
print(f'[SUCCESS] Wrote {SAFETY_L5X}')
print(' - Contains: inputs, outputs, estops, zones, resets routines')
# Limited Main program (safety_tag_map, estop_check) with embedded controller tags
print("\n2. Generating MainProgram with embedded controller tags...")
LimitedMainProgramGenerator(excel_file, zones_dict=zones_dict).write(MAIN_L5X)
print(f'[SUCCESS] Wrote {MAIN_L5X}')
print(' - Contains: safety_tag_map, estop_check routines')
print(' - Contains: Controller-level tags embedded in L5X')
# Safety tag mapping
print("\n3. Generating safety tag mapping...")
loader = DataLoader(excel_file, zones_dict=zones_dict)
# Collect safety tags from appropriate source
safety_tags = loader.safety_tags_from_pb
# For limited mode, we don't have beacon tags, so pass empty sets
create_safety_tag_mapping(safety_tags, set(), set(), MAPPING_TXT)
print(f'[SUCCESS] Wrote {MAPPING_TXT}')
print(f' - Contains: {len(safety_tags)} safety tags ({len(safety_tags) - 1} push buttons + MCM)')
print(f"\nSafety-only generation complete!")
if args.desc_ip_mode:
print(f"Data sources: DESC_IP sheet with safety device extraction")
print(f" - RST devices: DESCA contains 'START' but NOT 'LIGHT'")
print(f" - STO devices: TAGNAME contains 'VFD' or DESCA contains 'STO'")
print(f" - EPC devices: DESCA contains 'EPC' or 'ESTOP'")
else:
print(f"Data sources: DESC_IP extraction (RST, STO, EPC) + zones_config.py")
print(f"Main routines: safety_tag_map, estop_check")
print(f"Output files:")
print(f" - {SAFETY_L5X}")
print(f" - {MAIN_L5X} (with embedded controller tags)")
print(f" - {MAPPING_TXT}")
if __name__ == '__main__':
main()

View File

@ -68,7 +68,7 @@ def main() -> None:
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], default='INFO', help='Log level')
parser.add_argument('--log-file', type=Path, help='Log file path')
parser.add_argument('--project-name', help='Project name (for compatibility, not used)')
parser.add_argument('--zones', help='JSON string with zones data')
# Zones option removed
# New compilation option
parser.add_argument('--compile-acd', action='store_true', help='Compile L5X files to ACD after generation')
@ -90,8 +90,7 @@ def main() -> None:
if args.excel_file:
unified_args.extend(['--excel-file', str(args.excel_file)])
if args.zones:
unified_args.extend(['--zones', args.zones])
# Zones option removed
# Add the 'all' command
unified_args.append('all')

Binary file not shown.

View File

@ -1,251 +1,310 @@
"""Base Generator Classes using Template Method Pattern.
Provides structured, extensible base classes for PLC program generation
using XML Builder and Plugin systems.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, List, Dict, Any
from .config import GeneratorConfig
from .data_loader import DataLoader
from .xml_builder import L5XBuilder, L5XBuilderFactory
from .plugin_system import RoutineManager, RoutineContext
from .logging_config import get_logger
class BaseGenerator(ABC):
"""Base class implementing the Template Method pattern for PLC generation."""
def __init__(self, config: GeneratorConfig, data_loader: DataLoader,
xml_builder_factory: L5XBuilderFactory):
self.config = config
self.data_loader = data_loader
self.xml_builder_factory = xml_builder_factory
self.logger = get_logger(self.__class__.__name__)
# Will be set during generation
self.builder: Optional[L5XBuilder] = None
self.routine_manager: Optional[RoutineManager] = None
def generate(self) -> ET.Element:
"""Template method for generating PLC programs.
This defines the algorithm structure while allowing subclasses
to customize specific steps.
"""
self.logger.info(f"Starting {self.__class__.__name__} generation")
try:
# Step 1: Create XML structure
self.builder = self.create_xml_structure()
# Step 2: Add controller-level elements
self.add_controller_elements()
# Step 3: Create routine context and manager
self.setup_routine_manager()
# Step 4: Generate routines
self.generate_routines()
# Step 5: Add program-level elements
self.add_program_elements()
# Step 6: Finalize
self.finalize_generation()
xml_tree = self.builder.build()
self.logger.info(f"Successfully completed {self.__class__.__name__} generation")
return xml_tree
except Exception as e:
self.logger.error(f"Failed to generate {self.__class__.__name__}: {e}")
raise
@abstractmethod
def create_xml_structure(self) -> L5XBuilder:
"""Create the basic XML structure for this program type."""
pass
def add_controller_elements(self) -> None:
"""Add controller-level elements. Override if needed."""
pass
def setup_routine_manager(self) -> None:
"""Set up the routine manager with proper context."""
if not self.builder:
raise ValueError("XML builder must be created first")
context = RoutineContext(
data_loader=self.data_loader,
config=self.config,
routines_element=self.builder.get_routines_section(),
program_element=self.builder.get_program_element(),
metadata=self.get_context_metadata()
)
from .plugin_system import RoutineManager, get_default_registry
self.routine_manager = RoutineManager(context, get_default_registry())
@abstractmethod
def generate_routines(self) -> None:
"""Generate the routines for this program type."""
pass
def add_program_elements(self) -> None:
"""Add program-level elements. Override if needed."""
pass
def finalize_generation(self) -> None:
"""Finalize the generation process. Override if needed."""
pass
def get_context_metadata(self) -> Dict[str, Any]:
"""Get metadata for routine context. Override to add custom metadata."""
return {
'generator_type': self.__class__.__name__,
'config': self.config,
'excel_file': str(self.data_loader.excel_path)
}
def build_xml_string(self) -> str:
"""Build and return the formatted XML string."""
if not self.builder:
raise ValueError("Generation must be completed first")
return self.builder.build_xml_string()
def write(self, output_file: str | Path) -> None:
"""Write the generated XML to a file."""
if not self.builder:
# Generate if not already done
self.generate()
self.builder.write_to_file(output_file)
self.logger.info(f"Written {self.__class__.__name__} to {output_file}")
class SafetyProgramGenerator(BaseGenerator):
"""Template Method implementation for SafetyProgram generation."""
def create_xml_structure(self) -> L5XBuilder:
"""Create SafetyProgram XML structure."""
self.logger.debug("Creating SafetyProgram XML structure")
return self.xml_builder_factory.create_safety_program_builder("SafetyProgram")
def generate_routines(self) -> None:
"""Generate safety routines."""
if not self.routine_manager:
raise ValueError("Routine manager must be set up first")
self.logger.info("Generating safety routines...")
# Generate safety-specific routines
safety_routines = [
'inputs',
'outputs',
'resets',
'estops',
'zones'
]
results = {}
for routine_name in safety_routines:
results[routine_name] = self.routine_manager.generate_routine(routine_name)
self.logger.info(f"Safety routine generation results: {results}")
def add_program_elements(self) -> None:
"""Add safety-specific program elements."""
if not self.builder:
raise ValueError("XML builder must be created first")
# Add safety signatures
self.builder.add_safety_signatures()
# Add safety tag map if needed
self._add_safety_tag_map()
def _add_safety_tag_map(self) -> None:
"""Add safety tag map to the program."""
program_element = self.builder.get_program_element()
# Get safety tags from data
safety_tags = self.data_loader.safety_tags_from_pb
if safety_tags:
from .generators.safety_program import create_safety_tag_map
create_safety_tag_map(program_element, safety_tags, set())
self.logger.debug(f"Added safety tag map with {len(safety_tags)} tags")
class MainProgramGenerator(BaseGenerator):
"""Template Method implementation for MainProgram generation."""
def create_xml_structure(self) -> L5XBuilder:
"""Create MainProgram XML structure."""
self.logger.debug("Creating MainProgram XML structure")
return self.xml_builder_factory.create_main_program_builder("MainProgram")
def add_controller_elements(self) -> None:
"""Add controller tags for MainProgram."""
if not self.builder:
raise ValueError("XML builder must be created first")
self.logger.debug("Adding controller tags...")
# Generate and add controller tags
from .writers.xml_tag_writer import create_limited_tag_xml_elements
tag_elements = create_limited_tag_xml_elements(
self.data_loader.excel_path,
data_loader=self.data_loader
)
self.builder.add_controller_tags(tag_elements)
self.logger.info(f"Added {len(tag_elements)} controller tags")
def generate_routines(self) -> None:
"""Generate main program routines."""
if not self.routine_manager:
raise ValueError("Routine manager must be set up first")
self.logger.info("Generating main program routines...")
# Generate main program routines
main_routines = [
'main_routine',
'safety_tag_map',
'estop_check'
]
results = {}
for routine_name in main_routines:
results[routine_name] = self.routine_manager.generate_routine(routine_name)
self.logger.info(f"Main routine generation results: {results}")
class LegacyCompatibilityMixin:
"""Mixin to provide backward compatibility with legacy generators."""
def __init__(self, excel_path: str | Path, zones_dict: Optional[List[Dict[str, str]]] = None):
"""Legacy constructor interface."""
# This will be mixed with the new generators to maintain compatibility
from .config import get_config
from .data_loader import DataLoader
from .xml_builder import L5XBuilderFactory
config = get_config()
data_loader = DataLoader(excel_path=excel_path, zones_dict=zones_dict)
xml_builder_factory = L5XBuilderFactory(config)
# Call the parent constructor
super().__init__(config, data_loader, xml_builder_factory)
# Backward compatible generator classes
class ModernSafetyProgramGenerator(LegacyCompatibilityMixin, SafetyProgramGenerator):
"""Modern SafetyProgram generator with legacy interface."""
pass
class ModernMainProgramGenerator(LegacyCompatibilityMixin, MainProgramGenerator):
"""Modern MainProgram generator with legacy interface."""
"""Base Generator Classes using Template Method Pattern.
Provides structured, extensible base classes for PLC program generation
using XML Builder and Plugin systems.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, List, Dict, Any
from .config import GeneratorConfig
from .data_loader import DataLoader
from .xml_builder import L5XBuilder, L5XBuilderFactory
from .plugin_system import RoutineManager, RoutineContext
from .logging_config import get_logger
class BaseGenerator(ABC):
"""Base class implementing the Template Method pattern for PLC generation."""
def __init__(self, config: GeneratorConfig, data_loader: DataLoader,
xml_builder_factory: L5XBuilderFactory):
self.config = config
self.data_loader = data_loader
self.xml_builder_factory = xml_builder_factory
self.logger = get_logger(self.__class__.__name__)
# Will be set during generation
self.builder: Optional[L5XBuilder] = None
self.routine_manager: Optional[RoutineManager] = None
def generate(self) -> ET.Element:
"""Template method for generating PLC programs.
This defines the algorithm structure while allowing subclasses
to customize specific steps.
"""
self.logger.info(f"Starting {self.__class__.__name__} generation")
try:
# Step 1: Create XML structure
self.builder = self.create_xml_structure()
# Step 2: Add controller-level elements
self.add_controller_elements()
# Step 3: Create routine context and manager
self.setup_routine_manager()
# Step 4: Generate routines
self.generate_routines()
# Step 5: Add program-level elements
self.add_program_elements()
# Step 6: Finalize
self.finalize_generation()
xml_tree = self.builder.build()
self.logger.info(f"Successfully completed {self.__class__.__name__} generation")
return xml_tree
except Exception as e:
self.logger.error(f"Failed to generate {self.__class__.__name__}: {e}")
raise
@abstractmethod
def create_xml_structure(self) -> L5XBuilder:
"""Create the basic XML structure for this program type."""
pass
def add_controller_elements(self) -> None:
"""Add controller-level elements. Override if needed."""
pass
def setup_routine_manager(self) -> None:
"""Set up the routine manager with proper context."""
if not self.builder:
raise ValueError("XML builder must be created first")
context = RoutineContext(
data_loader=self.data_loader,
config=self.config,
routines_element=self.builder.get_routines_section(),
program_element=self.builder.get_program_element(),
metadata=self.get_context_metadata()
)
from .plugin_system import RoutineManager, get_default_registry
self.routine_manager = RoutineManager(context, get_default_registry())
@abstractmethod
def generate_routines(self) -> None:
"""Generate the routines for this program type."""
pass
def add_program_elements(self) -> None:
"""Add program-level elements. Override if needed."""
pass
def finalize_generation(self) -> None:
"""Finalize the generation process. Override if needed."""
pass
def get_context_metadata(self) -> Dict[str, Any]:
"""Get metadata for routine context. Override to add custom metadata."""
return {
'generator_type': self.__class__.__name__,
'config': self.config,
'excel_file': str(self.data_loader.excel_path)
}
def build_xml_string(self) -> str:
"""Build and return the formatted XML string."""
if not self.builder:
raise ValueError("Generation must be completed first")
return self.builder.build_xml_string()
def write(self, output_file: str | Path) -> None:
"""Write the generated XML to a file."""
if not self.builder:
# Generate if not already done
self.generate()
self.builder.write_to_file(output_file)
self.logger.info(f"Written {self.__class__.__name__} to {output_file}")
class SafetyProgramGenerator(BaseGenerator):
"""Template Method implementation for SafetyProgram generation."""
def create_xml_structure(self) -> L5XBuilder:
"""Create SafetyProgram XML structure."""
self.logger.debug("Creating SafetyProgram XML structure")
return self.xml_builder_factory.create_safety_program_builder("SafetyProgram")
def generate_routines(self) -> None:
"""Generate safety routines."""
if not self.routine_manager:
raise ValueError("Routine manager must be set up first")
self.logger.info("Generating safety routines...")
# If a config-driven routine plan is provided, use only entries targeting SafetyProgram
if getattr(self.config, 'routine_plan', None):
results = {}
for entry in sorted(
[e for e in self.config.routine_plan if e.enabled and e.program == 'SafetyProgram'],
key=lambda e: e.order
):
# enrich context with per-routine params/filters
self.routine_manager.context.metadata['params'] = entry.params or {}
self.routine_manager.context.metadata['filters'] = self.config.filters.for_routine(entry.name)
results[entry.name] = self.routine_manager.generate_routine(entry.plugin)
self.logger.info(f"Safety routine generation results: {results}")
return
# Fallback: default fixed set
safety_routines = ['inputs', 'outputs', 'resets', 'estops']
results = {name: self.routine_manager.generate_routine(name) for name in safety_routines}
self.logger.info(f"Safety routine generation results: {results}")
def add_program_elements(self) -> None:
"""Add safety-specific program elements."""
if not self.builder:
raise ValueError("XML builder must be created first")
# Add safety signatures
self.builder.add_safety_signatures()
# Add safety tag map if needed
self._add_safety_tag_map()
# Add zones routine from configuration if available
try:
zones_df = self.data_loader.zones
if zones_df is not None:
from .routines.zones import create_zones_routine
create_zones_routine(self.builder.get_routines_section(), zones_df, self.data_loader.epc)
self.logger.info("Added R030_ZONES routine from zones.json")
except Exception:
# Zones are optional; proceed without blocking generation
pass
# Ensure a MainRoutine exists and references generated safety routines
# The ProgramAttributes set MainRoutineName to config.routines.main_routine_name
# so we must create that routine here.
program_el = self.builder.get_program_element()
routines_el = self.builder.get_routines_section()
from .config import get_config
cfg_local = get_config()
main_routine_name = cfg_local.routines.main_routine_name
import xml.etree.ElementTree as ET
# Create MainRoutine only if it does not already exist
if not any(r.get('Name') == main_routine_name for r in routines_el.findall('Routine')):
routine = ET.SubElement(routines_el, 'Routine', Name=main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
nm = cfg_local.routines.name_map
# Ensure ESTOPS before ZONES so MCM_EPB_DCS_CTRL exists before ZONES references .O1
calls = [
nm.get('inputs', 'R010_INPUTS'),
nm.get('outputs', 'R011_OUTPUTS'),
nm.get('resets', 'R012_RESETS'),
nm.get('estops', 'R020_ESTOPS'),
nm.get('zones', 'R030_ZONES'),
]
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'
def _add_safety_tag_map(self) -> None:
"""Add safety tag map to the program."""
program_element = self.builder.get_program_element()
# Get safety tags from data
safety_tags = self.data_loader.safety_tags_from_pb
if safety_tags:
from .generators.safety_program import create_safety_tag_map
create_safety_tag_map(program_element, safety_tags, set())
self.logger.debug(f"Added safety tag map with {len(safety_tags)} tags")
class MainProgramGenerator(BaseGenerator):
"""Template Method implementation for MainProgram generation."""
def create_xml_structure(self) -> L5XBuilder:
"""Create MainProgram XML structure."""
self.logger.debug("Creating MainProgram XML structure")
return self.xml_builder_factory.create_main_program_builder("MainProgram")
def add_controller_elements(self) -> None:
"""Add controller tags for MainProgram."""
if not self.builder:
raise ValueError("XML builder must be created first")
self.logger.debug("Adding controller tags...")
# Generate and add controller tags
from .writers.xml_tag_writer import create_limited_tag_xml_elements
tag_elements = create_limited_tag_xml_elements(
self.data_loader.excel_path,
data_loader=self.data_loader
)
self.builder.add_controller_tags(tag_elements)
self.logger.info(f"Added {len(tag_elements)} controller tags")
def generate_routines(self) -> None:
"""Generate main program routines."""
if not self.routine_manager:
raise ValueError("Routine manager must be set up first")
self.logger.info("Generating main program routines...")
# Config-driven plan if available (MainProgram only)
if getattr(self.config, 'routine_plan', None):
results = {}
# Sort entries by order, but always run main_routine last so it can JSR to generated routines
entries = sorted(
[e for e in self.config.routine_plan if e.enabled and e.program == 'MainProgram'],
key=lambda e: e.order
)
non_main = [e for e in entries if getattr(e, 'plugin', '') != 'main_routine' and getattr(e, 'name', '') != 'main_routine']
mains = [e for e in entries if getattr(e, 'plugin', '') == 'main_routine' or getattr(e, 'name', '') == 'main_routine']
def _run(entry):
self.routine_manager.context.metadata['params'] = entry.params or {}
self.routine_manager.context.metadata['filters'] = self.config.filters.for_routine(entry.name)
results[entry.name] = self.routine_manager.generate_routine(entry.plugin)
for entry in non_main:
_run(entry)
for entry in mains:
_run(entry)
self.logger.info(f"Main routine generation results: {results}")
return
# Fallback default set (ensure MainRoutine is generated last so it can JSR to routines that exist)
main_routines = ['safety_tag_map', 'rack', 'estop_check', 'main_routine']
results = {name: self.routine_manager.generate_routine(name) for name in main_routines}
self.logger.info(f"Main routine generation results: {results}")
class LegacyCompatibilityMixin:
"""Mixin to provide backward compatibility with legacy generators."""
def __init__(self, excel_path: str | Path, zones_dict: Optional[List[Dict[str, str]]] = None):
"""Legacy constructor interface."""
# This will be mixed with the new generators to maintain compatibility
from .config import get_config
from .data_loader import DataLoader
from .xml_builder import L5XBuilderFactory
config = get_config()
data_loader = DataLoader(excel_path=excel_path, zones_dict=zones_dict)
xml_builder_factory = L5XBuilderFactory(config)
# Call the parent constructor
super().__init__(config, data_loader, xml_builder_factory)
# Backward compatible generator classes
class ModernSafetyProgramGenerator(LegacyCompatibilityMixin, SafetyProgramGenerator):
"""Modern SafetyProgram generator with legacy interface."""
pass
class ModernMainProgramGenerator(LegacyCompatibilityMixin, MainProgramGenerator):
"""Modern MainProgram generator with legacy interface."""
pass

View File

@ -7,8 +7,7 @@ from generators import (
SafetyProgramGenerator,
MainProgramGenerator,
)
from writers import create_new_csv_with_tags, create_safety_tag_mapping
from writers.csv_writer import create_limited_csv_with_tags
from writers import create_safety_tag_mapping
def _cmd_safety(args: argparse.Namespace) -> None:
@ -25,9 +24,7 @@ def _cmd_main(args: argparse.Namespace) -> None:
print(f"Main L5X written to {args.output}")
def _cmd_csv(args: argparse.Namespace) -> None:
std, saf, dcs = create_new_csv_with_tags(args.excel, args.original, args.output)
print(f"CSV generated → {args.output} (Std {std}, Safety {saf}, DCS {dcs})")
# CSV generation removed (deprecated)
def _cmd_mapping(args: argparse.Namespace) -> None:
@ -36,7 +33,7 @@ def _cmd_mapping(args: argparse.Namespace) -> None:
def _cmd_safety_only(args: argparse.Namespace) -> None:
"""Generate only essential safety routines using DESC_IP data extraction (RST, STO, EPC) and zones_config.py."""
"""Generate only essential safety routines using DESC_IP data extraction (RST, STO, EPC)."""
# Check if ignore_estop1ok flag is set
ignore_estop1ok = getattr(args, 'ignore_estop1ok', False)
@ -55,14 +52,7 @@ def _cmd_safety_only(args: argparse.Namespace) -> None:
main_gen.write(main_output)
print(f"Limited Main L5X written to {main_output}")
# Generate limited CSV with tags (requires original CSV)
csv_output = args.csv_output or 'MTN6_MCM01_Controller_Tags_Limited.CSV'
original_csv = args.original_csv or 'MTN6_MCM01_Controller_Tags_Original.CSV'
if Path(original_csv).exists():
std_count, saf_count, dcs_count = create_limited_csv_with_tags(args.excel, original_csv, csv_output)
print(f"Limited CSV written to {csv_output} (Std {std_count}, Safety {saf_count}, DCS {dcs_count})")
else:
print(f"Warning: Original CSV file {original_csv} not found - skipping CSV generation")
# CSV generation removed
# Create safety tag mapping for the limited mode
from data_loader import LimitedDataLoader
@ -83,15 +73,14 @@ def _cmd_safety_only(args: argparse.Namespace) -> None:
print(f"Limited safety tag mapping written to {mapping_output}")
print(f"\n[SUCCESS] Safety-only mode complete:")
print(f" - Safety routines: inputs, outputs, estops, zones, resets")
print(f" - Safety routines: inputs, outputs, estops, resets")
print(f" - Main routines: safety_tag_map, estop_check")
print(f" - Data sources: DESC_IP extraction (RST, STO, EPC) + zones_config.py")
print(f" - Data sources: DESC_IP extraction (RST, STO, EPC)")
_DEF_OUTPUTS = {
'safety': 'SafetyProgram_Generated.L5X',
'main': 'MainProgram_Generated.L5X',
'csv': 'MTN6_MCM01_Controller_Tags_Complete.CSV',
'mapping': 'SafetyTagMapping.txt',
}
@ -112,12 +101,7 @@ def build_parser() -> argparse.ArgumentParser:
m.add_argument('-o', '--output', default=_DEF_OUTPUTS['main'])
m.set_defaults(func=_cmd_main)
# CSV
c = sub.add_parser('csv', help='Generate complete CSV tags list')
c.add_argument('excel')
c.add_argument('original', help='Original CSV file with existing tags')
c.add_argument('-o', '--output', default=_DEF_OUTPUTS['csv'])
c.set_defaults(func=_cmd_csv)
# CSV subcommand removed
# Mapping
mp = sub.add_parser('mapping', help='Generate SafetyTag mapping file')
@ -126,14 +110,13 @@ def build_parser() -> argparse.ArgumentParser:
mp.set_defaults(func=_cmd_mapping)
# Safety-only mode
so = sub.add_parser('safety-only', help='Generate only essential safety routines (inputs, outputs, estops, zones, resets, safety_tag_map, estop_check) using DESC_IP data extraction and zones_config.py')
so = sub.add_parser('safety-only', help='Generate only essential safety routines (inputs, outputs, estops, resets, safety_tag_map, estop_check) using DESC_IP data extraction')
so.add_argument('excel', help='Merged descriptor Excel file')
so.add_argument('--safety-output', help='Safety L5X output file (default: SafetyProgram_Limited.L5X)')
so.add_argument('--main-output', help='Main L5X output file (default: MainProgram_Limited.L5X)')
so.add_argument('--csv-output', help='CSV tags output file (default: MTN6_MCM01_Controller_Tags_Limited.CSV)')
so.add_argument('--original-csv', help='Original CSV file (default: MTN6_MCM01_Controller_Tags_Original.CSV)')
# CSV options removed
so.add_argument('--mapping-output', help='Safety tag mapping output file (default: SafetyTagMapping_Limited.txt)')
so.add_argument('--ignore-estop1ok', action='store_true', help='Ignore ESTOP1OK tags in inputs, estops, and zones routines')
so.add_argument('--ignore-estop1ok', action='store_true', help='Ignore ESTOP1OK tags in inputs and estops routines')
so.set_defaults(func=_cmd_safety_only)
return p

View File

@ -2,20 +2,18 @@ from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, Optional, List, Any
import json
@dataclass
class FileConfig:
"""File path configuration."""
excel_file: Path = Path('DESC_IP_MERGED.xlsx')
original_csv: Path = Path('MTN6_MCM01_Controller_Tags_Original.CSV')
output_dir: Path = Path('.')
# Output file names
safety_l5x: str = 'SafetyProgram_Generated.L5X'
main_l5x: str = 'MainProgram_Generated.L5X'
complete_csv: str = 'MTN6_MCM01_Controller_Tags_Complete.CSV'
mapping_txt: str = 'SafetyTagMapping.txt'
@dataclass
@ -36,6 +34,8 @@ class ExtractionConfig:
rst_desc_excludes: list[str] = field(default_factory=lambda: ['LIGHT'])
rst_desca_patterns: list[str] = field(default_factory=lambda: ['S1_PB', 'S2_PB'])
rst_desca_endings: list[str] = field(default_factory=lambda: ['SPB'])
rst_desca_exclude_patterns: list[str] = field(default_factory=lambda: ['GS1'])
# STO extraction rules
sto_tagname_patterns: list[str] = field(default_factory=lambda: ['VFD'])
@ -44,14 +44,88 @@ class ExtractionConfig:
# EPC extraction rules
epc_desca_patterns: list[str] = field(default_factory=lambda: ['EPC', 'ESTOP'])
# Additional device extraction rules (override defaults when provided)
dpm_partnumber_contains: list[str] = field(default_factory=lambda: ['OS30-002404-2S'])
fiom_partnumber_contains: list[str] = field(default_factory=lambda: ['5032-8IOLM12DR'])
fioh_partnumber_contains: list[str] = field(default_factory=lambda: ['5032-8IOLM12DR'])
fioh_desca_contains: list[str] = field(default_factory=lambda: ['FIOH'])
# NETWORK sheet extraction rules
apf_partnumber_prefix: list[str] = field(default_factory=lambda: ['35S'])
extendo_partnumber_exact: list[str] = field(default_factory=lambda: ['CALJAN'])
pmm_partnumber_exact: list[str] = field(default_factory=lambda: ['1420-V2-ENT'])
speed_ctrl_partnumber_prefix: list[str] = field(default_factory=lambda: ['35S'])
# CB monitor extraction rules
cb_desca_include: list[str] = field(default_factory=lambda: ['CB'])
cb_desca_exclude: list[str] = field(default_factory=lambda: ['BCN'])
# D2C_CHUTE patterns
s0_prefix: str = 'S0'
d2c_gs1_pb_token: str = 'GS1_PB'
d2c_gs1_pb_lt_token: str = 'GS1_PB_LT'
d2c_bcn_token: str = 'BCN'
d2c_zmx_suffix: str = '_ZMX'
# PB_CHUTE components
pb_chute_components: list[str] = field(default_factory=lambda: ['PE1', 'PE2', 'PR1', 'SOL1'])
fioh_token: str = 'FIOH'
bcn_token: str = 'BCN'
beacon_stack_3_tokens: list[str] = field(default_factory=lambda: ['3-STACK', '3 STACK'])
beacon_segment_a_pin4: str = 'Connector_1_A_Pin_4'
beacon_segment_b_pin2: str = 'Connector_1_B_Pin_2'
# STATION_JR_CHUTE / STATION_JR_PB
jr1_token: str = 'JR1'
jr1_pb_token: str = 'JR1_PB'
jr1_pb_lt_token: str = 'JR1_PB_LT'
jr2_token: str = 'JR2'
jr2_pb_token: str = 'JR2_PB'
# Optional: accept JR2 PB light token from config for forward compatibility
jr2_pb_lt_token: str = 'JR2_PB_LT'
jr1_exclude_tokens: list[str] = field(default_factory=lambda: ['FL', 'S0'])
vfd_prefix_regex: str = r'(FL\d+_\d+)'
vfd_suffix_default: str = '_VFD1'
# JPE / FPE
jpe_include_tokens: list[str] = field(default_factory=lambda: ['JPE', '_2_PE'])
jpe_exclude_tokens: list[str] = field(default_factory=lambda: ['3CH_PE'])
jpe_input_default: str = 'In_2'
fpe_include_tokens: list[str] = field(default_factory=lambda: ['FPE', '3CH_PE'])
# FLOW_CTRL generation controls
# Regex to extract lane grouping and position from VFD names (generic: <lane>_<pos>_VFDn)
flow_ctrl_vfd_name_regex: str = r'^(?P<lane>[^_]+)_(?P<pos>\d+[A-Za-z]?)_VFD\d*'
# Regex to extract lane grouping and position from EXTENDO names (generic: <lane>_<pos>_EXn)
flow_ctrl_extendo_name_regex: str = r'^(?P<lane>[^_]+)_(?P<pos>\d+[A-Za-z]?)_EX\d*'
# Chain order strategy: 'lane' chains per lane using extracted numeric positions; 'natural' uses natural sort
flow_ctrl_chain_order: str = 'lane'
# Whether to emit EXTENDO interlocks based on discovered dependencies
flow_ctrl_enable_extendo_interlocks: bool = True
# Whether to emit an unconditional OTE for the first VFD in each lane
flow_ctrl_emit_first_vfd_ote: bool = True
# Chain direction: 'downstream_to_upstream' or 'upstream_to_downstream'
flow_ctrl_chain_direction: str = 'downstream_to_upstream'
# Whether to emit an unconditional OTE for the last VFD in each lane (always interlock last in lane)
flow_ctrl_emit_last_vfd_ote: bool = True
# Whether to emit an unconditional OTE for the last EXTENDO in each lane
flow_ctrl_emit_last_extendo_ote: bool = True
# Selection: choose VFDs by Name regex instead of PartNumber prefixes
flow_ctrl_select_vfds_by_name: bool = True
# Lane grouping: number of leading underscore-separated tokens to use as lane key (e.g., 3 -> UL5_3_VFD1 base UL5_3_VFD)
flow_ctrl_lane_tokens: int = 3
@dataclass
class RoutineConfig:
"""Routine generation configuration."""
# Routine naming patterns
main_routine_name: str = 'MainRoutine'
safety_tag_map_name: str = 'R000_SAFETY_TAG_MAP'
estop_check_name: str = 'R100_ESTOP_CHECK'
safety_tag_map_name: str = 'R130_SAFETY_TAG_MAP'
estop_check_name: str = 'R120_ESTOP_CHECK'
# Safety tag mapping prefix and MCM input address
safety_tag_prefix: str = 'SFT_'
mcm_input_address: str = 'Local:5:I.Data.0'
# Safety routine names
inputs_routine: str = 'R010_INPUTS'
@ -64,6 +138,61 @@ class RoutineConfig:
mcm_safety_tag: str = 'MCM_S_PB'
mcm_epb_tag: str = 'MCM_EPB_DCS_CTRL.O1'
# Routine name map for all known plugins (can be overridden in config)
name_map: Dict[str, str] = field(default_factory=lambda: {
'main_routine': 'MainRoutine',
'safety_tag_map': 'R130_SAFETY_TAG_MAP',
'estop_check': 'R120_ESTOP_CHECK',
'dpm': 'R020_DPM',
'fiom': 'R030_FIOM',
'fioh': 'R031_FIOH',
'apf': 'R040_APF',
'extendo': 'R041_EXTENDO',
'flow_ctrl': 'R050_FLOW_CTRL',
'speed_ctrl': 'R051_SPEED_CTRL',
'd2c_chute': 'R042_D2C_CHUTE',
'pb_chute': 'R043_PB_CHUTE',
'station_jr_chute': 'R044_STATION_JR_CHUTE',
'station_jr_pb': 'R090_STATION_JR_PB',
'jpe': 'R100_JPE',
'fpe': 'R101_FPE',
'pmm': 'R060_PMM',
'cb_monitor': 'R070_CB_MONITOR',
'rack': 'R011_RACK',
'mcm': 'R010_MCM',
'inputs': 'R010_INPUTS',
'outputs': 'R011_OUTPUTS',
'resets': 'R012_RESETS',
'estops': 'R020_ESTOPS',
'zones': 'R030_ZONES',
})
@dataclass
class RoutineEntry:
"""Config entry for a single routine in the config-driven plan."""
name: str
plugin: str
enabled: bool = True
program: str = "MainProgram" # or "SafetyProgram"
order: int = 100
params: Dict[str, Any] = field(default_factory=dict)
@dataclass
class FiltersConfig:
"""Global and per-routine filters. Values are simple include/exclude lists."""
global_filters: Dict[str, List[str]] = field(default_factory=dict)
per_routine: Dict[str, Dict[str, List[str]]] = field(default_factory=dict)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'FiltersConfig':
# Support keys: "global" and "per_routine"
global_raw = data.get('global', {})
per_routine_raw = data.get('per_routine', {})
return cls(global_filters=global_raw, per_routine=per_routine_raw)
def for_routine(self, routine_name: str) -> Dict[str, List[str]]:
return self.per_routine.get(routine_name, {})
@dataclass
class GeneratorConfig:
"""Complete configuration for the generator system."""
@ -72,6 +201,12 @@ class GeneratorConfig:
xml: XMLConfig = field(default_factory=XMLConfig)
extraction: ExtractionConfig = field(default_factory=ExtractionConfig)
routines: RoutineConfig = field(default_factory=RoutineConfig)
# Optional config-driven routine plan and filters
routine_plan: List[RoutineEntry] = field(default_factory=list)
filters: FiltersConfig = field(default_factory=FiltersConfig)
# Tags control (per-program module inclusion). Example:
# {"MainProgram": {"DPM": true, "FIOM": false}}
tags: Dict[str, Dict[str, bool]] = field(default_factory=dict)
@classmethod
def from_file(cls, config_path: Path) -> 'GeneratorConfig':
@ -90,17 +225,44 @@ class GeneratorConfig:
# Convert specific path fields to Path objects
if 'excel_file' in files_data:
files_data['excel_file'] = Path(files_data['excel_file'])
if 'original_csv' in files_data:
files_data['original_csv'] = Path(files_data['original_csv'])
if 'output_dir' in files_data:
files_data['output_dir'] = Path(files_data['output_dir'])
files = FileConfig(**files_data)
xml = XMLConfig(**config_data.get('xml', {}))
extraction = ExtractionConfig(**config_data.get('extraction', {}))
routines = RoutineConfig(**config_data.get('routines', {}))
return cls(files=files, xml=xml, extraction=extraction, routines=routines)
# Support both legacy dict-based routines config and new list-based plan
routines_section = config_data.get('routines', {})
routine_plan: List[RoutineEntry] = []
routines: RoutineConfig
if isinstance(routines_section, list):
# New plan format under key 'routines'
for entry in routines_section:
try:
routine_plan.append(RoutineEntry(**entry))
except Exception:
# Skip invalid entries silently to keep robustness
continue
routines = RoutineConfig()
else:
routines = RoutineConfig(**routines_section)
# Optional filters section
filters_section = config_data.get('filters', {})
filters = FiltersConfig.from_dict(filters_section) if isinstance(filters_section, dict) else FiltersConfig()
# Tags section
tags = config_data.get('tags', {}) or {}
return cls(
files=files,
xml=xml,
extraction=extraction,
routines=routines,
routine_plan=routine_plan,
filters=filters,
tags=tags,
)
def save_to_file(self, config_path: Path) -> None:
"""Save configuration to JSON file."""
@ -108,8 +270,11 @@ class GeneratorConfig:
'files': self.files.__dict__,
'xml': self.xml.__dict__,
'extraction': self.extraction.__dict__,
'routines': self.routines.__dict__
'routines': self.routines.__dict__,
'tags': self.tags,
}
# Intentionally do not overwrite user's list-based routines if present; save only legacy schema by default.
# Advanced users can manage JSON manually for routine_plan/filters.
# Convert Path objects to strings for JSON serialization
def convert_paths(obj):

View File

@ -26,27 +26,41 @@ class GeneratorContainer:
self._logger.debug("Creating DataLoader instance", excel_file=str(self.config.files.excel_file))
self._instances['data_loader'] = DataLoader(
excel_path=self.config.files.excel_file,
zones_dict=self.zones_dict
zones_dict=self.zones_dict or []
)
return self._instances['data_loader']
# Zones support removed: no config file or inline loading
def get_safety_program_generator(self):
"""Get or create SafetyProgramGenerator instance."""
"""Get or create SafetyProgramGenerator instance (modern by default).
Note: Limited generators are deprecated. This method now returns the
modern generator implementation.
"""
if 'safety_program_generator' not in self._instances:
from .generators.safety_program import LimitedSafetyProgramGenerator
self._logger.debug("Creating SafetyProgramGenerator instance")
self._instances['safety_program_generator'] = LimitedSafetyProgramGenerator(
from .base_generator import ModernSafetyProgramGenerator
self._logger.warning(
"LimitedSafetyProgramGenerator is deprecated; using ModernSafetyProgramGenerator"
)
self._instances['safety_program_generator'] = ModernSafetyProgramGenerator(
excel_path=self.config.files.excel_file,
zones_dict=self.zones_dict
)
return self._instances['safety_program_generator']
def get_main_program_generator(self):
"""Get or create MainProgramGenerator instance."""
"""Get or create MainProgramGenerator instance (modern by default).
Note: Limited generators are deprecated. This method now returns the
modern generator implementation.
"""
if 'main_program_generator' not in self._instances:
from .generators.main_program import LimitedMainProgramGenerator
self._logger.debug("Creating MainProgramGenerator instance")
self._instances['main_program_generator'] = LimitedMainProgramGenerator(
from .base_generator import ModernMainProgramGenerator
self._logger.warning(
"LimitedMainProgramGenerator is deprecated; using ModernMainProgramGenerator"
)
self._instances['main_program_generator'] = ModernMainProgramGenerator(
excel_path=self.config.files.excel_file,
zones_dict=self.zones_dict
)
@ -109,12 +123,7 @@ class GeneratorContainer:
return self._instances['tag_writer']
def get_csv_writer(self):
"""Get or create CSVWriter instance."""
if 'csv_writer' not in self._instances:
from .writers.csv_writer import create_limited_csv_with_tags
self._logger.debug("Creating CSVWriter wrapper")
self._instances['csv_writer'] = create_limited_csv_with_tags
return self._instances['csv_writer']
raise NotImplementedError("CSV generation is removed. Use config-driven XML generation only.")
def get_mapping_writer(self):
"""Get or create MappingWriter instance."""

File diff suppressed because it is too large Load Diff

View File

@ -2,16 +2,30 @@
import xml.etree.ElementTree as ET
from datetime import datetime
import re
from pathlib import Path
from typing import Optional, List, Dict
import pandas as pd
from ..data_loader import DataLoader
from ..config import get_config
from ..routines.estop_check import create_estop_check_routine
from ..routines.dpm import create_dpm_routine
from ..routines.fiom import create_fiom_routine
from ..routines.fioh import create_fioh_routine
from ..routines.apf import generate_apf_routine
from ..routines.extendo import generate_extendo_routine
from ..routines.d2c_chute import generate_d2c_chute_routine
from ..routines.pb_chute import generate_pb_chute_routine
from ..routines.station_jr_chute import generate_station_jr_chute_routine
from ..routines.station_jr_pb import generate_station_jr_pb_routine
from ..routines.jpe import generate_jpe_routine
from ..routines.fpe import generate_fpe_routine
from ..routines.pmm import generate_pmm_routine
from ..routines.cb_monitor import generate_cb_monitor_routine
from ..routines.flow_control import generate_flow_control_routine
from ..routines.speed_control import generate_speed_control_routine
from ..utils.common import format_xml_to_match_original
from ..writers.xml_tag_writer import create_limited_tag_xml_elements
@ -36,26 +50,31 @@ class LimitedMainProgramGenerator:
else:
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
cfg = get_config()
root = ET.Element('RSLogix5000Content', attrib={
'SchemaRevision': '1.0',
'SoftwareRevision': '36.00',
'SchemaRevision': cfg.xml.schema_revision,
'SoftwareRevision': cfg.xml.software_revision,
'TargetName': 'MainProgram',
'TargetType': 'Program',
'TargetClass': 'Standard',
'TargetClass': cfg.xml.target_class,
'ContainsContext': 'true',
'ExportDate': xl_time,
'ExportOptions': 'References NoRawData L5KData DecoratedData Context Dependencies ForceProtectedEncoding AllProjDocTrans',
'ExportOptions': cfg.xml.export_options,
})
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MTN6_MCM01')
controller = ET.SubElement(root, 'Controller', Use='Context', Name=cfg.xml.controller_name)
# Add empty AddOnInstructionDefinitions element
ET.SubElement(controller, 'AddOnInstructionDefinitions')
# Add Tags section at controller level
tags_section = ET.SubElement(controller, 'Tags')
print(" Generating limited controller tags...", flush=True)
tag_elements = create_limited_tag_xml_elements(self.loader.excel_path, data_loader=self.loader)
print(" Generating limited controller tags (safety-only)...", flush=True)
# Build tags using config-driven toggles only
tag_elements = create_limited_tag_xml_elements(
self.loader.excel_path,
data_loader=self.loader,
)
# Add all tag elements to the Tags section
for tag_elem in tag_elements:
@ -65,54 +84,118 @@ class LimitedMainProgramGenerator:
programs = ET.SubElement(controller, 'Programs', Use='Context')
program = ET.SubElement(programs, 'Program', Use='Target', Name='MainProgram',
TestEdits='false', MainRoutineName='MainRoutine', Disabled='false',
Class='Standard', UseAsFolder='false')
TestEdits='false', MainRoutineName=cfg.routines.main_routine_name, Disabled='false',
Class=cfg.xml.target_class, UseAsFolder='false')
ET.SubElement(program, 'Tags')
routines_el = ET.SubElement(program, 'Routines')
print(" Creating limited MainProgram routines...", flush=True)
self._create_main_routine(routines_el)
print(" [SUCCESS] Created MainRoutine", flush=True)
# Config-driven routine plan support (optional)
cfg = get_config()
plan_entries = [
e for e in getattr(cfg, 'routine_plan', [])
if e.enabled and e.program == 'MainProgram'
]
plan_entries.sort(key=lambda e: e.order)
self._create_safety_tag_map_routine(routines_el)
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
def _apply_filters(df, flt: dict):
if df is None or not isinstance(flt, dict) or df.empty:
return df
result = df
# DESCA filters
inc = flt.get('desca_include')
if inc:
result = result[result['DESCA'].str.contains('|'.join(inc), case=False, na=False)]
exc = flt.get('desca_exclude')
if exc:
result = result[~result['DESCA'].str.contains('|'.join(exc), case=False, na=False)]
# TAGNAME filters
inc_t = flt.get('tagname_include')
if inc_t and 'TAGNAME' in result.columns:
result = result[result['TAGNAME'].str.contains('|'.join(inc_t), case=False, na=False)]
exc_t = flt.get('tagname_exclude')
if exc_t and 'TAGNAME' in result.columns:
result = result[~result['TAGNAME'].str.contains('|'.join(exc_t), case=False, na=False)]
return result
def run_default():
self._create_main_routine(routines_el)
print(" [SUCCESS] Created MainRoutine", flush=True)
self._create_safety_tag_map_routine(routines_el)
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, None)
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, self.loader.zones)
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
# Respect routine plan strictly: if a plan exists in config, do NOT fall back to defaults
if getattr(cfg, 'routine_plan', None):
if plan_entries:
# Map config plugin names to concrete generators
generated = []
for entry in plan_entries:
plugin = entry.plugin
if plugin == 'main_routine':
self._create_main_routine(routines_el)
generated.append('main_routine')
elif plugin == 'safety_tag_map':
# Optional filtering for RST via filters
flt = getattr(cfg, 'filters', None).for_routine(entry.name) if 'cfg' in locals() else {}
rst_df = _apply_filters(self.loader.rst, flt)
self._create_safety_tag_map_routine(routines_el, rst_df=rst_df)
generated.append('safety_tag_map')
elif plugin == 'estop_check':
flt = getattr(cfg, 'filters', None).for_routine(entry.name) if 'cfg' in locals() else {}
epc = _apply_filters(self.loader.epc, flt)
sto = _apply_filters(self.loader.sto, flt)
create_estop_check_routine(routines_el, epc, sto, None)
generated.append('estop_check')
else:
# Unknown plugin for MainProgram skip silently
continue
print(f" [SUCCESS] Created MainProgram routines (config-driven): {', '.join(generated)}", flush=True)
else:
print(" [INFO] Routine plan present; no MainProgram routines enabled -> skipping defaults", flush=True)
else:
run_default()
return root
@staticmethod
def _create_main_routine(routines_el: ET.Element) -> None:
routine = ET.SubElement(routines_el, 'Routine', Name='MainRoutine', Type='RLL')
from ..config import get_config
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
# Only JSR calls for safety_tag_map and estop_check
text.text = '[JSR(R000_SAFETY_TAG_MAP,0) ,JSR(R100_ESTOP_CHECK,0) ];'
# Only JSR calls for safety_tag_map and estop_check (config-driven names)
safety_tag_map_name = cfg_local.routines.name_map.get('safety_tag_map', 'R130_SAFETY_TAG_MAP')
estop_check_name = cfg_local.routines.name_map.get('estop_check', 'R120_ESTOP_CHECK')
text.text = f'[JSR({safety_tag_map_name},0) ,JSR({estop_check_name},0) ];'
def _create_safety_tag_map_routine(self, routines_el: ET.Element) -> None:
def _create_safety_tag_map_routine(self, routines_el: ET.Element, rst_df: Optional[pd.DataFrame] = None) -> None:
"""Create the R000_SAFETY_TAG_MAP routine with JSR to SafetyProgram ESTOP ZONES."""
routine = ET.SubElement(routines_el, 'Routine', Name='R000_SAFETY_TAG_MAP', Type='RLL')
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.safety_tag_map_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
# Determine subsystem from Excel file path to get the correct MCM tag name
import re
excel_path_str = str(self.loader.excel_path)
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM01"
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM"
# MCM mapping rung with subsystem-specific naming
rung_num = 0
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f'XIC(Local:5:I.Data.0)OTE({subsystem}_S_PB);'
text.text = f'XIC({cfg_local.routines.mcm_input_address})OTE({subsystem}_S_PB);'
rung_num += 1
# Push buttons from RST sheet
# Push buttons from RST sheet (allow override via filtered df)
df = rst_df if rst_df is not None else self.loader.rst
pb_count = 0
for _, row in self.loader.rst.iterrows():
for _, row in df.iterrows():
if not isinstance(row['DESCA'], str):
continue
# Skip GS1 patterns
@ -153,18 +236,22 @@ class DescIPMainProgramGenerator:
def _build_xml_tree(self) -> ET.Element:
"""Build MainProgram XML tree using DESC_IP extracted data."""
# Build basic XML structure
root = ET.Element('RSLogix5000Content')
root.set('SchemaRevision', '1.0')
root.set('SoftwareRevision', '36.00')
root.set('TargetName', 'MainProgram')
root.set('TargetType', 'Program')
root.set('ContainsContext', 'true')
root.set('ExportDate', 'Wed Jul 03 11:47:56 2024')
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MainProgram')
# Build basic XML structure (config-driven)
cfg = get_config()
from datetime import datetime
xl_time = datetime.now().strftime('%a %b %d %H:%M:%S %Y')
root = ET.Element('RSLogix5000Content', attrib={
'SchemaRevision': cfg.xml.schema_revision,
'SoftwareRevision': cfg.xml.software_revision,
'TargetName': 'MainProgram',
'TargetType': 'Program',
'ContainsContext': 'true',
'ExportDate': xl_time,
'ExportOptions': cfg.xml.export_options,
})
controller = ET.SubElement(root, 'Controller', Use='Context', Name=cfg.xml.controller_name)
programs = ET.SubElement(controller, 'Programs')
program = ET.SubElement(programs, 'Program', Use='Target', Name='MainProgram', Type='Normal')
program = ET.SubElement(programs, 'Program', Use='Target', Name='MainProgram', Type=cfg.xml.target_class)
routines_el = ET.SubElement(program, 'Routines')
# Main routine with JSRs (safety-only)
@ -178,24 +265,35 @@ class DescIPMainProgramGenerator:
@staticmethod
def _create_main_routine(routines_el: ET.Element) -> None:
routine = ET.SubElement(routines_el, 'Routine', Name='MainRoutine', Type='RLL')
from ..config import get_config
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
# Only JSR calls for safety_tag_map and estop_check
text.text = '[JSR(R000_SAFETY_TAG_MAP,0) ,JSR(R100_ESTOP_CHECK,0) ];'
# Only JSR calls for safety_tag_map and estop_check (config-driven)
safety_tag_map_name = cfg_local.routines.name_map.get('safety_tag_map', 'R000_SAFETY_TAG_MAP')
estop_check_name = cfg_local.routines.name_map.get('estop_check', 'R100_ESTOP_CHECK')
text.text = f'[JSR({safety_tag_map_name},0) ,JSR({estop_check_name},0) ];'
def _create_desc_ip_safety_tag_map_routine(self, routines_el: ET.Element) -> None:
"""Create R000_SAFETY_TAG_MAP routine from DESC_IP extracted data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R000_SAFETY_TAG_MAP', Type='RLL')
cfg_local = get_config()
safety_tag_map_name = cfg_local.routines.name_map.get('safety_tag_map', 'R000_SAFETY_TAG_MAP')
routine = ET.SubElement(routines_el, 'Routine', Name=safety_tag_map_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
# MCM mapping rung (always the same)
# MCM mapping rung (config-driven address and subsystem tag)
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = 'XIC(Local:5:I.Data.0)OTE(MCM_S_PB);'
import re
excel_path_str = str(self.loader.excel_path)
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM"
mcm_addr = cfg_local.routines.mcm_input_address
text.text = f'XIC({mcm_addr})OTE({subsystem}_S_PB);'
rung_num += 1
# Extract RST devices for push button mappings
@ -222,7 +320,9 @@ class DescIPMainProgramGenerator:
def _create_desc_ip_estop_check_routine(self, routines_el: ET.Element) -> None:
"""Create R100_ESTOP_CHECK routine from DESC_IP extracted data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R100_ESTOP_CHECK', Type='RLL')
cfg_local = get_config()
estop_check_name = cfg_local.routines.name_map.get('estop_check', 'R100_ESTOP_CHECK')
routine = ET.SubElement(routines_el, 'Routine', Name=estop_check_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
@ -283,7 +383,7 @@ class FullMainProgramGenerator(LimitedMainProgramGenerator):
'ExportOptions': 'References NoRawData L5KData DecoratedData Context Dependencies ForceProtectedEncoding AllProjDocTrans',
})
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MTN6_MCM01')
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MTN6_MCM')
# Add empty AddOnInstructionDefinitions element
ET.SubElement(controller, 'AddOnInstructionDefinitions')
@ -307,35 +407,172 @@ class FullMainProgramGenerator(LimitedMainProgramGenerator):
routines_el = ET.SubElement(program, 'Routines')
print(" Creating full MainProgram routines...", flush=True)
self._create_main_routine_extended(routines_el)
print(" [SUCCESS] Created MainRoutine with DPM calls", flush=True)
self._create_safety_tag_map_routine(routines_el)
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, self.loader.zones)
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
# Add DPM routine
create_dpm_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R020_DPM routine", flush=True)
# Add FIOM routine
create_fiom_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R030_FIOM routine", flush=True)
# Add FIOH routine
create_fioh_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R031_FIOH routine", flush=True)
# Config-driven support: if routine_plan is provided, honor entries for MainProgram
cfg = get_config()
plan_entries = [
e for e in getattr(cfg, 'routine_plan', [])
if e.enabled and e.program == 'MainProgram'
]
plan_entries.sort(key=lambda e: e.order)
def run_defaults():
self._create_main_routine_extended(routines_el)
print(" [SUCCESS] Created MainRoutine with DPM calls", flush=True)
self._create_safety_tag_map_routine(routines_el)
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, None)
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
create_dpm_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R020_DPM routine", flush=True)
create_fiom_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R030_FIOM routine", flush=True)
create_fioh_routine(routines_el, self.loader.desc_ip)
print(" [SUCCESS] Created R031_FIOH routine", flush=True)
apf_routine_xml = generate_apf_routine(self.loader)
if apf_routine_xml:
routines_el.append(ET.fromstring(apf_routine_xml))
print(" [SUCCESS] Created R040_APF routine", flush=True)
extendo_routine_xml = generate_extendo_routine(self.loader)
if extendo_routine_xml:
routines_el.append(ET.fromstring(extendo_routine_xml))
print(" [SUCCESS] Created R041_EXTENDO routine", flush=True)
flow_ctrl_routine = generate_flow_control_routine(self.loader)
if flow_ctrl_routine is not None:
routines_el.append(flow_ctrl_routine)
print(" [SUCCESS] Created R050_FLOW_CTRL routine", flush=True)
speed_ctrl_routine = generate_speed_control_routine(self.loader)
if speed_ctrl_routine is not None:
routines_el.append(speed_ctrl_routine)
print(" [SUCCESS] Created R051_SPEED_CTRL routine", flush=True)
d2c_routine = generate_d2c_chute_routine(self.loader)
if d2c_routine is not None:
routines_el.append(d2c_routine)
print(" [SUCCESS] Created R042_D2C_CHUTE routine", flush=True)
pb_chute_routine = generate_pb_chute_routine(self.loader)
if pb_chute_routine is not None:
routines_el.append(pb_chute_routine)
print(" [SUCCESS] Created R043_PB_CHUTE routine", flush=True)
station_jr_chute_routine = generate_station_jr_chute_routine(self.loader)
if station_jr_chute_routine is not None:
routines_el.append(station_jr_chute_routine)
print(" [SUCCESS] Created R044_STATION_JR_CHUTE routine", flush=True)
station_jr_pb_routine = generate_station_jr_pb_routine(self.loader)
if station_jr_pb_routine is not None:
routines_el.append(station_jr_pb_routine)
print(" [SUCCESS] Created R090_STATION_JR_PB routine", flush=True)
jpe_routine = generate_jpe_routine(self.loader)
if jpe_routine is not None:
routines_el.append(jpe_routine)
print(" [SUCCESS] Created R100_JPE routine", flush=True)
fpe_routine = generate_fpe_routine(self.loader)
if fpe_routine is not None:
routines_el.append(fpe_routine)
print(" [SUCCESS] Created R101_FPE routine", flush=True)
pmm_routine = generate_pmm_routine(self.loader)
if pmm_routine is not None:
routines_el.append(pmm_routine)
print(" [SUCCESS] Created R060_PMM routine", flush=True)
cb_monitor_routine = generate_cb_monitor_routine(self.loader)
if cb_monitor_routine is not None:
routines_el.append(cb_monitor_routine)
print(" [SUCCESS] Created R070_CB_MONITOR routine", flush=True)
# Respect routine plan strictly: if a plan exists in config, do NOT fall back to defaults
if getattr(cfg, 'routine_plan', None):
if plan_entries:
# Helper to apply routine-level filters to a DataFrame
def _apply_filters(df: pd.DataFrame, flt: dict) -> pd.DataFrame:
if df is None or not isinstance(flt, dict) or df.empty:
return df
result = df
inc = flt.get('desca_include')
if inc and 'DESCA' in result.columns:
result = result[result['DESCA'].str.contains('|'.join(inc), case=False, na=False)]
exc = flt.get('desca_exclude')
if exc and 'DESCA' in result.columns:
result = result[~result['DESCA'].str.contains('|'.join(exc), case=False, na=False)]
inc_t = flt.get('tagname_include')
if inc_t and 'TAGNAME' in result.columns:
result = result[result['TAGNAME'].str.contains('|'.join(inc_t), case=False, na=False)]
exc_t = flt.get('tagname_exclude')
if exc_t and 'TAGNAME' in result.columns:
result = result[~result['TAGNAME'].str.contains('|'.join(exc_t), case=False, na=False)]
return result
generated = []
# Prepare plugin manager context once
from ..plugin_system import get_default_registry, RoutineManager, RoutineContext
plugin_context = RoutineContext(self.loader, cfg, routines_el, program, {'params': {}, 'filters': {}})
plugin_manager = RoutineManager(plugin_context, get_default_registry())
for entry in plan_entries:
plugin = entry.plugin
# Routine-specific filter set (from config)
flt = getattr(cfg, 'filters', None).for_routine(entry.name) if 'cfg' in locals() else {}
if plugin == 'main_routine':
self._create_main_routine_extended(routines_el)
generated.append('main_routine')
elif plugin == 'safety_tag_map':
self._create_safety_tag_map_routine(routines_el)
generated.append('safety_tag_map')
elif plugin == 'estop_check':
# Use plugin path for ESTOP_CHECK
plugin_context.metadata['params'] = entry.params or {}
plugin_context.metadata['filters'] = flt or {}
ok = plugin_manager.generate_routine('estop_check')
if ok:
generated.append('estop_check')
else:
# Fallback to direct call
epc_df = _apply_filters(self.loader.epc, flt)
sto_df = _apply_filters(self.loader.sto, flt)
create_estop_check_routine(routines_el, epc_df, sto_df, None)
generated.append('estop_check')
else:
# Plugin-based generation for all other entries
plugin_context.metadata['params'] = entry.params or {}
plugin_context.metadata['filters'] = flt or {}
ok = plugin_manager.generate_routine(plugin)
if ok:
generated.append(plugin)
else:
# Unknown/missing plugin skip
continue
print(f" [SUCCESS] Created MainProgram routines (config-driven): {', '.join(generated)}", flush=True)
else:
print(" [INFO] Routine plan present; no MainProgram routines enabled -> skipping defaults", flush=True)
else:
run_defaults()
return root
def _create_main_routine_extended(self, routines_el: ET.Element) -> None:
"""Create extended MainRoutine with JSR calls for all routines including DPM."""
routine = ET.SubElement(routines_el, 'Routine', Name='MainRoutine', Type='RLL')
from ..config import get_config
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
# Extended JSR calls including DPM, FIOM, and FIOH routines
text.text = '[JSR(R000_SAFETY_TAG_MAP,0) ,JSR(R020_DPM,0) ,JSR(R030_FIOM,0) ,JSR(R031_FIOH,0) ,JSR(R100_ESTOP_CHECK,0) ];'
# Extended JSR calls including DPM, FIOM, FIOH, APF, EXTENDO, and D2C routines using config names
nm = cfg_local.routines.name_map
calls = [
nm.get('safety_tag_map', 'R000_SAFETY_TAG_MAP'),
nm.get('dpm', 'R020_DPM'),
nm.get('fiom', 'R030_FIOM'),
nm.get('fioh', 'R031_FIOH'),
nm.get('apf', 'R040_APF'),
nm.get('extendo', 'R041_EXTENDO'),
nm.get('flow_ctrl', 'R050_FLOW_CTRL'),
nm.get('speed_ctrl', 'R051_SPEED_CTRL'),
nm.get('d2c_chute', 'R042_D2C_CHUTE'),
nm.get('pb_chute', 'R043_PB_CHUTE'),
nm.get('station_jr_chute', 'R044_STATION_JR_CHUTE'),
nm.get('pmm', 'R060_PMM'),
nm.get('cb_monitor', 'R070_CB_MONITOR'),
nm.get('station_jr_pb', 'R090_STATION_JR_PB'),
nm.get('jpe', 'R100_JPE'),
nm.get('fpe', 'R101_FPE'),
nm.get('estop_check', 'R100_ESTOP_CHECK'),
]
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'

View File

@ -2,16 +2,17 @@ from __future__ import annotations
import xml.etree.ElementTree as ET
from datetime import datetime
import re
from pathlib import Path
from typing import Optional, List, Dict
import pandas as pd
from ..data_loader import DataLoader
from ..config import get_config
from ..routines.inputs import create_inputs_routine
from ..routines.outputs import create_outputs_routine
from ..routines.resets import create_resets_routine
from ..routines.estops import create_estops_routine
from ..routines.zones import create_zones_routine
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..utils.tag_utils import device_base_from_desca
@ -30,7 +31,9 @@ def create_safety_tag_map(program_el: ET.Element, safety_tags: set[str], beacon_
print(" - WARNING: No safety tags found for SafetyTagMap.", flush=True)
return
map_text = ",".join(f"{tag}=SFT_{tag}" for tag in all_tags)
from ..config import get_config
prefix = get_config().routines.safety_tag_prefix
map_text = ",".join(f"{tag}={prefix}{tag}" for tag in all_tags)
safety_map = ET.SubElement(program_el, "SafetyTagMap")
safety_map.text = map_text
@ -55,21 +58,22 @@ class LimitedSafetyProgramGenerator:
else:
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
cfg = get_config()
root = ET.Element('RSLogix5000Content', attrib={
'SchemaRevision': '1.0',
'SoftwareRevision': '36.00',
'SchemaRevision': cfg.xml.schema_revision,
'SoftwareRevision': cfg.xml.software_revision,
'TargetName': 'SafetyProgram',
'TargetType': 'Program',
'TargetClass': 'Safety',
'ContainsContext': 'true',
'ExportDate': xl_time,
'ExportOptions': 'References NoRawData L5KData DecoratedData Context Dependencies ForceProtectedEncoding AllProjDocTrans',
'ExportOptions': cfg.xml.export_options,
})
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MTN6_MCM01')
controller = ET.SubElement(root, 'Controller', Use='Context', Name=cfg.xml.controller_name)
programs = ET.SubElement(controller, 'Programs', Use='Context')
program = ET.SubElement(programs, 'Program', Use='Target', Name='SafetyProgram',
TestEdits='false', MainRoutineName='MainRoutine', Disabled='false',
TestEdits='false', MainRoutineName=cfg.routines.main_routine_name, Disabled='false',
Class='Safety', UseAsFolder='false')
ET.SubElement(program, 'Tags')
routines_el = ET.SubElement(program, 'Routines')
@ -82,7 +86,7 @@ class LimitedSafetyProgramGenerator:
create_safety_tag_map(
program,
self.loader.safety_tags_from_pb,
set() # No beacon tags in limited mode
set()
)
print(" [SUCCESS] Created SafetyTagMap", flush=True)
@ -90,21 +94,96 @@ class LimitedSafetyProgramGenerator:
import re
excel_path_str = str(self.loader.excel_path)
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM01"
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM"
create_inputs_routine(routines_el, self.loader.epc, ignore_estop1ok=self.ignore_estop1ok)
print(" [SUCCESS] Created R010_INPUTS routine", flush=True)
create_resets_routine(routines_el, self.loader.rst, self.loader.epc, subsystem)
print(" [SUCCESS] Created R012_RESETS routine", flush=True)
# Config-driven support: if routine_plan is provided, honor entries for SafetyProgram
cfg = get_config()
plan_entries = [
e for e in getattr(cfg, 'routine_plan', [])
if e.enabled and e.program == 'SafetyProgram'
]
plan_entries.sort(key=lambda e: e.order)
# Only the 5 essential safety routines
create_outputs_routine(routines_el, self.loader.zones, self.loader.sto)
create_estops_routine(routines_el, self.loader.epc, self.loader.rst, subsystem, ignore_estop1ok=self.ignore_estop1ok)
print(" [SUCCESS] Created R020_ESTOPS routine", flush=True)
create_zones_routine(routines_el, self.loader.zones, self.loader.epc, ignore_estop1ok=self.ignore_estop1ok)
print(" [SUCCESS] Created R030_ZONES routine", flush=True)
def _apply_filters(df, flt: dict):
if df is None or not isinstance(flt, dict) or df.empty:
return df
result = df
# Global-style includes/excludes
inc = flt.get('desca_include')
if inc and 'DESCA' in result.columns:
result = result[result['DESCA'].str.contains('|'.join(inc), case=False, na=False)]
exc = flt.get('desca_exclude')
if exc and 'DESCA' in result.columns:
result = result[~result['DESCA'].str.contains('|'.join(exc), case=False, na=False)]
inc_t = flt.get('tagname_include')
if inc_t and 'TAGNAME' in result.columns:
result = result[result['TAGNAME'].str.contains('|'.join(inc_t), case=False, na=False)]
exc_t = flt.get('tagname_exclude')
if exc_t and 'TAGNAME' in result.columns:
result = result[~result['TAGNAME'].str.contains('|'.join(exc_t), case=False, na=False)]
# Column-specific filters: { columns: { COL: {include:[], exclude:[]} } }
cols = flt.get('columns', {}) or {}
for col, specs in cols.items():
if col in result.columns:
col_inc = specs.get('include') if isinstance(specs, dict) else None
col_exc = specs.get('exclude') if isinstance(specs, dict) else None
if col_inc:
result = result[result[col].astype(str).str.contains('|'.join(col_inc), case=False, na=False)]
if col_exc:
result = result[~result[col].astype(str).str.contains('|'.join(col_exc), case=False, na=False)]
# Drop duplicates by configured columns
dd = flt.get('drop_duplicates_by')
if dd:
result = result.drop_duplicates(subset=[c for c in dd if c in result.columns])
return result
def run_defaults():
# Apply global filters when no routine plan is defined
global_flt = getattr(cfg, 'filters', None).global_filters if 'cfg' in locals() else {}
epc = _apply_filters(self.loader.epc, global_flt or {})
rst = _apply_filters(self.loader.rst, global_flt or {})
sto = _apply_filters(self.loader.sto, global_flt or {})
create_inputs_routine(routines_el, epc, ignore_estop1ok=self.ignore_estop1ok)
print(" [SUCCESS] Created R010_INPUTS routine", flush=True)
create_resets_routine(routines_el, rst, epc, subsystem)
print(" [SUCCESS] Created R012_RESETS routine", flush=True)
create_outputs_routine(routines_el, pd.DataFrame(), sto)
create_estops_routine(routines_el, epc, rst, subsystem, ignore_estop1ok=self.ignore_estop1ok)
print(" [SUCCESS] Created R020_ESTOPS routine", flush=True)
# ZONES routine removed
# Respect routine plan strictly: if a plan exists in config, do NOT fall back to defaults
if getattr(cfg, 'routine_plan', None):
if plan_entries:
generated = []
for entry in plan_entries:
flt = getattr(cfg, 'filters', None).for_routine(entry.name) if 'cfg' in locals() else {}
if entry.plugin == 'inputs':
epc = _apply_filters(self.loader.epc, flt)
create_inputs_routine(routines_el, epc, ignore_estop1ok=self.ignore_estop1ok)
generated.append('inputs')
elif entry.plugin == 'resets':
rst = _apply_filters(self.loader.rst, flt)
epc = _apply_filters(self.loader.epc, flt)
create_resets_routine(routines_el, rst, epc, subsystem)
generated.append('resets')
elif entry.plugin == 'outputs':
sto = _apply_filters(self.loader.sto, flt)
create_outputs_routine(routines_el, pd.DataFrame(), sto)
generated.append('outputs')
elif entry.plugin == 'estops':
epc = _apply_filters(self.loader.epc, flt)
rst = _apply_filters(self.loader.rst, flt)
create_estops_routine(routines_el, epc, rst, subsystem, ignore_estop1ok=self.ignore_estop1ok)
generated.append('estops')
else:
continue
print(f" [SUCCESS] Created SafetyProgram routines (config-driven): {', '.join(generated)}", flush=True)
else:
print(" [INFO] Routine plan present; no SafetyProgram routines enabled -> skipping defaults", flush=True)
else:
run_defaults()
ET.SubElement(controller, 'SafetySignaturesHmac')
@ -112,12 +191,21 @@ class LimitedSafetyProgramGenerator:
@staticmethod
def _create_main_routine(routines_el: ET.Element) -> None:
routine = ET.SubElement(routines_el, 'Routine', Name='MainRoutine', Type='RLL')
from ..config import get_config
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
# Only JSR calls for the 5 essential safety routines
text.text = '[JSR(R010_INPUTS,0) ,JSR(R011_OUTPUTS,0) ,JSR(R012_RESETS,0) ,JSR(R020_ESTOPS,0) ,JSR(R030_ZONES,0) ];'
# Only JSR calls for the 5 essential safety routines (config-driven names)
nm = cfg_local.routines.name_map
calls = [
nm.get('inputs', 'R010_INPUTS'),
nm.get('outputs', 'R011_OUTPUTS'),
nm.get('resets', 'R012_RESETS'),
nm.get('estops', 'R020_ESTOPS'),
]
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'
# ------------------------------------------------------------------
def build_xml_string(self) -> str:
@ -171,27 +259,33 @@ class DescIPSafetyProgramGenerator:
self._create_desc_ip_resets_routine(routines_el, rst_data)
self._create_desc_ip_estops_routine(routines_el, epc_data)
# Use zones from file if available (zones typically aren't extracted from DESC_IP)
try:
zones_data = self.loader.zones
self._create_desc_ip_zones_routine(routines_el, epc_data)
except Exception as e:
print(f" - Warning: Could not load zones data: {e}")
# ZONES routine removed
return root
@staticmethod
def _create_main_routine(routines_el: ET.Element) -> None:
routine = ET.SubElement(routines_el, 'Routine', Name='MainRoutine', Type='RLL')
from ..config import get_config
cfg_local = get_config()
routine = ET.SubElement(routines_el, 'Routine', Name=cfg_local.routines.main_routine_name, Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
text = ET.SubElement(rung, 'Text')
# Only JSR calls for essential safety routines
text.text = '[JSR(R010_INPUTS,0) ,JSR(R011_OUTPUTS,0) ,JSR(R012_RESETS,0) ,JSR(R020_ESTOPS,0) ,JSR(R030_ZONES,0) ];'
# Only JSR calls for essential safety routines (config-driven names)
nm = cfg_local.routines.name_map
calls = [
nm.get('inputs', 'R010_INPUTS'),
nm.get('outputs', 'R011_OUTPUTS'),
nm.get('resets', 'R012_RESETS'),
nm.get('estops', 'R020_ESTOPS'),
]
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'
def _create_desc_ip_inputs_routine(self, routines_el: ET.Element, epc_data: pd.DataFrame) -> None:
"""Create R010_INPUTS routine from DESC_IP extracted EPC data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R010_INPUTS', Type='RLL')
from ..config import get_config
nm = get_config().routines.name_map
routine = ET.SubElement(routines_el, 'Routine', Name=nm.get('inputs', 'R010_INPUTS'), Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
@ -217,14 +311,13 @@ class DescIPSafetyProgramGenerator:
def _create_desc_ip_outputs_routine(self, routines_el: ET.Element, sto_data: pd.DataFrame) -> None:
"""Create R011_OUTPUTS routine from DESC_IP extracted STO data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R011_OUTPUTS', Type='RLL')
from ..config import get_config
nm = get_config().routines.name_map
routine = ET.SubElement(routines_el, 'Routine', Name=nm.get('outputs', 'R011_OUTPUTS'), Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
# Get zones data to determine proper zone OK tags
zones_df = self.data_loader.zones
# Process STO data for safety outputs
for _, row in sto_data.iterrows():
if pd.notna(row['IO_PATH']) and 'VFD' in str(row['TAGNAME']):
@ -233,13 +326,8 @@ class DescIPSafetyProgramGenerator:
vfd_name = str(row['TAGNAME'])
sto_tag = f"STO_{vfd_name}"
# Use proper zone OK tag - for MCM zone it should be EStop_MCM_OK
zone_ok_tag = "EStop_MCM_OK" # Default to MCM zone
if not zones_df.empty:
# Get the first zone name and create proper tag
first_zone = zones_df.iloc[0]
zone_name = first_zone["NAME"].replace(" ", "_").replace("-", "_")
zone_ok_tag = f"EStop_{zone_name}_OK"
# Zone logic removed: gate by MCM OK
zone_ok_tag = "EStop_MCM_OK"
text.text = f"XIC({zone_ok_tag})OTE({sto_tag});"
rung_num += 1
@ -248,7 +336,9 @@ class DescIPSafetyProgramGenerator:
def _create_desc_ip_resets_routine(self, routines_el: ET.Element, rst_data: pd.DataFrame) -> None:
"""Create R012_RESETS routine from DESC_IP extracted RST data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R012_RESETS', Type='RLL')
from ..config import get_config
nm = get_config().routines.name_map
routine = ET.SubElement(routines_el, 'Routine', Name=nm.get('resets', 'R012_RESETS'), Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
@ -268,7 +358,9 @@ class DescIPSafetyProgramGenerator:
def _create_desc_ip_estops_routine(self, routines_el: ET.Element, epc_data: pd.DataFrame) -> None:
"""Create R020_ESTOPS routine from DESC_IP extracted EPC data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R020_ESTOPS', Type='RLL')
from ..config import get_config
nm = get_config().routines.name_map
routine = ET.SubElement(routines_el, 'Routine', Name=nm.get('estops', 'R020_ESTOPS'), Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
rung_num = 0
@ -285,42 +377,7 @@ class DescIPSafetyProgramGenerator:
print(f" - Added {rung_num} E-stop rungs from DESC_IP data", flush=True)
def _create_desc_ip_zones_routine(self, routines_el: ET.Element, epc_data: pd.DataFrame) -> None:
"""Create R030_ZONES routine from DESC_IP extracted data."""
routine = ET.SubElement(routines_el, 'Routine', Name='R030_ZONES', Type='RLL')
rll_content = ET.SubElement(routine, 'RLLContent')
# Get zones data from data loader
zones_df = self.data_loader.zones
rung_num = 0
# Handle MCM zone first (special case)
mcm_zone = zones_df[zones_df["NAME"].isin(["MCM", "MCM01"])]
if not mcm_zone.empty:
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = "XIC(MCM_EPB_DCS_CTRL.O1)OTE(EStop_MCM_OK);"
rung_num += 1
# Handle other zones if any
for _, zone in zones_df.sort_values("NAME").iterrows():
# Skip MCM zones (already handled above)
if zone["NAME"] in ["MCM", "MCM01"]:
continue
if pd.isna(zone["START"]) or pd.isna(zone["END"]) or zone["START"] == "" or zone["END"] == "":
continue
zone_name = zone["NAME"].replace(" ", "_").replace("-", "_")
zone_ok_tag = f"EStop_{zone_name}_OK"
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
text = ET.SubElement(rung, 'Text')
# This would need more sophisticated logic for actual device ranges
text.text = f"XIC(SYSTEM_OK)OTE({zone_ok_tag});"
rung_num += 1
print(f" - Added {rung_num} zone logic rungs from zones data", flush=True)
# ZONES routine removed
def build_xml_string(self) -> str:
"""Build and return the complete XML string."""

View File

@ -42,6 +42,10 @@ class RoutinePlugin(ABC):
def __init__(self, context: RoutineContext):
self.context = context
self.logger = get_logger(f"{self.__class__.__module__}.{self.__class__.__name__}")
# Extract optional per-plugin parameters and filters from context metadata
meta = context.metadata or {}
self.params: Dict[str, Any] = meta.get('params', {})
self.filters: Dict[str, Any] = meta.get('filters', {})
@abstractmethod
def can_generate(self) -> bool:
@ -236,7 +240,12 @@ def get_default_registry() -> RoutineRegistry:
"""Get the default plugin registry, creating it if necessary."""
global _default_registry
if _default_registry is None:
_default_registry = RoutineRegistry()
# Auto-discover plugins in src.routines
try:
_default_registry = auto_discover_plugins("src.routines")
except Exception:
# Fallback to empty registry if discovery fails
_default_registry = RoutineRegistry()
return _default_registry
def register_plugin(plugin_class: Type[RoutinePlugin]) -> None:

View File

@ -0,0 +1,116 @@
"""
Generate R040_APF routine from DESC_IP data.
"""
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_apf_routine(data_loader) -> str:
"""Generate R040_APF routine from NETWORK sheet data.
Args:
data_loader: DataLoader instance with access to NETWORK sheet
Returns:
Formatted XML string for R040_APF routine
"""
# Get APF data from NETWORK sheet
apf_df = data_loader.apf
# Sort by Name for deterministic output
if 'Name' in apf_df.columns:
apf_df = apf_df.sort_values('Name', key=lambda x: [natural_sort_key(name) for name in x])
else:
# This shouldn't happen with NETWORK sheet data
return format_xml_to_match_original(ET.tostring(ET.Element("Routine", attrib={"Name": "R040_APF", "Type": "RLL"}), encoding='unicode'))
# Create routine XML structure (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('apf', 'R040_APF')
except Exception:
routine_name = 'R040_APF'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Comment rung
rung0 = ET.SubElement(rll_content, "Rung", attrib={"Number": "0", "Type": "N"})
comment = ET.SubElement(rung0, "Comment")
comment.text = """APF (Variable Frequency Drive) Instantiation Routine
These VFDs are connected to the DPMs for their Ethernet Communication"""
text0 = ET.SubElement(rung0, "Text")
text0.text = "NOP();"
# Generate AOI_APF calls for each module
rung_number = 1
for _, apf_row in apf_df.iterrows():
# APF name comes from Name column in NETWORK sheet
if 'Name' not in apf_row or pd.isna(apf_row['Name']):
continue
apf_name = str(apf_row['Name']).strip()
if not apf_name:
continue
# Extract DPM name from DPM column (from NETWORK sheet)
dpm_name = None
if 'DPM' in apf_row and pd.notna(apf_row['DPM']) and str(apf_row['DPM']).strip():
dpm_name = str(apf_row['DPM']).strip()
else:
# This should not happen with proper NETWORK sheet data
print(f"Warning: No DPM specified for APF {apf_name}, using MCM")
dpm_name = "MCM"
print(f" APF {apf_name} -> DPM {dpm_name}")
# Create rung for this APF module
rung = ET.SubElement(rll_content, "Rung", attrib={"Number": str(rung_number), "Type": "N"})
text = ET.SubElement(rung, "Text")
# Generate AOI_APF call with MOVE instruction
# Pattern: AOI_APF({APF}.AOI,{APF}.HMI,{APF}.CTRL,{APF},{APF}:I,{APF}:O,MCM01.CTRL,{DPM}.CTRL,{APF}:I.In_0,1,NO_Horn)MOVE({APF}.CTRL.STS.Log,{APF}.CTRL.STS.Log);
aoi_call = f"AOI_APF({apf_name}.AOI,{apf_name}.HMI,{apf_name}.CTRL,{apf_name},{apf_name}:I,{apf_name}:O,MCM.CTRL,{dpm_name}.CTRL,{apf_name}:I.In_0,1,NO_Horn)MOVE({apf_name}.CTRL.STS.Log,{apf_name}.CTRL.STS.Log);"
text.text = aoi_call
rung_number += 1
# Convert to string and format to match Rockwell L5X format
xml_str = ET.tostring(routine, encoding='unicode')
return format_xml_to_match_original(xml_str)
def _get_apf_data(data_loader) -> pd.DataFrame:
"""Helper to get APF data from DataLoader."""
return data_loader.apf
def append_apf_routine(routines_element: ET.Element, data_loader):
"""Append APF routine to routines element."""
routine_content = generate_apf_routine(data_loader)
routine_element = ET.fromstring(routine_content)
# Append to routines element
routines_element.append(routine_element)
class ApfRoutinePlugin(RoutinePlugin):
name = "apf"
description = "Generates the APF routine"
category = "device"
def can_generate(self) -> bool:
return not self.context.data_loader.apf.empty
def generate(self) -> bool:
xml_text = generate_apf_routine(self.context.data_loader)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
register_plugin(ApfRoutinePlugin)

View File

@ -0,0 +1,83 @@
"""
CB_MONITOR routine generator.
Generates AOI_CB_MONITOR calls for circuit breaker monitoring at PDPs.
"""
import xml.etree.ElementTree as ET
from typing import Any, Dict
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_cb_monitor_routine(data_loader) -> ET.Element:
"""Generate the R070_CB_MONITOR routine XML element."""
cb_monitor_data = data_loader.cb_monitor_data
# Config-driven routine name
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('cb_monitor', 'R070_CB_MONITOR')
except Exception:
routine_name = 'R070_CB_MONITOR'
routine = ET.Element('Routine', Name=routine_name, Type='RLL')
# Create RLLContent
rll_content = ET.SubElement(routine, 'RLLContent')
# Rung 0: Comment explaining the routine
rung0 = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
comment0 = ET.SubElement(rung0, 'Comment')
comment0.text = ('CB (Circuit Breaker) Monitoring Routine\n\n'
'This routine monitors the status of up to 26 circuit breakers per PDP.')
text0 = ET.SubElement(rung0, 'Text')
text0.text = 'NOP();'
# Generate AOI calls for each PDP
rung_number = 1
for pdp_name in sorted(cb_monitor_data.keys(), key=natural_sort_key):
config = cb_monitor_data[pdp_name]
# Create rung for this CB_MONITOR
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
# Build AOI_CB_MONITOR call
# Format: AOI_CB_MONITOR(tag.AOI, tag.HMI, tag.CTRL, MCM.CTRL, ConnectionFault, CB1...CB26)
cb_params = ','.join(config['cb_inputs'])
# Note: Using {pdp_name}_CB_MONITOR as the tag name to match the boilerplate
tag_name = f"{pdp_name}_CB_MONITOR"
aoi_call = (f"AOI_CB_MONITOR({tag_name}.AOI,"
f"{tag_name}.HMI,"
f"{tag_name}.CTRL,"
f"MCM.CTRL,"
f"{config['connection_fault']},"
f"{cb_params});")
text.text = aoi_call
rung_number += 1
# Convert to string and format
xml_str = ET.tostring(routine, encoding='unicode')
formatted_xml = format_xml_to_match_original(xml_str)
return ET.fromstring(formatted_xml)
class CbMonitorRoutinePlugin(RoutinePlugin):
name = "cb_monitor"
description = "Generates the CB_MONITOR routine"
category = "device"
def can_generate(self) -> bool:
return bool(self.context.data_loader.cb_monitor_data)
def generate(self) -> bool:
elem = generate_cb_monitor_routine(self.context.data_loader)
if elem is None:
return False
self.context.routines_element.append(elem)
return True
register_plugin(CbMonitorRoutinePlugin)

View File

@ -0,0 +1,184 @@
"""
R042_D2C_CHUTE routine generator
Generates D2C_CHUTE routine with AOI_D2C_CHUTE calls grouped by S0 numbers.
Each S0 group includes ZMX device, DPM controller, FIO I/O points, and beacon configurations.
"""
import xml.etree.ElementTree as ET
import pandas as pd
import re
from typing import Dict, List, Optional, Tuple
from ..plugin_system import RoutinePlugin, register_plugin
def extract_d2c_data(desc_ip_df: pd.DataFrame, network_df: pd.DataFrame) -> Dict[str, Dict]:
"""
Extract D2C-related data for each unique S0 number.
Returns dict with structure:
{
'S012001': {
'gs1_pb': {'io_path': 'VSB_DPM1_FIO1:I.Pt04.Data', 'fio': 'VSB_DPM1_FIO1'},
'gs1_pb_lt': {'io_path': 'VSB_DPM1_FIO1:O.Pt05.Data'},
'bcn': {'tagname': 'VSB_DPM1_FIO1', 'stack_type': '2 STACK'},
'zmx': {'name': 'S012001_ZMX1', 'dpm': 'VSB_DPM1'}
}
}
"""
# Extract unique S0 numbers
s0_pattern = r'(S0\d+)'
s0_entries = desc_ip_df[desc_ip_df['DESCA'].str.contains('S0', na=False)]
s0_numbers = s0_entries['DESCA'].str.extract(s0_pattern)[0].dropna().unique()
s0_numbers = sorted(set(s0_numbers))
s0_data = {}
for s0 in s0_numbers:
s0_data[s0] = {}
# Find GS1_PB entries (not _LT)
gs_pb = desc_ip_df[
desc_ip_df['DESCA'].str.contains(f'{s0}_GS1_PB', na=False) &
~desc_ip_df['DESCA'].str.contains('_LT', na=False)
]
if not gs_pb.empty:
row = gs_pb.iloc[0]
io_path = row['IO_PATH']
fio = io_path.split(':')[0] if ':' in io_path else ''
s0_data[s0]['gs1_pb'] = {
'io_path': io_path,
'fio': fio
}
# Find GS1_PB_LT entries
gs_pb_lt = desc_ip_df[desc_ip_df['DESCA'].str.contains(f'{s0}_GS1_PB_LT', na=False)]
if not gs_pb_lt.empty:
s0_data[s0]['gs1_pb_lt'] = {
'io_path': gs_pb_lt.iloc[0]['IO_PATH']
}
# Find BCN entries
bcn = desc_ip_df[desc_ip_df['DESCA'].str.contains(f'{s0}_BCN', na=False)]
if not bcn.empty:
bcn_row = bcn.iloc[0]
stack_type = "3 STACK" if "3 STACK" in bcn_row['DESCA'] else "2 STACK"
s0_data[s0]['bcn'] = {
'tagname': bcn_row['TAGNAME'],
'stack_type': stack_type
}
# Find ZMX in NETWORK
zmx = network_df[network_df['Name'].str.contains(f'{s0}_ZMX', na=False)]
if not zmx.empty:
zmx_row = zmx.iloc[0]
s0_data[s0]['zmx'] = {
'name': zmx_row['Name'],
'dpm': zmx_row['DPM']
}
# Filter to only include S0s with all required components
complete_s0_data = {
s0: data for s0, data in s0_data.items()
if all(key in data for key in ['gs1_pb', 'gs1_pb_lt', 'bcn', 'zmx'])
}
return complete_s0_data
def generate_d2c_chute_routine(data_loader) -> ET.Element:
"""Generate the R042_D2C_CHUTE routine XML element."""
# Get D2C data from DataLoader
d2c_data = data_loader.d2c_data
# Create routine element (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('d2c_chute', 'R042_D2C_CHUTE')
except Exception:
routine_name = 'R042_D2C_CHUTE'
routine = ET.Element('Routine', Name=routine_name, Type='RLL')
description = ET.SubElement(routine, 'Description')
description.text = ''#'D2C Chute Control - Divert to Chute logic for S0 stations'
# Create RLLContent
rll_content = ET.SubElement(routine, 'RLLContent')
rung_number = 0
# Group S0s by their properties for efficient rung generation
for s0, data in sorted(d2c_data.items()):
rung_number += 1
# Create rung
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
comment = ET.SubElement(rung, 'Comment')
comment.text = f'{s0} - Divert to Chute Control'
text = ET.SubElement(rung, 'Text')
# Build AOI_D2C_CHUTE call
zmx_name = data['zmx']['name']
dpm_name = data['zmx']['dpm']
gs_pb_path = data['gs1_pb']['io_path']
gs_pb_lt_path = data['gs1_pb_lt']['io_path']
bcn_tagname = data['bcn']['tagname']
stack_type = data['bcn']['stack_type']
# Build beacon parameters based on stack type
if stack_type == "3 STACK":
# For 3-stack: use segments 1 and 3
beacon_params = (
f"{bcn_tagname}:O.ProcessDataOut.Segment_1_Color_1,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_1_Animation_Type,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_3_Color_1,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_3_Animation_Type"
)
else:
# For 2-stack: use segments 1 and 2
beacon_params = (
f"{bcn_tagname}:O.ProcessDataOut.Segment_1_Color_1,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_1_Animation_Type,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_2_Color_1,"
f"{bcn_tagname}:O.ProcessDataOut.Segment_2_Animation_Type"
)
# Build complete AOI call
aoi_call = (
f"AOI_D2C_CHUTE("
f"{s0}_D2C_CHUTE.AOI,"
f"{s0}_D2C_CHUTE.HMI,"
f"{s0}_D2C_CHUTE.CTRL,"
f"Station.CTRL," # Using Station as placeholder for JR
f"{zmx_name}:I.Data,"
f"{zmx_name}:O.Data,"
f"{dpm_name}.CTRL,"
f"{gs_pb_path},"
f"{gs_pb_lt_path},"
f"{beacon_params}"
");"
)
text.text = aoi_call
return routine
class D2cChuteRoutinePlugin(RoutinePlugin):
name = "d2c_chute"
description = "Generates the D2C_CHUTE routine"
category = "device"
def can_generate(self) -> bool:
return bool(self.context.data_loader.d2c_data)
def generate(self) -> bool:
elem = generate_d2c_chute_routine(self.context.data_loader)
if elem is None:
return False
self.context.routines_element.append(elem)
return True
register_plugin(D2cChuteRoutinePlugin)

View File

@ -7,6 +7,7 @@ DPM modules are identified by PARTNUMBER containing 'OS30-002404-2S'.
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_dpm_routine(desc_ip_df: pd.DataFrame) -> str:
@ -26,8 +27,13 @@ def generate_dpm_routine(desc_ip_df: pd.DataFrame) -> str:
dpm_df = dpm_df.drop_duplicates(subset=['TAGNAME'])
dpm_df = dpm_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
# Create routine XML structure
routine = ET.Element("Routine", attrib={"Name": "R020_DPM", "Type": "RLL"})
# Create routine XML structure (name from config)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('dpm', 'R020_DPM')
except Exception:
routine_name = 'R020_DPM'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Documentation comment
@ -57,8 +63,7 @@ VFDs, and Field IO are connected to the switch that is part of the DPM"""
text = ET.SubElement(rung, "Text")
# Generate AOI_DPM call with standard parameters
# Pattern: AOI_DPM({TAGNAME}.AOI,{TAGNAME}.HMI,{TAGNAME}.CTRL,MCM01.CTRL,Rack.AOI.Slot2_EN4TR_Faulted,{TAGNAME}:I.ConnectionFaulted);
aoi_call = f"AOI_DPM({tagname}.AOI,{tagname}.HMI,{tagname}.CTRL,MCM01.CTRL,Rack.AOI.Slot2_EN4TR_Faulted,{tagname}:I.ConnectionFaulted);"
aoi_call = f"AOI_DPM({tagname}.AOI,{tagname}.HMI,{tagname}.CTRL,MCM.CTRL,Rack.AOI.Slot2_EN4TR_Faulted,{tagname}:I.ConnectionFaulted);"
text.text = aoi_call
rung_number += 1
@ -104,4 +109,27 @@ def extract_dpm_from_desc_ip(desc_ip_df: pd.DataFrame) -> pd.DataFrame:
# Sort for deterministic output
dpm_df = dpm_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
return dpm_df
return dpm_df
class DpmRoutinePlugin(RoutinePlugin):
"""Plugin wrapper for DPM routine generation."""
name = "dpm"
description = "Generates the DPM routine"
category = "device"
def can_generate(self) -> bool:
# Ensure there is at least one DPM entry
return not self.context.data_loader.dpm.empty
def generate(self) -> bool:
desc_ip_df = self.context.data_loader.desc_ip
xml_text = generate_dpm_routine(desc_ip_df)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
# Register plugin in default registry
register_plugin(DpmRoutinePlugin)

View File

@ -11,6 +11,7 @@ from ..utils.tag_utils import (
device_base_from_desca,
is_estop_desca,
)
from ..plugin_system import RoutinePlugin, register_plugin
__all__ = ["create_estop_check_routine"]
@ -30,49 +31,54 @@ def _collect_all_stos(sto_df: pd.DataFrame) -> list[tuple[str, str]]:
return all_stos
def _split_stos_by_zone(
def _split_stos(
all_stos: list[tuple[str, str]],
device_zone: pd.Series,
device_prefix: str,
device_num: int,
) -> tuple[list[str], list[str]]:
"""Return (zone_stos, other_stos) IO paths based on zone boundaries."""
start_prefix = device_zone["START"].split("_")[0]
start_num = int(device_zone["START"].split("_")[1])
end_prefix = device_zone["END"].split("_")[0]
end_num = int(device_zone["END"].split("_")[1])
"""Split STOs into same-device-group vs other-device-group based on prefix/number proximity.
zone_stos: list[str] = []
other_stos: list[str] = []
Heuristic: same group if same prefix and |sto_num - device_num| <= 50. Otherwise other.
This replaces explicit zone ranges.
"""
same_group: list[str] = []
other_group: list[str] = []
for sto_tag, sto_path in all_stos:
sto_parts = sto_tag.split("_")
sto_prefix = sto_parts[0]
sto_num_part = sto_parts[1]
sto_num = int(sto_num_part[:-1]) if sto_num_part[-1] in ["A", "B"] else int(sto_num_part)
in_zone = False
if start_prefix == end_prefix:
if sto_prefix == start_prefix and start_num <= sto_num <= end_num:
in_zone = True
parts = sto_tag.split('_')
if len(parts) < 2:
other_group.append(sto_path)
continue
sto_prefix = parts[0]
num_part = parts[1]
try:
sto_num = int(num_part[:-1]) if num_part[-1] in ('A', 'B') else int(num_part)
except ValueError:
other_group.append(sto_path)
continue
if sto_prefix == device_prefix and abs(sto_num - device_num) <= 50:
same_group.append(sto_path)
else:
if (
(sto_prefix == start_prefix and sto_num >= start_num)
or (sto_prefix == end_prefix and sto_num <= end_num)
or (start_prefix < sto_prefix < end_prefix)
):
in_zone = True
(zone_stos if in_zone else other_stos).append(sto_path)
return zone_stos, other_stos
other_group.append(sto_path)
return same_group, other_group
def create_estop_check_routine(
routines: ET.Element,
epc_df: pd.DataFrame,
sto_df: pd.DataFrame,
zones_df: pd.DataFrame,
zones_df: pd.DataFrame | None,
) -> None:
"""Append the ESTOP_CHECK routine (R100_ESTOP_CHECK) to the `<Routines>` element."""
# Use config-driven routine name
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('estop_check', 'R100_ESTOP_CHECK')
except Exception:
routine_name = 'R100_ESTOP_CHECK'
routine = ET.SubElement(routines, "Routine")
routine.set("Name", "R100_ESTOP_CHECK")
routine.set("Name", routine_name)
routine.set("Type", "RLL")
rll_content = ET.SubElement(routine, "RLLContent")
@ -91,48 +97,10 @@ def create_estop_check_routine(
except (ValueError, IndexError):
continue # Skip devices that don't follow numeric pattern
# Identify the zone this device belongs to
device_zone = None
for _, zone in zones_df.iterrows():
# Skip MCM zone and zones that don't have proper START/END values
if zone["NAME"] in ["MCM", "MCM01"]:
continue
if pd.isna(zone["START"]) or pd.isna(zone["END"]) or zone["START"] == "" or zone["END"] == "":
continue
try:
start_parts = zone["START"].split("_")
end_parts = zone["END"].split("_")
if len(start_parts) < 2 or len(end_parts) < 2:
continue
start_prefix = start_parts[0]
start_num = int(start_parts[1])
end_prefix = end_parts[0]
end_num = int(end_parts[1])
except (ValueError, IndexError):
continue # Skip zones with non-numeric device patterns
in_zone = False
if start_prefix == end_prefix:
if device_prefix == start_prefix and start_num <= device_num <= end_num:
in_zone = True
else:
if (
(device_prefix == start_prefix and device_num >= start_num)
or (device_prefix == end_prefix and device_num <= end_num)
or (start_prefix < device_prefix < end_prefix)
):
in_zone = True
if in_zone:
device_zone = zone
break
if device_zone is None:
continue
# Determine related STOs without zones (heuristic proximity)
zone_stos, other_stos = _split_stos(all_stos, device_prefix, device_num)
estop_device = is_estop_desca(group.iloc[0]["DESCA"])
zone_stos, other_stos = _split_stos_by_zone(all_stos, device_zone)
# Deduplicate STOs to ensure each appears only once
zone_stos = sorted(list(set(zone_stos)))
@ -184,4 +152,25 @@ def create_estop_check_routine(
text.text = f"{raw_inputs}{sto_checks}OTL({checked_tag});"
rung_num += 1
print(f" - Added {rung_num} E-stop check rungs", flush=True)
print(f" - Added {rung_num} E-stop check rungs", flush=True)
class EstopCheckRoutinePlugin(RoutinePlugin):
name = "estop_check"
description = "Generates the ESTOP_CHECK routine"
category = "safety"
def can_generate(self) -> bool:
return not self.context.data_loader.epc.empty and not self.context.data_loader.sto.empty
def generate(self) -> bool:
create_estop_check_routine(
self.context.routines_element,
self.context.data_loader.epc,
self.context.data_loader.sto,
None,
)
return True
register_plugin(EstopCheckRoutinePlugin)

View File

@ -34,7 +34,7 @@ def create_estops_routine(routines: ET.Element, epc_df: pd.DataFrame, rst_df: pd
text = ET.SubElement(rung, "Text")
text.text = (
f"DCS(MCM_EPB_DCS_CTRL,EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
f"AUTOMATIC,Local:7:I.Pt02.Data,Local:7:I.Pt03.Data,MCM_EPB_STATUS,SFT_{subsystem}_S_PB);"
f"AUTOMATIC,Local:7:I.Pt02.Data,Local:7:I.Pt03.Data,MCM_EPB_STATUS,SFT_MCM_S_PB);"
)
rung_num = 1
@ -69,7 +69,7 @@ def create_estops_routine(routines: ET.Element, epc_df: pd.DataFrame, rst_df: pd
if not rst_row.empty:
reset_tag = f"SFT_{rst_row.iloc[0]['DESCA']}"
else:
reset_tag = f"SFT_{subsystem}_S_PB"
reset_tag = f"SFT_MCM_S_PB"
text.text = (
f"DCS({dcs_tag},EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
@ -111,4 +111,36 @@ def create_estops_routine(routines: ET.Element, epc_df: pd.DataFrame, rst_df: pd
f"DCS({dcs_tag},EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
f"AUTOMATIC,{input1},{input2},{status_tag},{reset_tag});"
)
rung_num += 1
rung_num += 1
# --- Plugin wrapper so modern generator can execute this routine ---
from ..plugin_system import RoutinePlugin
class EstopsRoutinePlugin(RoutinePlugin):
name = "estops"
description = "Generates the R020_ESTOPS routine"
category = "safety"
def can_generate(self) -> bool:
try:
return not self.context.data_loader.epc.empty
except Exception:
return False
def generate(self) -> bool:
# Determine subsystem from excel path
import re
excel_path_str = str(self.context.data_loader.excel_path)
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
subsystem = subsystem_match.group(1).upper() if subsystem_match else "MCM01"
params = self.context.metadata.get("params", {}) if isinstance(self.context.metadata, dict) else {}
ignore_flag = bool(params.get("ignore_estop1ok", False))
create_estops_routine(
self.context.routines_element,
self.context.data_loader.epc,
self.context.data_loader.rst,
subsystem=subsystem,
ignore_estop1ok=ignore_flag,
)
return True

View File

@ -0,0 +1,116 @@
"""
Generate R041_EXTENDO routine from NETWORK sheet data.
"""
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_extendo_routine(data_loader) -> str:
"""Generate R041_EXTENDO routine from NETWORK sheet data.
Args:
data_loader: DataLoader instance with access to NETWORK sheet
Returns:
Formatted XML string for R041_EXTENDO routine
"""
# Get EXTENDO data from NETWORK sheet
extendo_df = data_loader.extendo
# Sort by Name for deterministic output
if 'Name' in extendo_df.columns:
extendo_df = extendo_df.sort_values('Name', key=lambda x: [natural_sort_key(name) for name in x])
else:
# This shouldn't happen with NETWORK sheet data
return format_xml_to_match_original(ET.tostring(ET.Element("Routine", attrib={"Name": "R041_EXTENDO", "Type": "RLL"}), encoding='unicode'))
# Create routine XML structure (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('extendo', 'R041_EXTENDO')
except Exception:
routine_name = 'R041_EXTENDO'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Comment rung
rung0 = ET.SubElement(rll_content, "Rung", attrib={"Number": "0", "Type": "N"})
comment = ET.SubElement(rung0, "Comment")
comment.text = """EXTENDO (Siemens ET 200SP) Instantiation Routine
These Extendo modules are connected to the DPMs for their Ethernet Communication"""
text0 = ET.SubElement(rung0, "Text")
text0.text = "NOP();"
# Generate AOI_EXTENDO calls for each module
rung_number = 1
for _, extendo_row in extendo_df.iterrows():
# EXTENDO name comes from Name column in NETWORK sheet
if 'Name' not in extendo_row or pd.isna(extendo_row['Name']):
continue
extendo_name = str(extendo_row['Name']).strip()
if not extendo_name:
continue
# Extract DPM name from DPM column (from NETWORK sheet)
dpm_name = None
if 'DPM' in extendo_row and pd.notna(extendo_row['DPM']) and str(extendo_row['DPM']).strip():
dpm_name = str(extendo_row['DPM']).strip()
else:
# This should not happen with proper NETWORK sheet data
print(f"Warning: No DPM specified for EXTENDO {extendo_name}, using MCM")
dpm_name = "MCM"
print(f" EXTENDO {extendo_name} -> DPM {dpm_name}")
# Create rung for this EXTENDO module
rung = ET.SubElement(rll_content, "Rung", attrib={"Number": str(rung_number), "Type": "N"})
text = ET.SubElement(rung, "Text")
# Generate AOI_EXTENDO call
# Pattern: AOI_EXTENDO({EXTENDO}.AOI,{EXTENDO}.HMI,{EXTENDO}.CTRL,{EXTENDO}:I,{EXTENDO}:O,MCM01.CTRL,{DPM}.CTRL);
aoi_call = f"AOI_EXTENDO({extendo_name}.AOI,{extendo_name}.HMI,{extendo_name}.CTRL,{extendo_name}:I,{extendo_name}:O,MCM.CTRL,{dpm_name}.CTRL);"
text.text = aoi_call
rung_number += 1
# Convert to string and format to match Rockwell L5X format
xml_str = ET.tostring(routine, encoding='unicode')
return format_xml_to_match_original(xml_str)
def _get_extendo_data(data_loader) -> pd.DataFrame:
"""Helper to get EXTENDO data from DataLoader."""
return data_loader.extendo
def append_extendo_routine(routines_element: ET.Element, data_loader):
"""Append EXTENDO routine to routines element."""
routine_content = generate_extendo_routine(data_loader)
routine_element = ET.fromstring(routine_content)
# Append to routines element
routines_element.append(routine_element)
class ExtendoRoutinePlugin(RoutinePlugin):
name = "extendo"
description = "Generates the EXTENDO routine"
category = "device"
def can_generate(self) -> bool:
return not self.context.data_loader.extendo.empty
def generate(self) -> bool:
xml_text = generate_extendo_routine(self.context.data_loader)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
register_plugin(ExtendoRoutinePlugin)

View File

@ -7,6 +7,7 @@ FIOH modules are identified by PARTNUMBER containing '5032-8IOLM12DR' and DESCA
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_fioh_routine(desc_ip_df: pd.DataFrame) -> str:
@ -36,8 +37,13 @@ def generate_fioh_routine(desc_ip_df: pd.DataFrame) -> str:
fioh_df = fioh_df.drop_duplicates(subset=['TAGNAME'])
fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
# Create routine XML structure
routine = ET.Element("Routine", attrib={"Name": "R031_FIOH", "Type": "RLL"})
# Create routine XML structure (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('fioh', 'R031_FIOH')
except Exception:
routine_name = 'R031_FIOH'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Documentation comment
@ -66,7 +72,7 @@ FIOHs are dependent on the masters for communication back to the DPM"""
elif 'TAGNAME' in fioh_row and pd.notna(fioh_row['TAGNAME']):
fio_name = str(fioh_row['TAGNAME']).strip()
else:
fio_name = "MCM01" # Default fallback
fio_name = "MCM" # Default fallback
# Validate both names exist and are not empty
if not fioh_name or not fio_name:
@ -80,7 +86,7 @@ FIOHs are dependent on the masters for communication back to the DPM"""
# Generate AOI_IO_BLOCK call with FIO controller reference
# Pattern: AOI_IO_BLOCK({FIOH_NAME}.AOI,{FIOH_NAME}.HMI,{FIOH_NAME}.CTRL,MCM01.CTRL,{FIO_NAME}.CTRL,{FIOH_NAME}:I.ConnectionFaulted);
aoi_call = f"AOI_IO_BLOCK({fioh_name}.AOI,{fioh_name}.HMI,{fioh_name}.CTRL,MCM01.CTRL,{fio_name}.CTRL,{fioh_name}:I.ConnectionFaulted);"
aoi_call = f"AOI_IO_BLOCK({fioh_name}.AOI,{fioh_name}.HMI,{fioh_name}.CTRL,MCM.CTRL,{fio_name}.CTRL,{fioh_name}:I.ConnectionFaulted);"
text.text = aoi_call
rung_number += 1
@ -134,4 +140,24 @@ def extract_fioh_from_desc_ip(desc_ip_df: pd.DataFrame) -> pd.DataFrame:
fioh_df = fioh_df.drop_duplicates(subset=['TAGNAME'])
fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
return fioh_df
return fioh_df
class FiohRoutinePlugin(RoutinePlugin):
name = "fioh"
description = "Generates the FIOH routine"
category = "device"
def can_generate(self) -> bool:
return not self.context.data_loader.fioh.empty
def generate(self) -> bool:
desc_ip_df = self.context.data_loader.desc_ip
xml_text = generate_fioh_routine(desc_ip_df)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
register_plugin(FiohRoutinePlugin)

View File

@ -7,6 +7,7 @@ FIOM modules are identified by PARTNUMBER containing '5032-8IOLM12DR'.
import xml.etree.ElementTree as ET
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_fiom_routine(desc_ip_df: pd.DataFrame) -> str:
@ -30,8 +31,13 @@ def generate_fiom_routine(desc_ip_df: pd.DataFrame) -> str:
fiom_df = fiom_df.drop_duplicates(subset=['TAGNAME'])
fiom_df = fiom_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
# Create routine XML structure
routine = ET.Element("Routine", attrib={"Name": "R030_FIOM", "Type": "RLL"})
# Create routine XML structure (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('fiom', 'R030_FIOM')
except Exception:
routine_name = 'R030_FIOM'
routine = ET.Element("Routine", attrib={"Name": routine_name, "Type": "RLL"})
rll_content = ET.SubElement(routine, "RLLContent")
# Rung 0: Documentation comment
@ -70,7 +76,7 @@ FIOMs are ethernet network devices that have an IP address and connect directly
dpm_name = '_'.join(dpm_parts) + '_DPM1'
else:
# Default fallback
dpm_name = "MCM01"
dpm_name = "MCM"
print(f" FIOM {fiom_name} -> DPM {dpm_name}")
@ -80,7 +86,7 @@ FIOMs are ethernet network devices that have an IP address and connect directly
# Generate AOI_IO_BLOCK call with DPM controller reference
# Pattern: AOI_IO_BLOCK({FIOM_NAME}.AOI,{FIOM_NAME}.HMI,{FIOM_NAME}.CTRL,MCM01.CTRL,{DPM_NAME}.CTRL,{FIOM_NAME}:I.ConnectionFaulted);
aoi_call = f"AOI_IO_BLOCK({fiom_name}.AOI,{fiom_name}.HMI,{fiom_name}.CTRL,MCM01.CTRL,{dpm_name}.CTRL,{fiom_name}:I.ConnectionFaulted);"
aoi_call = f"AOI_IO_BLOCK({fiom_name}.AOI,{fiom_name}.HMI,{fiom_name}.CTRL,MCM.CTRL,{dpm_name}.CTRL,{fiom_name}:I.ConnectionFaulted);"
text.text = aoi_call
rung_number += 1
@ -128,4 +134,24 @@ def extract_fiom_from_desc_ip(desc_ip_df: pd.DataFrame) -> pd.DataFrame:
fiom_df = fiom_df.drop_duplicates(subset=['TAGNAME'])
fiom_df = fiom_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
return fiom_df
return fiom_df
class FiomRoutinePlugin(RoutinePlugin):
name = "fiom"
description = "Generates the FIOM routine"
category = "device"
def can_generate(self) -> bool:
return not self.context.data_loader.fiom.empty
def generate(self) -> bool:
desc_ip_df = self.context.data_loader.desc_ip
xml_text = generate_fiom_routine(desc_ip_df)
if not xml_text:
return False
self.context.routines_element.append(ET.fromstring(xml_text))
return True
register_plugin(FiomRoutinePlugin)

View File

@ -0,0 +1,189 @@
"""
FLOW_CTRL routine generator.
Generates flow control logic with EXTENDO interlock.
"""
import xml.etree.ElementTree as ET
from typing import Any, Dict
import pandas as pd
from ..utils.common import format_xml_to_match_original, natural_sort_key
from ..plugin_system import RoutinePlugin, register_plugin
def generate_flow_control_routine(data_loader) -> ET.Element:
"""Generate the R050_FLOW_CTRL routine XML element."""
flow_ctrl_data = data_loader.flow_ctrl_data
# Config-driven routine name
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('flow_ctrl', 'R050_FLOW_CTRL')
except Exception:
routine_name = 'R050_FLOW_CTRL'
routine = ET.Element('Routine', Name=routine_name, Type='RLL')
# Create RLLContent
rll_content = ET.SubElement(routine, 'RLLContent')
# Start at rung 0 with no NOP/comment rungs
rung_number = 0
for dpm_name in sorted(flow_ctrl_data.keys(), key=natural_sort_key):
config = flow_ctrl_data[dpm_name]
devices = config['devices']
# No comment/NOP rung per group
# Partition devices
extendos = [d for d in devices if d.get('type') == 'EXTENDO']
vfds = [d for d in devices if d.get('type') == 'VFD']
# Sort per config: lane-based numeric sort or natural
try:
from ..config import get_config
cfg = get_config()
name_regex = getattr(cfg.extraction, 'flow_ctrl_vfd_name_regex', None)
chain_order = getattr(cfg.extraction, 'flow_ctrl_chain_order', 'lane')
enable_extendo = bool(getattr(cfg.extraction, 'flow_ctrl_enable_extendo_interlocks', True))
emit_first_vfd = bool(getattr(cfg.extraction, 'flow_ctrl_emit_first_vfd_ote', True))
chain_dir = getattr(cfg.extraction, 'flow_ctrl_chain_direction', 'downstream_to_upstream')
emit_last_vfd = bool(getattr(cfg.extraction, 'flow_ctrl_emit_last_vfd_ote', False))
except Exception:
name_regex = None
chain_order = 'lane'
enable_extendo = True
emit_first_vfd = True
chain_dir = 'downstream_to_upstream'
emit_last_vfd = False
import re
extendos.sort(key=lambda x: natural_sort_key(x['name']))
if chain_order == 'lane' and name_regex:
rx = re.compile(name_regex)
def vfd_order_key(d):
m = rx.match(d['name'])
if not m:
return (natural_sort_key(d['name']),)
lane = m.group('lane')
pos = int(m.group('pos')) if m.group('pos').isdigit() else 0
return (natural_sort_key(lane), pos)
vfds.sort(key=vfd_order_key)
else:
vfds.sort(key=lambda x: natural_sort_key(x['name']))
# 1) Chain VFDs per lane: do not cross lanes when building pairs
if vfds:
# Group VFDs by lane using the same regex
lane_groups: dict[str, list[str]] = {}
if name_regex:
rx_lane = re.compile(name_regex)
for v in vfds:
nm = v['name']
m = rx_lane.match(nm)
lane = m.group('lane') if m else ''
lane_groups.setdefault(lane, []).append(nm)
else:
# Fallback: single group with all names
lane_groups[''] = [v['name'] for v in vfds]
for lane, names in lane_groups.items():
# names are already ordered per above sorting
if len(names) == 0:
continue
# Build chain pairs based on direction, within this lane only
if chain_dir == 'downstream_to_upstream':
pairs = [(names[i], names[i-1]) for i in range(1, len(names))]
first_name = names[0]
last_name = names[-1]
else:
pairs = [(names[i-1], names[i]) for i in range(1, len(names))]
first_name = names[-1]
last_name = names[0]
for src, dst in pairs:
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f"XIC({src}.CTRL.STS.Enabled)OTE({dst}.CTRL.CMD.Interlock);"
rung_number += 1
# Optional unconditional OTE for first/last VFD in lane
if emit_first_vfd:
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f"OTE({first_name}.CTRL.CMD.Interlock);"
rung_number += 1
if emit_last_vfd:
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f"OTE({last_name}.CTRL.CMD.Interlock);"
rung_number += 1
# 2) EXTENDO interlocks: VFD.Enabled -> EXTENDO.Interlock using explicit depends_on if present
# Use data_loader-provided mapping from VFD to extendo when available
if enable_extendo:
extendo_set = {e['name'] for e in extendos}
for vfd in vfds:
dep = vfd.get('depends_on')
if dep and dep in extendo_set:
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f"XIC({vfd['name']}.CTRL.STS.Enabled)OTE({dep}.CTRL.CMD.Interlock);"
rung_number += 1
# Always interlock last EXTENDO per lane if configured
try:
from ..config import get_config as _gc
cfg2 = _gc()
emit_last_ext = bool(getattr(cfg2.extraction, 'flow_ctrl_emit_last_extendo_ote', True))
except Exception:
emit_last_ext = True
if emit_last_ext and extendos:
# Group extendos by lane using extendo regex
try:
from ..config import get_config as _gc2
ext_rx_pat = getattr(_gc2().extraction, 'flow_ctrl_extendo_name_regex', None)
except Exception:
ext_rx_pat = None
import re as _re
ext_rx = _re.compile(ext_rx_pat) if ext_rx_pat else None
lane_to_ext = {}
for e in extendos:
nm = e['name']
lane = ''
if ext_rx:
m = ext_rx.match(nm)
if m:
lane = m.group('lane')
lane_to_ext.setdefault(lane, []).append(nm)
for lane, names in lane_to_ext.items():
# names already sorted by natural
last_ext = names[-1]
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
text = ET.SubElement(rung, 'Text')
text.text = f"OTE({last_ext}.CTRL.CMD.Interlock);"
rung_number += 1
# Convert to string and format
xml_str = ET.tostring(routine, encoding='unicode')
formatted_xml = format_xml_to_match_original(xml_str)
return ET.fromstring(formatted_xml)
class FlowCtrlRoutinePlugin(RoutinePlugin):
name = "flow_ctrl"
description = "Generates the FLOW_CTRL routine"
category = "device"
def can_generate(self) -> bool:
return bool(self.context.data_loader.flow_ctrl_data)
def generate(self) -> bool:
elem = generate_flow_control_routine(self.context.data_loader)
if elem is None:
return False
self.context.routines_element.append(elem)
return True
register_plugin(FlowCtrlRoutinePlugin)

View File

@ -0,0 +1,98 @@
"""
FPE routine generator (R101_FPE).
Generates AOI_FPE calls for:
- Entries containing "FPE" in DESCA
- Entries containing "3CH_PE" pattern in DESCA
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING
import pandas as pd
from ..utils.common import format_xml_to_match_original
from ..plugin_system import RoutinePlugin, register_plugin
if TYPE_CHECKING:
from ..data_loader import DataLoader
def generate_fpe_routine(loader: DataLoader) -> ET.Element | None:
"""Generate R101_FPE routine from DESC_IP data."""
print("\n [R101_FPE] Starting FPE routine generation...")
# Extract FPE data
fpe_data = loader.fpe_data
if not fpe_data:
print(" [WARNING] No FPE configurations found")
return None
print(f" Found {len(fpe_data)} FPE configurations")
# Create routine element (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('fpe', 'R101_FPE')
except Exception:
routine_name = 'R101_FPE'
routine = ET.Element('Routine', Name=routine_name, Type='RLL')
desc = ET.SubElement(routine, 'Description')
desc.text = 'Full Photo Eye Control Routine'
rll_content = ET.SubElement(routine, 'RLLContent')
# Generate rungs for each FPE configuration
rung_number = 0
for fpe_name, config in sorted(fpe_data.items()):
# Build AOI call - FPE has fewer parameters than JPE
aoi_params = [
f"{fpe_name}.AOI",
f"{fpe_name}.HMI",
f"{fpe_name}.CTRL",
config['conveyor_ctrl'], # Conveyor/VFD controller
config['parent_comm_fault'], # Parent communication fault
config['input_path'], # Clear_I input path
config['beacon_output'] # Beacon_Light_O output path
]
# Create rung
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
comment = ET.SubElement(rung, 'Comment')
comment.text = f"Full Photo Eye Control for {fpe_name}"
text = ET.SubElement(rung, 'Text')
text.text = f"AOI_FPE({','.join(aoi_params)});"
rung_number += 1
print(f" Generated {rung_number} FPE rungs")
# Convert to string and format XML to match Rockwell standards
xml_str = ET.tostring(routine, encoding='unicode')
formatted_xml = format_xml_to_match_original(xml_str)
# Parse back to Element
return ET.fromstring(formatted_xml)
class FpeRoutinePlugin(RoutinePlugin):
name = "fpe"
description = "Generates the FPE routine"
category = "device"
def can_generate(self) -> bool:
return bool(self.context.data_loader.fpe_data)
def generate(self) -> bool:
elem = generate_fpe_routine(self.context.data_loader)
if elem is None:
return False
self.context.routines_element.append(elem)
return True
register_plugin(FpeRoutinePlugin)

View File

@ -119,4 +119,29 @@ def create_inputs_routine(routines: ET.Element, epc_df: pd.DataFrame, ignore_est
norm_desca = normalize_desca(str(row["DESCA"]))
estop_ok_tag = f"{norm_desca}_ESTOP_OK"
text.text = f"XIC({row['IO_PATH']})OTE({estop_ok_tag});"
rung_num += 1
rung_num += 1
# --- Plugin wrapper so modern generator can execute this routine ---
from ..plugin_system import RoutinePlugin
class InputsRoutinePlugin(RoutinePlugin):
name = "inputs"
description = "Generates the R010_INPUTS routine"
category = "safety"
def can_generate(self) -> bool:
try:
return not self.context.data_loader.epc.empty
except Exception:
return False
def generate(self) -> bool:
params = self.context.metadata.get("params", {}) if isinstance(self.context.metadata, dict) else {}
ignore_flag = bool(params.get("ignore_estop1ok", False))
create_inputs_routine(
self.context.routines_element,
self.context.data_loader.epc,
ignore_estop1ok=ignore_flag,
)
return True

View File

@ -0,0 +1,100 @@
"""
JPE routine generator (R0100_JPE).
Generates AOI_JPE calls for:
- Entries containing "JPE" in DESCA
- Entries containing "_2_PE" pattern in DESCA
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING
import pandas as pd
from ..utils.common import format_xml_to_match_original
from ..plugin_system import RoutinePlugin, register_plugin
if TYPE_CHECKING:
from ..data_loader import DataLoader
def generate_jpe_routine(loader: DataLoader) -> ET.Element | None:
"""Generate R0100_JPE routine from DESC_IP data."""
print("\n [R0100_JPE] Starting JPE routine generation...")
# Extract JPE data
jpe_data = loader.jpe_data
if not jpe_data:
print(" [WARNING] No JPE configurations found")
return None
print(f" Found {len(jpe_data)} JPE configurations")
# Create routine element (config-driven name)
try:
from ..config import get_config
routine_name = get_config().routines.name_map.get('jpe', 'R100_JPE')
except Exception:
routine_name = 'R100_JPE'
routine = ET.Element('Routine', Name=routine_name, Type='RLL')
desc = ET.SubElement(routine, 'Description')
desc.text = 'Jam Photo Eye Control Routine'
rll_content = ET.SubElement(routine, 'RLLContent')
# Generate rungs for each JPE configuration
rung_number = 0
for jpe_name, config in sorted(jpe_data.items()):
# Build AOI call
aoi_params = [
f"{jpe_name}.AOI",
f"{jpe_name}.HMI",
f"{jpe_name}.CTRL",
config['conveyor_ctrl'], # Conveyor/VFD controller
config['station_ctrl'], # Station controller (JR)
config['parent_comm_fault'], # Parent communication fault
config['input_path'], # Input path
"YES", # Run up PE - always YES
config['beacon_output'] # Beacon output path
]
# Create rung
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_number), Type='N')
comment = ET.SubElement(rung, 'Comment')
comment.text = f"Jam Photo Eye Control for {jpe_name}"
text = ET.SubElement(rung, 'Text')
text.text = f"AOI_JPE({','.join(aoi_params)});"
rung_number += 1
print(f" Generated {rung_number} JPE rungs")
# Convert to string and format XML to match Rockwell standards
xml_str = ET.tostring(routine, encoding='unicode')
formatted_xml = format_xml_to_match_original(xml_str)
# Parse back to Element
return ET.fromstring(formatted_xml)
class JpeRoutinePlugin(RoutinePlugin):
name = "jpe"
description = "Generates the JPE routine"
category = "device"
def can_generate(self) -> bool:
return bool(self.context.data_loader.jpe_data)
def generate(self) -> bool:
elem = generate_jpe_routine(self.context.data_loader)
if elem is None:
return False
self.context.routines_element.append(elem)
return True
register_plugin(JpeRoutinePlugin)

View File

@ -31,29 +31,85 @@ class MainRoutinePlugin(RoutinePlugin):
return errors
def generate(self) -> bool:
"""Generate the MainRoutine."""
"""Generate the MainRoutine with JSR calls to created routines."""
try:
self.logger.info("Generating MainRoutine")
self.logger.info("Generating MainRoutine with JSR calls")
# Resolve routine names from config mapping
try:
nm = self.context.config.routines.name_map
except Exception:
nm = {}
# If a routine plan is present, honor its order for MainProgram
calls: List[str] = []
try:
plan = getattr(self.context.config, 'routine_plan', []) or []
ordered = [e for e in plan if getattr(e, 'enabled', True) and getattr(e, 'program', 'MainProgram') == 'MainProgram']
ordered.sort(key=lambda e: getattr(e, 'order', 100))
for entry in ordered:
# Skip generating a JSR to ourselves
if getattr(entry, 'plugin', '') == 'main_routine' or getattr(entry, 'name', '') == 'main_routine':
continue
# Map plugin/name to configured routine name
plugin_key = getattr(entry, 'plugin', '') or getattr(entry, 'name', '')
routine_name = nm.get(plugin_key, nm.get(getattr(entry, 'name', ''), getattr(entry, 'name', '')))
if routine_name:
calls.append(routine_name)
except Exception:
calls = []
# Fallback to comprehensive default order if no plan provided
if not calls:
defaults = [
nm.get('safety_tag_map', 'R130_SAFETY_TAG_MAP'),
nm.get('rack', 'R011_RACK'),
nm.get('mcm', 'R010_MCM'),
nm.get('dpm', 'R020_DPM'),
nm.get('fiom', 'R030_FIOM'),
nm.get('fioh', 'R031_FIOH'),
nm.get('apf', 'R040_APF'),
nm.get('extendo', 'R041_EXTENDO'),
nm.get('flow_ctrl', 'R050_FLOW_CTRL'),
nm.get('speed_ctrl', 'R051_SPEED_CTRL'),
nm.get('d2c_chute', 'R042_D2C_CHUTE'),
nm.get('pb_chute', 'R043_PB_CHUTE'),
nm.get('station_jr_chute', 'R044_STATION_JR_CHUTE'),
nm.get('pmm', 'R060_PMM'),
nm.get('cb_monitor', 'R070_CB_MONITOR'),
nm.get('station_jr_pb', 'R090_STATION_JR_PB'),
nm.get('jpe', 'R100_JPE'),
nm.get('fpe', 'R101_FPE'),
nm.get('estop_check', 'R120_ESTOP_CHECK'),
]
calls = [c for c in defaults if c]
# Filter to only include JSRs for routines that actually exist in the program
existing = {r.get('Name') for r in self.context.routines_element.findall('Routine')}
calls = [c for c in calls if c in existing]
# Create the MainRoutine
routine = ET.SubElement(self.context.routines_element, "Routine")
routine.set("Name", "MainRoutine")
routine.set("Name", self.context.config.routines.main_routine_name if hasattr(self.context.config, 'routines') else "MainRoutine")
routine.set("Type", "RLL")
# Create RLL content
rll_content = ET.SubElement(routine, "RLLContent")
# Add a simple rung that enables output
# Single rung with JSR calls in the configured order
rung = ET.SubElement(rll_content, "Rung")
rung.set("Number", "0")
rung.set("Type", "N")
text = ET.SubElement(rung, "Text")
text.text = "XIC(GlobalEnable)OTE(SystemReady);"
if calls:
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'
else:
text.text = 'NOP();'
self.logger.info("Successfully generated MainRoutine")
return True
except Exception as e:
self.logger.error(f"Failed to generate MainRoutine: {e}")
return False
return False

Some files were not shown because too many files have changed in this diff Show More