added config to routines generator
This commit is contained in:
parent
f251cc7e25
commit
e06a57dbc2
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -19,6 +19,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import copy
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Base controller & fixed slot models
|
# Base controller & fixed slot models
|
||||||
@ -78,6 +79,9 @@ class ControllerBuilder:
|
|||||||
def __init__(self, controller_name: str, skip_chassis_modules: bool = False):
|
def __init__(self, controller_name: str, skip_chassis_modules: bool = False):
|
||||||
self.controller_name = controller_name
|
self.controller_name = controller_name
|
||||||
self.skip_chassis_modules = skip_chassis_modules
|
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
|
# 1. Build base controller from boilerplate
|
||||||
controller_cfg = create_l83es_controller(controller_name)
|
controller_cfg = create_l83es_controller(controller_name)
|
||||||
@ -129,6 +133,86 @@ class ControllerBuilder:
|
|||||||
# Indent & persist
|
# Indent & persist
|
||||||
self._save_project(self.tree, filename)
|
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):
|
def add_generated_tags(self, routines_generator_dir: str, zones_dict=None):
|
||||||
"""Add the generated tags from the Routines Generator into this controller.
|
"""Add the generated tags from the Routines Generator into this controller.
|
||||||
|
|
||||||
@ -557,6 +641,22 @@ class ControllerBuilder:
|
|||||||
# Apply CDATA wrapping to Text elements
|
# 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)
|
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)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
with open(filename, "w", encoding="utf-8") as fh:
|
with open(filename, "w", encoding="utf-8") as fh:
|
||||||
fh.write(full_xml)
|
fh.write(full_xml)
|
||||||
|
|||||||
@ -362,9 +362,7 @@ class EnhancedMCMGenerator:
|
|||||||
|
|
||||||
# Print summary of modules found
|
# Print summary of modules found
|
||||||
if self.iolm_modules:
|
if self.iolm_modules:
|
||||||
print(f"Found {len(self.iolm_modules)} 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')})")
|
|
||||||
|
|
||||||
if self.unknown_modules:
|
if self.unknown_modules:
|
||||||
print(f"WARNING: {len(self.unknown_modules)} unknown modules found")
|
print(f"WARNING: {len(self.unknown_modules)} unknown modules found")
|
||||||
@ -391,22 +389,33 @@ class EnhancedMCMGenerator:
|
|||||||
modules_section = builder.get_modules_section()
|
modules_section = builder.get_modules_section()
|
||||||
self._add_excel_modules(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
|
# 3. Embed generated programs from Routines Generator - TEMPORARILY DISABLED
|
||||||
# routines_generator_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Routines Generator")
|
# 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}")
|
# print(f"Looking for generated programs in: {routines_generator_dir}")
|
||||||
# builder.add_generated_programs(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
|
# 4. Embed generated tags from Routines Generator - TEMPORARILY DISABLED
|
||||||
# print(f"Looking for generated tags in: {routines_generator_dir}")
|
# print(f"Looking for generated tags in: {routines_generator_dir}")
|
||||||
# builder.add_generated_tags(routines_generator_dir, zones_dict=getattr(self, 'zones_dict', None))
|
# 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
|
# 5. Finalise and save
|
||||||
output_file = os.path.join(self.generated_dir, f"{self.controller_name}.L5X")
|
output_file = os.path.join(self.generated_dir, f"{self.controller_name}.L5X")
|
||||||
builder.finalise_and_save(output_file)
|
builder.finalise_and_save(output_file)
|
||||||
|
|
||||||
print(f"Generated complete project: {output_file}")
|
print(f"OK: Generated project: {output_file}")
|
||||||
return [output_file]
|
return [output_file]
|
||||||
|
|
||||||
def generate_split_projects(self) -> Tuple[str, str]:
|
def generate_split_projects(self) -> Tuple[str, str]:
|
||||||
@ -415,7 +424,7 @@ class EnhancedMCMGenerator:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (file1_path, file2_path)
|
Tuple of (file1_path, file2_path)
|
||||||
"""
|
"""
|
||||||
print("Generating split projects...")
|
print("Split projects generation")
|
||||||
|
|
||||||
# Validate parent-child relationships before splitting
|
# Validate parent-child relationships before splitting
|
||||||
self._validate_parent_child_relationships()
|
self._validate_parent_child_relationships()
|
||||||
@ -440,9 +449,9 @@ class EnhancedMCMGenerator:
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Split projects generated successfully:")
|
print("OK: Split projects generated")
|
||||||
print(f" Part 1: {file1_path} ({self._count_modules_in_group(group1_modules)} modules)")
|
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(f"- Part 2: {file2_path} ({self._count_modules_in_group(group2_modules)} modules)")
|
||||||
|
|
||||||
return file1_path, file2_path
|
return file1_path, file2_path
|
||||||
|
|
||||||
@ -452,7 +461,7 @@ class EnhancedMCMGenerator:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (group1_dict, group2_dict) where each dict contains module lists by type
|
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
|
# Initialize empty groups
|
||||||
group1 = {
|
group1 = {
|
||||||
@ -591,9 +600,7 @@ class EnhancedMCMGenerator:
|
|||||||
family_size += len(children)
|
family_size += len(children)
|
||||||
|
|
||||||
# Log family assignment for debugging
|
# Log family assignment for debugging
|
||||||
if children:
|
# Assignment details suppressed for concise output
|
||||||
child_names = [child.get('name', 'Unknown') for _, child in children]
|
|
||||||
print(f" Assigning family: {module_name} with {len(children)} children: {child_names}")
|
|
||||||
|
|
||||||
# Assign to group with fewer modules
|
# Assign to group with fewer modules
|
||||||
if group1_total <= group2_total:
|
if group1_total <= group2_total:
|
||||||
@ -605,8 +612,7 @@ class EnhancedMCMGenerator:
|
|||||||
group2_total += family_size
|
group2_total += family_size
|
||||||
group_name = "Group 2"
|
group_name = "Group 2"
|
||||||
|
|
||||||
if children:
|
# Assignment target suppressed for concise output
|
||||||
print(f" -> {group_name} (family size: {family_size})")
|
|
||||||
|
|
||||||
# Add parent module
|
# Add parent module
|
||||||
target_group[module_type].append(module)
|
target_group[module_type].append(module)
|
||||||
@ -645,12 +651,7 @@ class EnhancedMCMGenerator:
|
|||||||
orphaned_modules.append(('solenoid', solenoid))
|
orphaned_modules.append(('solenoid', solenoid))
|
||||||
|
|
||||||
# Distribute orphaned modules
|
# Distribute orphaned modules
|
||||||
if orphaned_modules:
|
# Orphan details suppressed; only assignment occurs
|
||||||
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})")
|
|
||||||
|
|
||||||
for module_type, module in orphaned_modules:
|
for module_type, module in orphaned_modules:
|
||||||
if group1_total <= group2_total:
|
if group1_total <= group2_total:
|
||||||
@ -662,8 +663,7 @@ class EnhancedMCMGenerator:
|
|||||||
group2_total += 1
|
group2_total += 1
|
||||||
group_name = "Group 2"
|
group_name = "Group 2"
|
||||||
|
|
||||||
module_name = module.get('name', 'Unknown')
|
# Assignment summary suppressed
|
||||||
print(f" -> Assigning orphaned {module_type} '{module_name}' to {group_name}")
|
|
||||||
assigned_modules.add(module_name)
|
assigned_modules.add(module_name)
|
||||||
|
|
||||||
def _generate_project_with_modules(self, project_name: str, module_groups: Dict, part_number: int) -> str:
|
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
|
# Add modules from the specific groups
|
||||||
self._add_excel_modules_from_groups(modules_section, module_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
|
# Save the file
|
||||||
output_filename = f"{self.generated_dir}/{project_name}.L5X"
|
output_filename = f"{self.generated_dir}/{project_name}.L5X"
|
||||||
builder.finalise_and_save(output_filename)
|
builder.finalise_and_save(output_filename)
|
||||||
@ -785,7 +796,7 @@ class EnhancedMCMGenerator:
|
|||||||
|
|
||||||
def _validate_parent_child_relationships(self):
|
def _validate_parent_child_relationships(self):
|
||||||
"""Validate parent-child relationships before splitting."""
|
"""Validate parent-child relationships before splitting."""
|
||||||
print(" Validating parent-child relationships...")
|
# Validating parent-child relationships
|
||||||
|
|
||||||
issues = []
|
issues = []
|
||||||
|
|
||||||
@ -844,15 +855,15 @@ class EnhancedMCMGenerator:
|
|||||||
issues.append(f"Solenoid module '{solenoid.get('name')}' references non-existent IOLM parent '{parent}'")
|
issues.append(f"Solenoid module '{solenoid.get('name')}' references non-existent IOLM parent '{parent}'")
|
||||||
|
|
||||||
if issues:
|
if issues:
|
||||||
print(f" WARNING: Found {len(issues)} parent-child relationship issues:")
|
print(f"Warning: {len(issues)} parent-child relationship issues detected")
|
||||||
for issue in issues:
|
for issue in issues[:5]:
|
||||||
print(f" - {issue}")
|
print(f"- {issue}")
|
||||||
else:
|
else:
|
||||||
print(" [SUCCESS] All parent-child relationships are valid")
|
print("OK: Parent-child relationships valid")
|
||||||
|
|
||||||
def _validate_split_integrity(self, group1: Dict, group2: Dict):
|
def _validate_split_integrity(self, group1: Dict, group2: Dict):
|
||||||
"""Validate that the split maintains parent-child relationships."""
|
"""Validate that the split maintains parent-child relationships."""
|
||||||
print(" Validating split integrity...")
|
# Validating split integrity
|
||||||
|
|
||||||
# Build module name to group mapping
|
# Build module name to group mapping
|
||||||
group1_modules = set()
|
group1_modules = set()
|
||||||
@ -894,12 +905,12 @@ class EnhancedMCMGenerator:
|
|||||||
violations.append(f"{child_type} '{child_name}' separated from parent '{parent_name}'")
|
violations.append(f"{child_type} '{child_name}' separated from parent '{parent_name}'")
|
||||||
|
|
||||||
if violations:
|
if violations:
|
||||||
print(f" ERROR: Found {len(violations)} split integrity violations:")
|
print(f"Error: {len(violations)} split integrity violations detected")
|
||||||
for violation in violations:
|
for violation in violations[:5]:
|
||||||
print(f" - {violation}")
|
print(f"- {violation}")
|
||||||
raise ValueError("Split integrity validation failed - parent-child relationships would be broken")
|
raise ValueError("Split integrity validation failed - parent-child relationships would be broken")
|
||||||
else:
|
else:
|
||||||
print(" [SUCCESS] Split maintains all parent-child relationships")
|
print("OK: Split integrity valid")
|
||||||
|
|
||||||
def _configure_controller_settings(self, root):
|
def _configure_controller_settings(self, root):
|
||||||
"""Configure controller-specific settings."""
|
"""Configure controller-specific settings."""
|
||||||
@ -1166,11 +1177,10 @@ def main():
|
|||||||
if len(sys.argv) > 2 and not sys.argv[2].startswith("--"):
|
if len(sys.argv) > 2 and not sys.argv[2].startswith("--"):
|
||||||
project_name = sys.argv[2]
|
project_name = sys.argv[2]
|
||||||
|
|
||||||
print(f"Enhanced MCM Generator")
|
print("Enhanced MCM Generator")
|
||||||
print(f"Project: {project_name}")
|
print(f"- Project: {project_name}")
|
||||||
print(f"Excel file: {excel_file}")
|
print(f"- Excel: {excel_file}")
|
||||||
print(f"Mode: {'Split (2 files)' if split_mode else 'Single file'}")
|
print(f"- Mode: {'Split' if split_mode else 'Single file'}")
|
||||||
print(f"Zones: {'Custom' if zones_dict else 'Default'}")
|
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
|
|
||||||
# Create generator with zones
|
# Create generator with zones
|
||||||
@ -1181,15 +1191,14 @@ def main():
|
|||||||
if split_mode:
|
if split_mode:
|
||||||
# Generate split projects
|
# Generate split projects
|
||||||
file1, file2 = generator.generate_complete_project(split_mode=True)
|
file1, file2 = generator.generate_complete_project(split_mode=True)
|
||||||
print(f"\nSplit generation complete!")
|
print("Split generation complete")
|
||||||
print(f"Generated files:")
|
print(f"- {file1}")
|
||||||
print(f" {file1}")
|
print(f"- {file2}")
|
||||||
print(f" {file2}")
|
|
||||||
else:
|
else:
|
||||||
# Generate single project
|
# Generate single project
|
||||||
output_file = generator.generate_complete_project(split_mode=False)
|
output_file = generator.generate_complete_project(split_mode=False)
|
||||||
print(f"\nSingle file generation complete!")
|
print("Single file generation complete")
|
||||||
print(f"Generated file: {output_file}")
|
print(f"- {output_file}")
|
||||||
else:
|
else:
|
||||||
print("ERROR: Failed to load/process Excel data")
|
print("ERROR: Failed to load/process Excel data")
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
|
||||||
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo ====================================
|
echo ====================================
|
||||||
echo PLC Compilation: MTN6_MCM05_CHUTE_LOAD
|
echo PLC Compilation: mtn6
|
||||||
echo Project Type: MCM05
|
echo Project Type: UNKNOWN
|
||||||
echo ====================================
|
echo ====================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
@ -9,8 +9,8 @@ cd /d "C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler"
|
|||||||
echo Working directory: %CD%
|
echo Working directory: %CD%
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
if not exist "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_MCM05_CHUTE_LOAD.L5X
|
echo ERROR: L5X file not found: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
@ -61,28 +61,28 @@ if errorlevel 1 (
|
|||||||
echo ✓ Logix Designer SDK found
|
echo ✓ Logix Designer SDK found
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo Input L5X file: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.L5X
|
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_MCM05_CHUTE_LOAD.L5X") do echo File size: %%~zF bytes
|
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.L5X") do echo File size: %%~zF bytes
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo Starting compilation...
|
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.
|
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 ====================================
|
echo ====================================
|
||||||
echo SUCCESS: Compilation completed!
|
echo SUCCESS: Compilation completed!
|
||||||
echo Output: C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\MTN6_MCM05_CHUTE_LOAD.ACD
|
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_MCM05_CHUTE_LOAD.ACD") do echo ACD size: %%~zF bytes
|
for %%F in ("C:\Users\ilia.gurielidze\Projects\PLC Generation\L5X2ACD Compiler\mtn6.ACD") do echo ACD size: %%~zF bytes
|
||||||
echo ====================================
|
echo ====================================
|
||||||
) else (
|
) else (
|
||||||
echo.
|
echo.
|
||||||
echo ====================================
|
echo ====================================
|
||||||
echo ERROR: Compilation failed!
|
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 ====================================
|
echo ====================================
|
||||||
)
|
)
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
BIN
PLC Data Generator/MCM02_DESC_IP_MERGED.xlsx
Normal file
BIN
PLC Data Generator/MCM02_DESC_IP_MERGED.xlsx
Normal file
Binary file not shown.
410
PLC Data Generator/MCM02_OUTPUT.csv
Normal file
410
PLC Data Generator/MCM02_OUTPUT.csv
Normal 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
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -30,6 +30,8 @@ def classify_signal(desca, tagname, descb=None):
|
|||||||
return 'O'
|
return 'O'
|
||||||
elif re.search(r'BCN', desca_str) and 'FIOH' in tagname_str:
|
elif re.search(r'BCN', desca_str) and 'FIOH' in tagname_str:
|
||||||
return 'O'
|
return 'O'
|
||||||
|
elif re.search(r'_H', desca_str):
|
||||||
|
return 'O'
|
||||||
|
|
||||||
# IOLink patterns (check FIOH before FIO due to priority)
|
# IOLink patterns (check FIOH before FIO due to priority)
|
||||||
elif re.search(r'SOL', desca_str) and re.search(r'DIVERT', descb_str):
|
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'
|
return 'I'
|
||||||
elif re.search(r'FPE', desca_str):
|
elif re.search(r'FPE', desca_str):
|
||||||
return 'I'
|
return 'I'
|
||||||
elif re.search(r'ENC', desca_str):
|
elif re.search(r'EN', desca_str):
|
||||||
return 'I'
|
return 'I'
|
||||||
elif re.search(r'PS', desca_str):
|
elif re.search(r'PS', desca_str):
|
||||||
return 'I'
|
return 'I'
|
||||||
|
|||||||
Binary file not shown.
BIN
PLC Data Generator/data/SAT9_MCM01.xlsx
Normal file
BIN
PLC Data Generator/data/SAT9_MCM01.xlsx
Normal file
Binary file not shown.
BIN
PLC Data Generator/data/SAT9_MCM02.xlsx
Normal file
BIN
PLC Data Generator/data/SAT9_MCM02.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
@ -1 +0,0 @@
|
|||||||
MCM01_S_PB=SFT_MCM01_S_PB
|
|
||||||
105
Routines Generator/UDTs_Tags/CB_MONITOR_Boilerplate.xml
Normal file
105
Routines Generator/UDTs_Tags/CB_MONITOR_Boilerplate.xml
Normal 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>
|
||||||
54
Routines Generator/UDTs_Tags/D2C_Boilerplate.xml
Normal file
54
Routines Generator/UDTs_Tags/D2C_Boilerplate.xml
Normal 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>
|
||||||
60
Routines Generator/UDTs_Tags/EXTENDO_Boilerplate.xml
Normal file
60
Routines Generator/UDTs_Tags/EXTENDO_Boilerplate.xml
Normal 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>
|
||||||
45
Routines Generator/UDTs_Tags/FPE_Boilerplate.xml
Normal file
45
Routines Generator/UDTs_Tags/FPE_Boilerplate.xml
Normal 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>
|
||||||
47
Routines Generator/UDTs_Tags/JPE_Boilerplate.xml
Normal file
47
Routines Generator/UDTs_Tags/JPE_Boilerplate.xml
Normal 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>
|
||||||
142
Routines Generator/UDTs_Tags/MCM_Boilerplate.xml
Normal file
142
Routines Generator/UDTs_Tags/MCM_Boilerplate.xml
Normal 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>
|
||||||
62
Routines Generator/UDTs_Tags/PB_CHUTE_Boilerplate.xml
Normal file
62
Routines Generator/UDTs_Tags/PB_CHUTE_Boilerplate.xml
Normal 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>
|
||||||
26
Routines Generator/UDTs_Tags/PMM_Boilerplate.xml
Normal file
26
Routines Generator/UDTs_Tags/PMM_Boilerplate.xml
Normal 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>
|
||||||
25
Routines Generator/UDTs_Tags/RACK_Boilerplate.xml
Normal file
25
Routines Generator/UDTs_Tags/RACK_Boilerplate.xml
Normal 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>
|
||||||
@ -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>
|
||||||
62
Routines Generator/UDTs_Tags/STATION_JR_PB_Boilerplate.xml
Normal file
62
Routines Generator/UDTs_Tags/STATION_JR_PB_Boilerplate.xml
Normal 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>
|
||||||
BIN
Routines Generator/__pycache__/complete_workflow.cpython-312.pyc
Normal file
BIN
Routines Generator/__pycache__/complete_workflow.cpython-312.pyc
Normal file
Binary file not shown.
@ -14,6 +14,10 @@ import sys
|
|||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import re
|
||||||
|
|
||||||
def get_project_paths():
|
def get_project_paths():
|
||||||
"""Get standardized paths for all project components."""
|
"""Get standardized paths for all project components."""
|
||||||
@ -27,7 +31,7 @@ def get_project_paths():
|
|||||||
'io_tree_generator': project_root / "IO Tree Configuration Generator"
|
'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."""
|
"""Run the PLC Data Generator to create DESC_IP_MERGED.xlsx."""
|
||||||
data_gen_dir = paths['data_generator']
|
data_gen_dir = paths['data_generator']
|
||||||
data_gen_script = data_gen_dir / "main.py"
|
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}")
|
print(f"ERROR: Raw Excel file not found at {raw_excel_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(f"=== Step 1: Processing Raw Excel Data ===")
|
|
||||||
print(f"Input: {raw_excel_file}")
|
|
||||||
print(f"Processing with PLC Data Generator...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run the PLC Data Generator with the Excel file path as argument
|
# Run the PLC Data Generator with the Excel file path as argument
|
||||||
result = subprocess.run([
|
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
|
# 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 or (any(success_indicators) and "[Errno 1] Operation not permitted" in result.stdout):
|
||||||
if result.returncode != 0:
|
if verbose and result.returncode != 0:
|
||||||
print("WARNING: Permission error at end of processing, but core processing completed successfully")
|
print("Warning: Permission error at end of processing, core processing completed")
|
||||||
else:
|
if verbose:
|
||||||
print("SUCCESS: PLC Data Generator completed successfully")
|
print(result.stdout)
|
||||||
print(result.stdout)
|
|
||||||
|
|
||||||
# Copy DESC_IP_MERGED.xlsx from data generator output (it already has safety sheets)
|
# Copy DESC_IP_MERGED.xlsx from data generator output (it already has safety sheets)
|
||||||
dest = paths['routines_generator'] / "DESC_IP_MERGED.xlsx"
|
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():
|
if source.exists():
|
||||||
import shutil
|
import shutil
|
||||||
shutil.copy2(source, dest)
|
shutil.copy2(source, dest)
|
||||||
print(f"SUCCESS: DESC_IP_MERGED.xlsx (with safety sheets) copied to {dest}")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("ERROR: DESC_IP_MERGED.xlsx not found after data generation")
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print("ERROR: PLC Data Generator failed")
|
if verbose:
|
||||||
print("STDOUT:", result.stdout)
|
print("Error: Data processing failed")
|
||||||
print("STDERR:", result.stderr)
|
print("STDOUT:", result.stdout)
|
||||||
|
print("STDERR:", result.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
def run_routines_generator(paths: dict, project_name: str = None, ignore_estop1ok: bool = False) -> bool:
|
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 using generate_main_with_dpm.py script."""
|
"""Run the Routines Generator.
|
||||||
print(f"\n=== Step 2: Generating L5X Programs ===")
|
|
||||||
|
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']
|
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:
|
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 = [
|
cmd_args = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(main_dpm_script),
|
'-m', 'src.unified_cli',
|
||||||
"--desc-ip-mode" # Use DESC_IP data extraction mode
|
'--config', str(config_path),
|
||||||
|
'--excel-file', str(excel_path),
|
||||||
]
|
]
|
||||||
|
if verbose:
|
||||||
# Add project name if provided
|
cmd_args.extend(['--log-level', 'DEBUG'])
|
||||||
if project_name:
|
cmd_args.append(subcmd)
|
||||||
cmd_args.extend(["--project-name", project_name])
|
# Note: routine inclusion/exclusion is driven by config; project_name and ignore-estop1ok are configured in JSON
|
||||||
print(f"INFO: Running with project name: {project_name}")
|
|
||||||
|
|
||||||
# Add ignore-estop1ok flag if set
|
|
||||||
if ignore_estop1ok:
|
|
||||||
cmd_args.append("--ignore-estop1ok")
|
|
||||||
print("INFO: Running with --ignore-estop1ok flag")
|
|
||||||
|
|
||||||
# Run the main program generator script with DPM routines
|
# Run the unified CLI
|
||||||
result = subprocess.run(cmd_args, cwd=routines_dir, capture_output=True, text=True)
|
result = subprocess.run(cmd_args, cwd=routines_dir, capture_output=True, text=True)
|
||||||
|
|
||||||
print(result.stdout)
|
if verbose:
|
||||||
if result.stderr:
|
print(result.stdout)
|
||||||
print("STDERR:", result.stderr)
|
if result.stderr:
|
||||||
|
print("[generator stderr]", result.stderr)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print("SUCCESS: Routines generation completed successfully")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("ERROR: Routines generation failed")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Error running Routines Generator: {e}")
|
if verbose:
|
||||||
|
print(f"Error: Exception in routine generation: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run_io_tree_generator(paths: dict, project_name: str) -> bool:
|
def run_io_tree_generator(paths: dict, project_name: str, safety_only: bool = False, verbose: bool = False) -> bool:
|
||||||
"""Run the IO Tree Configuration Generator."""
|
"""Run the IO Tree Configuration Generator.
|
||||||
print(f"\n=== Step 3: Generating Complete Project L5X ===")
|
|
||||||
|
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']
|
io_tree_dir = paths['io_tree_generator']
|
||||||
enhanced_mcm_script = io_tree_dir / "enhanced_mcm_generator.py"
|
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"
|
desc_ip_file = paths['data_generator'] / "DESC_IP_MERGED.xlsx"
|
||||||
|
|
||||||
if not enhanced_mcm_script.exists():
|
if not enhanced_mcm_script.exists():
|
||||||
print(f"ERROR: enhanced_mcm_generator.py not found at {enhanced_mcm_script}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Load zones configuration for the project
|
# Zones fully removed: do not attempt to load or pass zones
|
||||||
zones_json = None
|
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:
|
try:
|
||||||
# Build command arguments
|
# Build command arguments
|
||||||
@ -185,56 +161,70 @@ def run_io_tree_generator(paths: dict, project_name: str) -> bool:
|
|||||||
project_name
|
project_name
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add zones if available
|
# Zones removed; no additional args
|
||||||
if zones_json:
|
|
||||||
cmd_args.extend(["--zones", zones_json])
|
|
||||||
|
|
||||||
# Run the IO Tree Configuration Generator
|
# Run the IO Tree Configuration Generator
|
||||||
result = subprocess.run(cmd_args, cwd=io_tree_dir, capture_output=True, text=True)
|
result = subprocess.run(cmd_args, cwd=io_tree_dir, capture_output=True, text=True)
|
||||||
|
|
||||||
print(result.stdout)
|
if verbose:
|
||||||
if result.stderr:
|
print(result.stdout)
|
||||||
print("STDERR:", result.stderr)
|
if result.stderr:
|
||||||
|
print("[io-tree stderr]", result.stderr)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print("SUCCESS: Complete project L5X generated successfully")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("ERROR: IO Tree generation failed")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
def run_l5x_to_acd_compiler(paths: dict, project_name: str) -> bool:
|
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."""
|
"""Prepare for L5X2ACD Compilation using dynamic compilation manager.
|
||||||
print(f"\n=== Step 4: Preparing for L5X to ACD Compilation ===")
|
|
||||||
|
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
|
# Find the generated complete project L5X file
|
||||||
io_tree_dir = paths['io_tree_generator']
|
io_tree_dir = paths['io_tree_generator']
|
||||||
generated_projects_dir = io_tree_dir / "generated_projects"
|
generated_projects_dir = io_tree_dir / "generated_projects"
|
||||||
|
|
||||||
if not generated_projects_dir.exists():
|
if not generated_projects_dir.exists():
|
||||||
print(f"ERROR: Generated projects directory not found at {generated_projects_dir}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Look for L5X files that start with the project name
|
# Look for L5X files that start with the project name
|
||||||
l5x_files = list(generated_projects_dir.glob(f"{project_name}*.L5X"))
|
l5x_files = list(generated_projects_dir.glob(f"{project_name}*.L5X"))
|
||||||
|
|
||||||
if not l5x_files:
|
if not l5x_files:
|
||||||
print(f"ERROR: No L5X files found starting with '{project_name}' in {generated_projects_dir}")
|
if verbose:
|
||||||
available_files = list(generated_projects_dir.glob("*.L5X"))
|
available_files = list(generated_projects_dir.glob("*.L5X"))
|
||||||
if available_files:
|
if available_files:
|
||||||
print(f"Available L5X files: {[f.name for f in available_files]}")
|
print(f"Available L5X files: {[f.name for f in available_files]}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(l5x_files) > 1:
|
if len(l5x_files) > 1 and verbose:
|
||||||
print(f"WARNING: Multiple L5X files found: {[f.name for f in l5x_files]}")
|
print(f"Warning: Multiple L5X files found, using first: {l5x_files[0].name}")
|
||||||
print(f"Using the first one: {l5x_files[0].name}")
|
|
||||||
|
|
||||||
complete_l5x = l5x_files[0]
|
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
|
# Use the dynamic compilation manager
|
||||||
l5x2acd_dir = paths['project_root'] / "L5X2ACD Compiler"
|
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"
|
project_type = "MCM04"
|
||||||
options['enable_feeder_optimization'] = True
|
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
|
# Setup compilation with wipe and dynamic generation
|
||||||
result = manager.setup_compilation(
|
if verbose:
|
||||||
source_l5x=complete_l5x,
|
result = manager.setup_compilation(
|
||||||
project_name=project_name or complete_l5x.stem,
|
source_l5x=complete_l5x,
|
||||||
compilation_options=options,
|
project_name=project_name or complete_l5x.stem,
|
||||||
wipe_existing=True
|
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")
|
if verbose:
|
||||||
print("=" * 60)
|
print("OK: Compilation setup completed")
|
||||||
print("⚠️ COMPILATION STEP REQUIRES WINDOWS:")
|
l5x2acd_windows_path = str(l5x2acd_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\')
|
||||||
print(f" L5X File: {result['l5x_file']}")
|
l5x_windows_path = str(result['l5x_file']).replace('/mnt/c/', 'C:\\').replace('/', '\\')
|
||||||
print(f" Batch File: {result['batch_file']}")
|
print("To compile on Windows:")
|
||||||
print()
|
print(f"- cd \"{l5x2acd_windows_path}\"")
|
||||||
print("🪟 To compile on Windows:")
|
print(f"- python l5x_to_acd.py \"{l5x_windows_path}\"")
|
||||||
print(f" 1. Run: {result['batch_file']}")
|
|
||||||
print(" 2. Or double-click: {result['batch_file'].name}")
|
|
||||||
print(" 3. Or manually run:")
|
|
||||||
l5x2acd_windows_path = str(l5x2acd_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\')
|
|
||||||
l5x_windows_path = str(result['l5x_file']).replace('/mnt/c/', 'C:\\').replace('/', '\\')
|
|
||||||
print(f" cd \"{l5x2acd_windows_path}\"")
|
|
||||||
print(f" python l5x_to_acd.py \"{l5x_windows_path}\"")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Failed to setup dynamic compilation: {e}")
|
if verbose:
|
||||||
import traceback
|
print(f"Error: Exception in compilation setup: {e}")
|
||||||
traceback.print_exc()
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return False
|
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:
|
def main() -> None:
|
||||||
"""Main entry point for complete workflow."""
|
"""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('--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('--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('--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Get project paths
|
# Get project paths
|
||||||
paths = get_project_paths()
|
paths = get_project_paths()
|
||||||
|
|
||||||
print("Complete PLC Generation Workflow")
|
print("PLC Generation Workflow")
|
||||||
print("=" * 60)
|
|
||||||
print(f"Project Root: {paths['project_root']}")
|
|
||||||
print(f"Input Excel: {args.excel_file}")
|
|
||||||
print(f"Project: {args.project_name}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Step 1: Process raw Excel data
|
# Step 1: Process raw Excel data
|
||||||
if not run_plc_data_generator(args.excel_file, paths):
|
print("Step 1: Data processing ...", end=" ")
|
||||||
print("ERROR: Workflow failed at Step 1 (Data Processing)")
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
# Step 2: Generate L5X programs (Routines Generator)
|
# Step 2: Generate L5X programs (Routines Generator)
|
||||||
if not run_routines_generator(paths, args.project_name, args.ignore_estop1ok):
|
print("Step 2: Routine generation ...", end=" ")
|
||||||
print("ERROR: Workflow failed at Step 2 (Routines Generation)")
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
# Step 3: Generate complete project L5X (IO Tree Generator)
|
# Step 3: Generate complete project L5X (IO Tree Generator)
|
||||||
if not run_io_tree_generator(paths, args.project_name):
|
if args.safety_only:
|
||||||
print("ERROR: Workflow failed at Step 3 (IO Tree Generation)")
|
print("Step 3: IO tree generation ... SKIPPED")
|
||||||
sys.exit(1)
|
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
|
# Step 4: Compile L5X to ACD
|
||||||
if not run_l5x_to_acd_compiler(paths, args.project_name):
|
if args.safety_only:
|
||||||
print("ERROR: Workflow failed at Step 4 (L5X2ACD Compilation)")
|
print("Step 4: Prepare compilation ... SKIPPED")
|
||||||
sys.exit(1)
|
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("Workflow complete")
|
||||||
print("SUCCESS: Complete PLC Generation Workflow completed!")
|
if args.verbose and not args.safety_only and args.project_name:
|
||||||
print(f"Generated L5X file: IO Tree Configuration Generator/generated_projects/{args.project_name}.L5X")
|
print(f"L5X: IO Tree Configuration Generator/generated_projects/{args.project_name}.L5X")
|
||||||
print("🪟 Run the generated batch file on Windows to compile to ACD")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
@ -1,170 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""generate_all.py – build SafetyProgram L5X, MainProgram L5X, full tag CSV, and safety-tag mapping.
|
"""
|
||||||
|
This script is deprecated. Use the unified CLI:
|
||||||
This is the updated version using the new configuration and logging systems.
|
python -m src.unified_cli --config ../generator_config.json --excel-file DESC_IP_MERGED.xlsx all
|
||||||
Run from repository root:
|
"""
|
||||||
python generate_all.py [options]
|
import sys
|
||||||
"""
|
print("generate_all.py is deprecated. Please use the unified CLI (src.unified_cli).", file=sys.stderr)
|
||||||
from __future__ import annotations
|
sys.exit(1)
|
||||||
|
|
||||||
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()
|
|
||||||
@ -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)
|
|
||||||
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -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-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('--log-file', type=Path, help='Log file path')
|
||||||
parser.add_argument('--project-name', help='Project name (for compatibility, not used)')
|
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
|
# New compilation option
|
||||||
parser.add_argument('--compile-acd', action='store_true', help='Compile L5X files to ACD after generation')
|
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:
|
if args.excel_file:
|
||||||
unified_args.extend(['--excel-file', str(args.excel_file)])
|
unified_args.extend(['--excel-file', str(args.excel_file)])
|
||||||
|
|
||||||
if args.zones:
|
# Zones option removed
|
||||||
unified_args.extend(['--zones', args.zones])
|
|
||||||
|
|
||||||
# Add the 'all' command
|
# Add the 'all' command
|
||||||
unified_args.append('all')
|
unified_args.append('all')
|
||||||
|
|||||||
Binary file not shown.
BIN
Routines Generator/src/__pycache__/cli.cpython-312.pyc
Normal file
BIN
Routines Generator/src/__pycache__/cli.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Routines Generator/src/__pycache__/xml_builder.cpython-312.pyc
Normal file
BIN
Routines Generator/src/__pycache__/xml_builder.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,251 +1,310 @@
|
|||||||
"""Base Generator Classes using Template Method Pattern.
|
"""Base Generator Classes using Template Method Pattern.
|
||||||
|
|
||||||
Provides structured, extensible base classes for PLC program generation
|
Provides structured, extensible base classes for PLC program generation
|
||||||
using XML Builder and Plugin systems.
|
using XML Builder and Plugin systems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
from .config import GeneratorConfig
|
from .config import GeneratorConfig
|
||||||
from .data_loader import DataLoader
|
from .data_loader import DataLoader
|
||||||
from .xml_builder import L5XBuilder, L5XBuilderFactory
|
from .xml_builder import L5XBuilder, L5XBuilderFactory
|
||||||
from .plugin_system import RoutineManager, RoutineContext
|
from .plugin_system import RoutineManager, RoutineContext
|
||||||
from .logging_config import get_logger
|
from .logging_config import get_logger
|
||||||
|
|
||||||
class BaseGenerator(ABC):
|
class BaseGenerator(ABC):
|
||||||
"""Base class implementing the Template Method pattern for PLC generation."""
|
"""Base class implementing the Template Method pattern for PLC generation."""
|
||||||
|
|
||||||
def __init__(self, config: GeneratorConfig, data_loader: DataLoader,
|
def __init__(self, config: GeneratorConfig, data_loader: DataLoader,
|
||||||
xml_builder_factory: L5XBuilderFactory):
|
xml_builder_factory: L5XBuilderFactory):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.data_loader = data_loader
|
self.data_loader = data_loader
|
||||||
self.xml_builder_factory = xml_builder_factory
|
self.xml_builder_factory = xml_builder_factory
|
||||||
self.logger = get_logger(self.__class__.__name__)
|
self.logger = get_logger(self.__class__.__name__)
|
||||||
|
|
||||||
# Will be set during generation
|
# Will be set during generation
|
||||||
self.builder: Optional[L5XBuilder] = None
|
self.builder: Optional[L5XBuilder] = None
|
||||||
self.routine_manager: Optional[RoutineManager] = None
|
self.routine_manager: Optional[RoutineManager] = None
|
||||||
|
|
||||||
def generate(self) -> ET.Element:
|
def generate(self) -> ET.Element:
|
||||||
"""Template method for generating PLC programs.
|
"""Template method for generating PLC programs.
|
||||||
|
|
||||||
This defines the algorithm structure while allowing subclasses
|
This defines the algorithm structure while allowing subclasses
|
||||||
to customize specific steps.
|
to customize specific steps.
|
||||||
"""
|
"""
|
||||||
self.logger.info(f"Starting {self.__class__.__name__} generation")
|
self.logger.info(f"Starting {self.__class__.__name__} generation")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Step 1: Create XML structure
|
# Step 1: Create XML structure
|
||||||
self.builder = self.create_xml_structure()
|
self.builder = self.create_xml_structure()
|
||||||
|
|
||||||
# Step 2: Add controller-level elements
|
# Step 2: Add controller-level elements
|
||||||
self.add_controller_elements()
|
self.add_controller_elements()
|
||||||
|
|
||||||
# Step 3: Create routine context and manager
|
# Step 3: Create routine context and manager
|
||||||
self.setup_routine_manager()
|
self.setup_routine_manager()
|
||||||
|
|
||||||
# Step 4: Generate routines
|
# Step 4: Generate routines
|
||||||
self.generate_routines()
|
self.generate_routines()
|
||||||
|
|
||||||
# Step 5: Add program-level elements
|
# Step 5: Add program-level elements
|
||||||
self.add_program_elements()
|
self.add_program_elements()
|
||||||
|
|
||||||
# Step 6: Finalize
|
# Step 6: Finalize
|
||||||
self.finalize_generation()
|
self.finalize_generation()
|
||||||
|
|
||||||
xml_tree = self.builder.build()
|
xml_tree = self.builder.build()
|
||||||
self.logger.info(f"Successfully completed {self.__class__.__name__} generation")
|
self.logger.info(f"Successfully completed {self.__class__.__name__} generation")
|
||||||
return xml_tree
|
return xml_tree
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to generate {self.__class__.__name__}: {e}")
|
self.logger.error(f"Failed to generate {self.__class__.__name__}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create_xml_structure(self) -> L5XBuilder:
|
def create_xml_structure(self) -> L5XBuilder:
|
||||||
"""Create the basic XML structure for this program type."""
|
"""Create the basic XML structure for this program type."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_controller_elements(self) -> None:
|
def add_controller_elements(self) -> None:
|
||||||
"""Add controller-level elements. Override if needed."""
|
"""Add controller-level elements. Override if needed."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setup_routine_manager(self) -> None:
|
def setup_routine_manager(self) -> None:
|
||||||
"""Set up the routine manager with proper context."""
|
"""Set up the routine manager with proper context."""
|
||||||
if not self.builder:
|
if not self.builder:
|
||||||
raise ValueError("XML builder must be created first")
|
raise ValueError("XML builder must be created first")
|
||||||
|
|
||||||
context = RoutineContext(
|
context = RoutineContext(
|
||||||
data_loader=self.data_loader,
|
data_loader=self.data_loader,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
routines_element=self.builder.get_routines_section(),
|
routines_element=self.builder.get_routines_section(),
|
||||||
program_element=self.builder.get_program_element(),
|
program_element=self.builder.get_program_element(),
|
||||||
metadata=self.get_context_metadata()
|
metadata=self.get_context_metadata()
|
||||||
)
|
)
|
||||||
|
|
||||||
from .plugin_system import RoutineManager, get_default_registry
|
from .plugin_system import RoutineManager, get_default_registry
|
||||||
self.routine_manager = RoutineManager(context, get_default_registry())
|
self.routine_manager = RoutineManager(context, get_default_registry())
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generate_routines(self) -> None:
|
def generate_routines(self) -> None:
|
||||||
"""Generate the routines for this program type."""
|
"""Generate the routines for this program type."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_program_elements(self) -> None:
|
def add_program_elements(self) -> None:
|
||||||
"""Add program-level elements. Override if needed."""
|
"""Add program-level elements. Override if needed."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def finalize_generation(self) -> None:
|
def finalize_generation(self) -> None:
|
||||||
"""Finalize the generation process. Override if needed."""
|
"""Finalize the generation process. Override if needed."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_context_metadata(self) -> Dict[str, Any]:
|
def get_context_metadata(self) -> Dict[str, Any]:
|
||||||
"""Get metadata for routine context. Override to add custom metadata."""
|
"""Get metadata for routine context. Override to add custom metadata."""
|
||||||
return {
|
return {
|
||||||
'generator_type': self.__class__.__name__,
|
'generator_type': self.__class__.__name__,
|
||||||
'config': self.config,
|
'config': self.config,
|
||||||
'excel_file': str(self.data_loader.excel_path)
|
'excel_file': str(self.data_loader.excel_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_xml_string(self) -> str:
|
def build_xml_string(self) -> str:
|
||||||
"""Build and return the formatted XML string."""
|
"""Build and return the formatted XML string."""
|
||||||
if not self.builder:
|
if not self.builder:
|
||||||
raise ValueError("Generation must be completed first")
|
raise ValueError("Generation must be completed first")
|
||||||
return self.builder.build_xml_string()
|
return self.builder.build_xml_string()
|
||||||
|
|
||||||
def write(self, output_file: str | Path) -> None:
|
def write(self, output_file: str | Path) -> None:
|
||||||
"""Write the generated XML to a file."""
|
"""Write the generated XML to a file."""
|
||||||
if not self.builder:
|
if not self.builder:
|
||||||
# Generate if not already done
|
# Generate if not already done
|
||||||
self.generate()
|
self.generate()
|
||||||
self.builder.write_to_file(output_file)
|
self.builder.write_to_file(output_file)
|
||||||
self.logger.info(f"Written {self.__class__.__name__} to {output_file}")
|
self.logger.info(f"Written {self.__class__.__name__} to {output_file}")
|
||||||
|
|
||||||
class SafetyProgramGenerator(BaseGenerator):
|
class SafetyProgramGenerator(BaseGenerator):
|
||||||
"""Template Method implementation for SafetyProgram generation."""
|
"""Template Method implementation for SafetyProgram generation."""
|
||||||
|
|
||||||
def create_xml_structure(self) -> L5XBuilder:
|
def create_xml_structure(self) -> L5XBuilder:
|
||||||
"""Create SafetyProgram XML structure."""
|
"""Create SafetyProgram XML structure."""
|
||||||
self.logger.debug("Creating SafetyProgram XML structure")
|
self.logger.debug("Creating SafetyProgram XML structure")
|
||||||
return self.xml_builder_factory.create_safety_program_builder("SafetyProgram")
|
return self.xml_builder_factory.create_safety_program_builder("SafetyProgram")
|
||||||
|
|
||||||
def generate_routines(self) -> None:
|
def generate_routines(self) -> None:
|
||||||
"""Generate safety routines."""
|
"""Generate safety routines."""
|
||||||
if not self.routine_manager:
|
if not self.routine_manager:
|
||||||
raise ValueError("Routine manager must be set up first")
|
raise ValueError("Routine manager must be set up first")
|
||||||
|
|
||||||
self.logger.info("Generating safety routines...")
|
self.logger.info("Generating safety routines...")
|
||||||
|
|
||||||
# Generate safety-specific routines
|
# If a config-driven routine plan is provided, use only entries targeting SafetyProgram
|
||||||
safety_routines = [
|
if getattr(self.config, 'routine_plan', None):
|
||||||
'inputs',
|
results = {}
|
||||||
'outputs',
|
for entry in sorted(
|
||||||
'resets',
|
[e for e in self.config.routine_plan if e.enabled and e.program == 'SafetyProgram'],
|
||||||
'estops',
|
key=lambda e: e.order
|
||||||
'zones'
|
):
|
||||||
]
|
# enrich context with per-routine params/filters
|
||||||
|
self.routine_manager.context.metadata['params'] = entry.params or {}
|
||||||
results = {}
|
self.routine_manager.context.metadata['filters'] = self.config.filters.for_routine(entry.name)
|
||||||
for routine_name in safety_routines:
|
results[entry.name] = self.routine_manager.generate_routine(entry.plugin)
|
||||||
results[routine_name] = self.routine_manager.generate_routine(routine_name)
|
self.logger.info(f"Safety routine generation results: {results}")
|
||||||
|
return
|
||||||
self.logger.info(f"Safety routine generation results: {results}")
|
|
||||||
|
# Fallback: default fixed set
|
||||||
def add_program_elements(self) -> None:
|
safety_routines = ['inputs', 'outputs', 'resets', 'estops']
|
||||||
"""Add safety-specific program elements."""
|
results = {name: self.routine_manager.generate_routine(name) for name in safety_routines}
|
||||||
if not self.builder:
|
self.logger.info(f"Safety routine generation results: {results}")
|
||||||
raise ValueError("XML builder must be created first")
|
|
||||||
|
def add_program_elements(self) -> None:
|
||||||
# Add safety signatures
|
"""Add safety-specific program elements."""
|
||||||
self.builder.add_safety_signatures()
|
if not self.builder:
|
||||||
|
raise ValueError("XML builder must be created first")
|
||||||
# Add safety tag map if needed
|
|
||||||
self._add_safety_tag_map()
|
# Add safety signatures
|
||||||
|
self.builder.add_safety_signatures()
|
||||||
def _add_safety_tag_map(self) -> None:
|
|
||||||
"""Add safety tag map to the program."""
|
# Add safety tag map if needed
|
||||||
program_element = self.builder.get_program_element()
|
self._add_safety_tag_map()
|
||||||
|
|
||||||
# Get safety tags from data
|
# Add zones routine from configuration if available
|
||||||
safety_tags = self.data_loader.safety_tags_from_pb
|
try:
|
||||||
|
zones_df = self.data_loader.zones
|
||||||
if safety_tags:
|
if zones_df is not None:
|
||||||
from .generators.safety_program import create_safety_tag_map
|
from .routines.zones import create_zones_routine
|
||||||
create_safety_tag_map(program_element, safety_tags, set())
|
create_zones_routine(self.builder.get_routines_section(), zones_df, self.data_loader.epc)
|
||||||
self.logger.debug(f"Added safety tag map with {len(safety_tags)} tags")
|
self.logger.info("Added R030_ZONES routine from zones.json")
|
||||||
|
except Exception:
|
||||||
class MainProgramGenerator(BaseGenerator):
|
# Zones are optional; proceed without blocking generation
|
||||||
"""Template Method implementation for MainProgram generation."""
|
pass
|
||||||
|
|
||||||
def create_xml_structure(self) -> L5XBuilder:
|
# Ensure a MainRoutine exists and references generated safety routines
|
||||||
"""Create MainProgram XML structure."""
|
# The ProgramAttributes set MainRoutineName to config.routines.main_routine_name
|
||||||
self.logger.debug("Creating MainProgram XML structure")
|
# so we must create that routine here.
|
||||||
return self.xml_builder_factory.create_main_program_builder("MainProgram")
|
program_el = self.builder.get_program_element()
|
||||||
|
routines_el = self.builder.get_routines_section()
|
||||||
def add_controller_elements(self) -> None:
|
|
||||||
"""Add controller tags for MainProgram."""
|
from .config import get_config
|
||||||
if not self.builder:
|
cfg_local = get_config()
|
||||||
raise ValueError("XML builder must be created first")
|
main_routine_name = cfg_local.routines.main_routine_name
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
self.logger.debug("Adding controller tags...")
|
|
||||||
|
# Create MainRoutine only if it does not already exist
|
||||||
# Generate and add controller tags
|
if not any(r.get('Name') == main_routine_name for r in routines_el.findall('Routine')):
|
||||||
from .writers.xml_tag_writer import create_limited_tag_xml_elements
|
routine = ET.SubElement(routines_el, 'Routine', Name=main_routine_name, Type='RLL')
|
||||||
tag_elements = create_limited_tag_xml_elements(
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
self.data_loader.excel_path,
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
data_loader=self.data_loader
|
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
|
||||||
self.builder.add_controller_tags(tag_elements)
|
calls = [
|
||||||
self.logger.info(f"Added {len(tag_elements)} controller tags")
|
nm.get('inputs', 'R010_INPUTS'),
|
||||||
|
nm.get('outputs', 'R011_OUTPUTS'),
|
||||||
def generate_routines(self) -> None:
|
nm.get('resets', 'R012_RESETS'),
|
||||||
"""Generate main program routines."""
|
nm.get('estops', 'R020_ESTOPS'),
|
||||||
if not self.routine_manager:
|
nm.get('zones', 'R030_ZONES'),
|
||||||
raise ValueError("Routine manager must be set up first")
|
]
|
||||||
|
text.text = '[' + ' ,'.join(f'JSR({c},0)' for c in calls) + ' ];'
|
||||||
self.logger.info("Generating main program routines...")
|
|
||||||
|
def _add_safety_tag_map(self) -> None:
|
||||||
# Generate main program routines
|
"""Add safety tag map to the program."""
|
||||||
main_routines = [
|
program_element = self.builder.get_program_element()
|
||||||
'main_routine',
|
|
||||||
'safety_tag_map',
|
# Get safety tags from data
|
||||||
'estop_check'
|
safety_tags = self.data_loader.safety_tags_from_pb
|
||||||
]
|
|
||||||
|
if safety_tags:
|
||||||
results = {}
|
from .generators.safety_program import create_safety_tag_map
|
||||||
for routine_name in main_routines:
|
create_safety_tag_map(program_element, safety_tags, set())
|
||||||
results[routine_name] = self.routine_manager.generate_routine(routine_name)
|
self.logger.debug(f"Added safety tag map with {len(safety_tags)} tags")
|
||||||
|
|
||||||
self.logger.info(f"Main routine generation results: {results}")
|
class MainProgramGenerator(BaseGenerator):
|
||||||
|
"""Template Method implementation for MainProgram generation."""
|
||||||
class LegacyCompatibilityMixin:
|
|
||||||
"""Mixin to provide backward compatibility with legacy generators."""
|
def create_xml_structure(self) -> L5XBuilder:
|
||||||
|
"""Create MainProgram XML structure."""
|
||||||
def __init__(self, excel_path: str | Path, zones_dict: Optional[List[Dict[str, str]]] = None):
|
self.logger.debug("Creating MainProgram XML structure")
|
||||||
"""Legacy constructor interface."""
|
return self.xml_builder_factory.create_main_program_builder("MainProgram")
|
||||||
# This will be mixed with the new generators to maintain compatibility
|
|
||||||
from .config import get_config
|
def add_controller_elements(self) -> None:
|
||||||
from .data_loader import DataLoader
|
"""Add controller tags for MainProgram."""
|
||||||
from .xml_builder import L5XBuilderFactory
|
if not self.builder:
|
||||||
|
raise ValueError("XML builder must be created first")
|
||||||
config = get_config()
|
|
||||||
data_loader = DataLoader(excel_path=excel_path, zones_dict=zones_dict)
|
self.logger.debug("Adding controller tags...")
|
||||||
xml_builder_factory = L5XBuilderFactory(config)
|
|
||||||
|
# Generate and add controller tags
|
||||||
# Call the parent constructor
|
from .writers.xml_tag_writer import create_limited_tag_xml_elements
|
||||||
super().__init__(config, data_loader, xml_builder_factory)
|
tag_elements = create_limited_tag_xml_elements(
|
||||||
|
self.data_loader.excel_path,
|
||||||
# Backward compatible generator classes
|
data_loader=self.data_loader
|
||||||
class ModernSafetyProgramGenerator(LegacyCompatibilityMixin, SafetyProgramGenerator):
|
)
|
||||||
"""Modern SafetyProgram generator with legacy interface."""
|
|
||||||
pass
|
self.builder.add_controller_tags(tag_elements)
|
||||||
|
self.logger.info(f"Added {len(tag_elements)} controller tags")
|
||||||
class ModernMainProgramGenerator(LegacyCompatibilityMixin, MainProgramGenerator):
|
|
||||||
"""Modern MainProgram generator with legacy interface."""
|
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
|
pass
|
||||||
@ -7,8 +7,7 @@ from generators import (
|
|||||||
SafetyProgramGenerator,
|
SafetyProgramGenerator,
|
||||||
MainProgramGenerator,
|
MainProgramGenerator,
|
||||||
)
|
)
|
||||||
from writers import create_new_csv_with_tags, create_safety_tag_mapping
|
from writers import create_safety_tag_mapping
|
||||||
from writers.csv_writer import create_limited_csv_with_tags
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd_safety(args: argparse.Namespace) -> None:
|
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}")
|
print(f"Main L5X written to {args.output}")
|
||||||
|
|
||||||
|
|
||||||
def _cmd_csv(args: argparse.Namespace) -> None:
|
# CSV generation removed (deprecated)
|
||||||
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})")
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd_mapping(args: argparse.Namespace) -> None:
|
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:
|
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
|
# Check if ignore_estop1ok flag is set
|
||||||
ignore_estop1ok = getattr(args, 'ignore_estop1ok', False)
|
ignore_estop1ok = getattr(args, 'ignore_estop1ok', False)
|
||||||
@ -55,14 +52,7 @@ def _cmd_safety_only(args: argparse.Namespace) -> None:
|
|||||||
main_gen.write(main_output)
|
main_gen.write(main_output)
|
||||||
print(f"Limited Main L5X written to {main_output}")
|
print(f"Limited Main L5X written to {main_output}")
|
||||||
|
|
||||||
# Generate limited CSV with tags (requires original CSV)
|
# CSV generation removed
|
||||||
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")
|
|
||||||
|
|
||||||
# Create safety tag mapping for the limited mode
|
# Create safety tag mapping for the limited mode
|
||||||
from data_loader import LimitedDataLoader
|
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"Limited safety tag mapping written to {mapping_output}")
|
||||||
|
|
||||||
print(f"\n[SUCCESS] Safety-only mode complete:")
|
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" - 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 = {
|
_DEF_OUTPUTS = {
|
||||||
'safety': 'SafetyProgram_Generated.L5X',
|
'safety': 'SafetyProgram_Generated.L5X',
|
||||||
'main': 'MainProgram_Generated.L5X',
|
'main': 'MainProgram_Generated.L5X',
|
||||||
'csv': 'MTN6_MCM01_Controller_Tags_Complete.CSV',
|
|
||||||
'mapping': 'SafetyTagMapping.txt',
|
'mapping': 'SafetyTagMapping.txt',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +101,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
m.add_argument('-o', '--output', default=_DEF_OUTPUTS['main'])
|
m.add_argument('-o', '--output', default=_DEF_OUTPUTS['main'])
|
||||||
m.set_defaults(func=_cmd_main)
|
m.set_defaults(func=_cmd_main)
|
||||||
|
|
||||||
# CSV
|
# CSV subcommand removed
|
||||||
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)
|
|
||||||
|
|
||||||
# Mapping
|
# Mapping
|
||||||
mp = sub.add_parser('mapping', help='Generate SafetyTag mapping file')
|
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)
|
mp.set_defaults(func=_cmd_mapping)
|
||||||
|
|
||||||
# Safety-only mode
|
# 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('excel', help='Merged descriptor Excel file')
|
||||||
so.add_argument('--safety-output', help='Safety L5X output file (default: SafetyProgram_Limited.L5X)')
|
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('--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)')
|
# CSV options removed
|
||||||
so.add_argument('--original-csv', help='Original CSV file (default: MTN6_MCM01_Controller_Tags_Original.CSV)')
|
|
||||||
so.add_argument('--mapping-output', help='Safety tag mapping output file (default: SafetyTagMapping_Limited.txt)')
|
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)
|
so.set_defaults(func=_cmd_safety_only)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|||||||
@ -2,20 +2,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, List, Any
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileConfig:
|
class FileConfig:
|
||||||
"""File path configuration."""
|
"""File path configuration."""
|
||||||
excel_file: Path = Path('DESC_IP_MERGED.xlsx')
|
excel_file: Path = Path('DESC_IP_MERGED.xlsx')
|
||||||
original_csv: Path = Path('MTN6_MCM01_Controller_Tags_Original.CSV')
|
|
||||||
output_dir: Path = Path('.')
|
output_dir: Path = Path('.')
|
||||||
|
|
||||||
# Output file names
|
# Output file names
|
||||||
safety_l5x: str = 'SafetyProgram_Generated.L5X'
|
safety_l5x: str = 'SafetyProgram_Generated.L5X'
|
||||||
main_l5x: str = 'MainProgram_Generated.L5X'
|
main_l5x: str = 'MainProgram_Generated.L5X'
|
||||||
complete_csv: str = 'MTN6_MCM01_Controller_Tags_Complete.CSV'
|
|
||||||
mapping_txt: str = 'SafetyTagMapping.txt'
|
mapping_txt: str = 'SafetyTagMapping.txt'
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -36,6 +34,8 @@ class ExtractionConfig:
|
|||||||
rst_desc_excludes: list[str] = field(default_factory=lambda: ['LIGHT'])
|
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_patterns: list[str] = field(default_factory=lambda: ['S1_PB', 'S2_PB'])
|
||||||
rst_desca_endings: list[str] = field(default_factory=lambda: ['SPB'])
|
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 extraction rules
|
||||||
sto_tagname_patterns: list[str] = field(default_factory=lambda: ['VFD'])
|
sto_tagname_patterns: list[str] = field(default_factory=lambda: ['VFD'])
|
||||||
@ -44,14 +44,88 @@ class ExtractionConfig:
|
|||||||
# EPC extraction rules
|
# EPC extraction rules
|
||||||
epc_desca_patterns: list[str] = field(default_factory=lambda: ['EPC', 'ESTOP'])
|
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
|
@dataclass
|
||||||
class RoutineConfig:
|
class RoutineConfig:
|
||||||
"""Routine generation configuration."""
|
"""Routine generation configuration."""
|
||||||
|
|
||||||
# Routine naming patterns
|
# Routine naming patterns
|
||||||
main_routine_name: str = 'MainRoutine'
|
main_routine_name: str = 'MainRoutine'
|
||||||
safety_tag_map_name: str = 'R000_SAFETY_TAG_MAP'
|
safety_tag_map_name: str = 'R130_SAFETY_TAG_MAP'
|
||||||
estop_check_name: str = 'R100_ESTOP_CHECK'
|
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
|
# Safety routine names
|
||||||
inputs_routine: str = 'R010_INPUTS'
|
inputs_routine: str = 'R010_INPUTS'
|
||||||
@ -64,6 +138,61 @@ class RoutineConfig:
|
|||||||
mcm_safety_tag: str = 'MCM_S_PB'
|
mcm_safety_tag: str = 'MCM_S_PB'
|
||||||
mcm_epb_tag: str = 'MCM_EPB_DCS_CTRL.O1'
|
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
|
@dataclass
|
||||||
class GeneratorConfig:
|
class GeneratorConfig:
|
||||||
"""Complete configuration for the generator system."""
|
"""Complete configuration for the generator system."""
|
||||||
@ -72,6 +201,12 @@ class GeneratorConfig:
|
|||||||
xml: XMLConfig = field(default_factory=XMLConfig)
|
xml: XMLConfig = field(default_factory=XMLConfig)
|
||||||
extraction: ExtractionConfig = field(default_factory=ExtractionConfig)
|
extraction: ExtractionConfig = field(default_factory=ExtractionConfig)
|
||||||
routines: RoutineConfig = field(default_factory=RoutineConfig)
|
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
|
@classmethod
|
||||||
def from_file(cls, config_path: Path) -> 'GeneratorConfig':
|
def from_file(cls, config_path: Path) -> 'GeneratorConfig':
|
||||||
@ -90,17 +225,44 @@ class GeneratorConfig:
|
|||||||
# Convert specific path fields to Path objects
|
# Convert specific path fields to Path objects
|
||||||
if 'excel_file' in files_data:
|
if 'excel_file' in files_data:
|
||||||
files_data['excel_file'] = Path(files_data['excel_file'])
|
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:
|
if 'output_dir' in files_data:
|
||||||
files_data['output_dir'] = Path(files_data['output_dir'])
|
files_data['output_dir'] = Path(files_data['output_dir'])
|
||||||
|
|
||||||
files = FileConfig(**files_data)
|
files = FileConfig(**files_data)
|
||||||
xml = XMLConfig(**config_data.get('xml', {}))
|
xml = XMLConfig(**config_data.get('xml', {}))
|
||||||
extraction = ExtractionConfig(**config_data.get('extraction', {}))
|
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:
|
def save_to_file(self, config_path: Path) -> None:
|
||||||
"""Save configuration to JSON file."""
|
"""Save configuration to JSON file."""
|
||||||
@ -108,8 +270,11 @@ class GeneratorConfig:
|
|||||||
'files': self.files.__dict__,
|
'files': self.files.__dict__,
|
||||||
'xml': self.xml.__dict__,
|
'xml': self.xml.__dict__,
|
||||||
'extraction': self.extraction.__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
|
# Convert Path objects to strings for JSON serialization
|
||||||
def convert_paths(obj):
|
def convert_paths(obj):
|
||||||
|
|||||||
@ -26,27 +26,41 @@ class GeneratorContainer:
|
|||||||
self._logger.debug("Creating DataLoader instance", excel_file=str(self.config.files.excel_file))
|
self._logger.debug("Creating DataLoader instance", excel_file=str(self.config.files.excel_file))
|
||||||
self._instances['data_loader'] = DataLoader(
|
self._instances['data_loader'] = DataLoader(
|
||||||
excel_path=self.config.files.excel_file,
|
excel_path=self.config.files.excel_file,
|
||||||
zones_dict=self.zones_dict
|
zones_dict=self.zones_dict or []
|
||||||
)
|
)
|
||||||
return self._instances['data_loader']
|
return self._instances['data_loader']
|
||||||
|
|
||||||
|
# Zones support removed: no config file or inline loading
|
||||||
|
|
||||||
def get_safety_program_generator(self):
|
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:
|
if 'safety_program_generator' not in self._instances:
|
||||||
from .generators.safety_program import LimitedSafetyProgramGenerator
|
from .base_generator import ModernSafetyProgramGenerator
|
||||||
self._logger.debug("Creating SafetyProgramGenerator instance")
|
self._logger.warning(
|
||||||
self._instances['safety_program_generator'] = LimitedSafetyProgramGenerator(
|
"LimitedSafetyProgramGenerator is deprecated; using ModernSafetyProgramGenerator"
|
||||||
|
)
|
||||||
|
self._instances['safety_program_generator'] = ModernSafetyProgramGenerator(
|
||||||
excel_path=self.config.files.excel_file,
|
excel_path=self.config.files.excel_file,
|
||||||
zones_dict=self.zones_dict
|
zones_dict=self.zones_dict
|
||||||
)
|
)
|
||||||
return self._instances['safety_program_generator']
|
return self._instances['safety_program_generator']
|
||||||
|
|
||||||
def get_main_program_generator(self):
|
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:
|
if 'main_program_generator' not in self._instances:
|
||||||
from .generators.main_program import LimitedMainProgramGenerator
|
from .base_generator import ModernMainProgramGenerator
|
||||||
self._logger.debug("Creating MainProgramGenerator instance")
|
self._logger.warning(
|
||||||
self._instances['main_program_generator'] = LimitedMainProgramGenerator(
|
"LimitedMainProgramGenerator is deprecated; using ModernMainProgramGenerator"
|
||||||
|
)
|
||||||
|
self._instances['main_program_generator'] = ModernMainProgramGenerator(
|
||||||
excel_path=self.config.files.excel_file,
|
excel_path=self.config.files.excel_file,
|
||||||
zones_dict=self.zones_dict
|
zones_dict=self.zones_dict
|
||||||
)
|
)
|
||||||
@ -109,12 +123,7 @@ class GeneratorContainer:
|
|||||||
return self._instances['tag_writer']
|
return self._instances['tag_writer']
|
||||||
|
|
||||||
def get_csv_writer(self):
|
def get_csv_writer(self):
|
||||||
"""Get or create CSVWriter instance."""
|
raise NotImplementedError("CSV generation is removed. Use config-driven XML generation only.")
|
||||||
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']
|
|
||||||
|
|
||||||
def get_mapping_writer(self):
|
def get_mapping_writer(self):
|
||||||
"""Get or create MappingWriter instance."""
|
"""Get or create MappingWriter instance."""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -2,16 +2,30 @@
|
|||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..data_loader import DataLoader
|
from ..data_loader import DataLoader
|
||||||
|
from ..config import get_config
|
||||||
from ..routines.estop_check import create_estop_check_routine
|
from ..routines.estop_check import create_estop_check_routine
|
||||||
from ..routines.dpm import create_dpm_routine
|
from ..routines.dpm import create_dpm_routine
|
||||||
from ..routines.fiom import create_fiom_routine
|
from ..routines.fiom import create_fiom_routine
|
||||||
from ..routines.fioh import create_fioh_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 ..utils.common import format_xml_to_match_original
|
||||||
from ..writers.xml_tag_writer import create_limited_tag_xml_elements
|
from ..writers.xml_tag_writer import create_limited_tag_xml_elements
|
||||||
|
|
||||||
@ -36,26 +50,31 @@ class LimitedMainProgramGenerator:
|
|||||||
else:
|
else:
|
||||||
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
|
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
|
||||||
|
|
||||||
|
cfg = get_config()
|
||||||
root = ET.Element('RSLogix5000Content', attrib={
|
root = ET.Element('RSLogix5000Content', attrib={
|
||||||
'SchemaRevision': '1.0',
|
'SchemaRevision': cfg.xml.schema_revision,
|
||||||
'SoftwareRevision': '36.00',
|
'SoftwareRevision': cfg.xml.software_revision,
|
||||||
'TargetName': 'MainProgram',
|
'TargetName': 'MainProgram',
|
||||||
'TargetType': 'Program',
|
'TargetType': 'Program',
|
||||||
'TargetClass': 'Standard',
|
'TargetClass': cfg.xml.target_class,
|
||||||
'ContainsContext': 'true',
|
'ContainsContext': 'true',
|
||||||
'ExportDate': xl_time,
|
'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
|
# Add empty AddOnInstructionDefinitions element
|
||||||
ET.SubElement(controller, 'AddOnInstructionDefinitions')
|
ET.SubElement(controller, 'AddOnInstructionDefinitions')
|
||||||
|
|
||||||
# Add Tags section at controller level
|
# Add Tags section at controller level
|
||||||
tags_section = ET.SubElement(controller, 'Tags')
|
tags_section = ET.SubElement(controller, 'Tags')
|
||||||
print(" Generating limited controller tags...", flush=True)
|
print(" Generating limited controller tags (safety-only)...", flush=True)
|
||||||
tag_elements = create_limited_tag_xml_elements(self.loader.excel_path, data_loader=self.loader)
|
# 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
|
# Add all tag elements to the Tags section
|
||||||
for tag_elem in tag_elements:
|
for tag_elem in tag_elements:
|
||||||
@ -65,54 +84,118 @@ class LimitedMainProgramGenerator:
|
|||||||
|
|
||||||
programs = ET.SubElement(controller, 'Programs', Use='Context')
|
programs = ET.SubElement(controller, 'Programs', Use='Context')
|
||||||
program = ET.SubElement(programs, 'Program', Use='Target', Name='MainProgram',
|
program = ET.SubElement(programs, 'Program', Use='Target', Name='MainProgram',
|
||||||
TestEdits='false', MainRoutineName='MainRoutine', Disabled='false',
|
TestEdits='false', MainRoutineName=cfg.routines.main_routine_name, Disabled='false',
|
||||||
Class='Standard', UseAsFolder='false')
|
Class=cfg.xml.target_class, UseAsFolder='false')
|
||||||
ET.SubElement(program, 'Tags')
|
ET.SubElement(program, 'Tags')
|
||||||
routines_el = ET.SubElement(program, 'Routines')
|
routines_el = ET.SubElement(program, 'Routines')
|
||||||
|
|
||||||
print(" Creating limited MainProgram routines...", flush=True)
|
print(" Creating limited MainProgram routines...", flush=True)
|
||||||
|
|
||||||
self._create_main_routine(routines_el)
|
# Config-driven routine plan support (optional)
|
||||||
print(" [SUCCESS] Created MainRoutine", flush=True)
|
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)
|
def _apply_filters(df, flt: dict):
|
||||||
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
|
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)
|
# Respect routine plan strictly: if a plan exists in config, do NOT fall back to defaults
|
||||||
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
|
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
|
return root
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_main_routine(routines_el: ET.Element) -> None:
|
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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
text = ET.SubElement(rung, 'Text')
|
||||||
# Only JSR calls for safety_tag_map and estop_check
|
# Only JSR calls for safety_tag_map and estop_check (config-driven names)
|
||||||
text.text = '[JSR(R000_SAFETY_TAG_MAP,0) ,JSR(R100_ESTOP_CHECK,0) ];'
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
# Determine subsystem from Excel file path to get the correct MCM tag name
|
# Determine subsystem from Excel file path to get the correct MCM tag name
|
||||||
import re
|
import re
|
||||||
excel_path_str = str(self.loader.excel_path)
|
excel_path_str = str(self.loader.excel_path)
|
||||||
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
|
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
|
# MCM mapping rung with subsystem-specific naming
|
||||||
rung_num = 0
|
rung_num = 0
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
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
|
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
|
pb_count = 0
|
||||||
for _, row in self.loader.rst.iterrows():
|
for _, row in df.iterrows():
|
||||||
if not isinstance(row['DESCA'], str):
|
if not isinstance(row['DESCA'], str):
|
||||||
continue
|
continue
|
||||||
# Skip GS1 patterns
|
# Skip GS1 patterns
|
||||||
@ -153,18 +236,22 @@ class DescIPMainProgramGenerator:
|
|||||||
def _build_xml_tree(self) -> ET.Element:
|
def _build_xml_tree(self) -> ET.Element:
|
||||||
"""Build MainProgram XML tree using DESC_IP extracted data."""
|
"""Build MainProgram XML tree using DESC_IP extracted data."""
|
||||||
|
|
||||||
# Build basic XML structure
|
# Build basic XML structure (config-driven)
|
||||||
root = ET.Element('RSLogix5000Content')
|
cfg = get_config()
|
||||||
root.set('SchemaRevision', '1.0')
|
from datetime import datetime
|
||||||
root.set('SoftwareRevision', '36.00')
|
xl_time = datetime.now().strftime('%a %b %d %H:%M:%S %Y')
|
||||||
root.set('TargetName', 'MainProgram')
|
root = ET.Element('RSLogix5000Content', attrib={
|
||||||
root.set('TargetType', 'Program')
|
'SchemaRevision': cfg.xml.schema_revision,
|
||||||
root.set('ContainsContext', 'true')
|
'SoftwareRevision': cfg.xml.software_revision,
|
||||||
root.set('ExportDate', 'Wed Jul 03 11:47:56 2024')
|
'TargetName': 'MainProgram',
|
||||||
|
'TargetType': 'Program',
|
||||||
controller = ET.SubElement(root, 'Controller', Use='Context', Name='MainProgram')
|
'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')
|
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')
|
routines_el = ET.SubElement(program, 'Routines')
|
||||||
|
|
||||||
# Main routine with JSRs (safety-only)
|
# Main routine with JSRs (safety-only)
|
||||||
@ -178,24 +265,35 @@ class DescIPMainProgramGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_main_routine(routines_el: ET.Element) -> None:
|
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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
text = ET.SubElement(rung, 'Text')
|
||||||
# Only JSR calls for safety_tag_map and estop_check
|
# Only JSR calls for safety_tag_map and estop_check (config-driven)
|
||||||
text.text = '[JSR(R000_SAFETY_TAG_MAP,0) ,JSR(R100_ESTOP_CHECK,0) ];'
|
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:
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
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')
|
rung = ET.SubElement(rll_content, 'Rung', Number=str(rung_num), Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
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
|
rung_num += 1
|
||||||
|
|
||||||
# Extract RST devices for push button mappings
|
# 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:
|
def _create_desc_ip_estop_check_routine(self, routines_el: ET.Element) -> None:
|
||||||
"""Create R100_ESTOP_CHECK routine from DESC_IP extracted data."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
rung_num = 0
|
||||||
@ -283,7 +383,7 @@ class FullMainProgramGenerator(LimitedMainProgramGenerator):
|
|||||||
'ExportOptions': 'References NoRawData L5KData DecoratedData Context Dependencies ForceProtectedEncoding AllProjDocTrans',
|
'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
|
# Add empty AddOnInstructionDefinitions element
|
||||||
ET.SubElement(controller, 'AddOnInstructionDefinitions')
|
ET.SubElement(controller, 'AddOnInstructionDefinitions')
|
||||||
@ -307,35 +407,172 @@ class FullMainProgramGenerator(LimitedMainProgramGenerator):
|
|||||||
routines_el = ET.SubElement(program, 'Routines')
|
routines_el = ET.SubElement(program, 'Routines')
|
||||||
|
|
||||||
print(" Creating full MainProgram routines...", flush=True)
|
print(" Creating full MainProgram routines...", flush=True)
|
||||||
|
|
||||||
self._create_main_routine_extended(routines_el)
|
# Config-driven support: if routine_plan is provided, honor entries for MainProgram
|
||||||
print(" [SUCCESS] Created MainRoutine with DPM calls", flush=True)
|
cfg = get_config()
|
||||||
|
plan_entries = [
|
||||||
self._create_safety_tag_map_routine(routines_el)
|
e for e in getattr(cfg, 'routine_plan', [])
|
||||||
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
|
if e.enabled and e.program == 'MainProgram'
|
||||||
|
]
|
||||||
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, self.loader.zones)
|
plan_entries.sort(key=lambda e: e.order)
|
||||||
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
|
|
||||||
|
def run_defaults():
|
||||||
# Add DPM routine
|
self._create_main_routine_extended(routines_el)
|
||||||
create_dpm_routine(routines_el, self.loader.desc_ip)
|
print(" [SUCCESS] Created MainRoutine with DPM calls", flush=True)
|
||||||
print(" [SUCCESS] Created R020_DPM routine", flush=True)
|
self._create_safety_tag_map_routine(routines_el)
|
||||||
|
print(" [SUCCESS] Created R000_SAFETY_TAG_MAP routine", flush=True)
|
||||||
# Add FIOM routine
|
create_estop_check_routine(routines_el, self.loader.epc, self.loader.sto, None)
|
||||||
create_fiom_routine(routines_el, self.loader.desc_ip)
|
print(" [SUCCESS] Created R100_ESTOP_CHECK routine", flush=True)
|
||||||
print(" [SUCCESS] Created R030_FIOM routine", flush=True)
|
create_dpm_routine(routines_el, self.loader.desc_ip)
|
||||||
|
print(" [SUCCESS] Created R020_DPM routine", flush=True)
|
||||||
# Add FIOH routine
|
create_fiom_routine(routines_el, self.loader.desc_ip)
|
||||||
create_fioh_routine(routines_el, self.loader.desc_ip)
|
print(" [SUCCESS] Created R030_FIOM routine", flush=True)
|
||||||
print(" [SUCCESS] Created R031_FIOH 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
|
return root
|
||||||
|
|
||||||
def _create_main_routine_extended(self, routines_el: ET.Element) -> None:
|
def _create_main_routine_extended(self, routines_el: ET.Element) -> None:
|
||||||
"""Create extended MainRoutine with JSR calls for all routines including DPM."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
text = ET.SubElement(rung, 'Text')
|
||||||
# Extended JSR calls including DPM, FIOM, and FIOH routines
|
# Extended JSR calls including DPM, FIOM, FIOH, APF, EXTENDO, and D2C routines using config names
|
||||||
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) ];'
|
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) + ' ];'
|
||||||
@ -2,16 +2,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..data_loader import DataLoader
|
from ..data_loader import DataLoader
|
||||||
|
from ..config import get_config
|
||||||
from ..routines.inputs import create_inputs_routine
|
from ..routines.inputs import create_inputs_routine
|
||||||
from ..routines.outputs import create_outputs_routine
|
from ..routines.outputs import create_outputs_routine
|
||||||
from ..routines.resets import create_resets_routine
|
from ..routines.resets import create_resets_routine
|
||||||
from ..routines.estops import create_estops_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.common import format_xml_to_match_original, natural_sort_key
|
||||||
from ..utils.tag_utils import device_base_from_desca
|
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)
|
print(" - WARNING: No safety tags found for SafetyTagMap.", flush=True)
|
||||||
return
|
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 = ET.SubElement(program_el, "SafetyTagMap")
|
||||||
safety_map.text = map_text
|
safety_map.text = map_text
|
||||||
@ -55,21 +58,22 @@ class LimitedSafetyProgramGenerator:
|
|||||||
else:
|
else:
|
||||||
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
|
xl_time = current_date.strftime('%a %b %d %H:%M:%S %Y')
|
||||||
|
|
||||||
|
cfg = get_config()
|
||||||
root = ET.Element('RSLogix5000Content', attrib={
|
root = ET.Element('RSLogix5000Content', attrib={
|
||||||
'SchemaRevision': '1.0',
|
'SchemaRevision': cfg.xml.schema_revision,
|
||||||
'SoftwareRevision': '36.00',
|
'SoftwareRevision': cfg.xml.software_revision,
|
||||||
'TargetName': 'SafetyProgram',
|
'TargetName': 'SafetyProgram',
|
||||||
'TargetType': 'Program',
|
'TargetType': 'Program',
|
||||||
'TargetClass': 'Safety',
|
'TargetClass': 'Safety',
|
||||||
'ContainsContext': 'true',
|
'ContainsContext': 'true',
|
||||||
'ExportDate': xl_time,
|
'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')
|
programs = ET.SubElement(controller, 'Programs', Use='Context')
|
||||||
program = ET.SubElement(programs, 'Program', Use='Target', Name='SafetyProgram',
|
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')
|
Class='Safety', UseAsFolder='false')
|
||||||
ET.SubElement(program, 'Tags')
|
ET.SubElement(program, 'Tags')
|
||||||
routines_el = ET.SubElement(program, 'Routines')
|
routines_el = ET.SubElement(program, 'Routines')
|
||||||
@ -82,7 +86,7 @@ class LimitedSafetyProgramGenerator:
|
|||||||
create_safety_tag_map(
|
create_safety_tag_map(
|
||||||
program,
|
program,
|
||||||
self.loader.safety_tags_from_pb,
|
self.loader.safety_tags_from_pb,
|
||||||
set() # No beacon tags in limited mode
|
set()
|
||||||
)
|
)
|
||||||
print(" [SUCCESS] Created SafetyTagMap", flush=True)
|
print(" [SUCCESS] Created SafetyTagMap", flush=True)
|
||||||
|
|
||||||
@ -90,21 +94,96 @@ class LimitedSafetyProgramGenerator:
|
|||||||
import re
|
import re
|
||||||
excel_path_str = str(self.loader.excel_path)
|
excel_path_str = str(self.loader.excel_path)
|
||||||
subsystem_match = re.search(r"(MCM\d+)", excel_path_str, re.IGNORECASE)
|
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)
|
# Config-driven support: if routine_plan is provided, honor entries for SafetyProgram
|
||||||
print(" [SUCCESS] Created R010_INPUTS routine", flush=True)
|
cfg = get_config()
|
||||||
|
plan_entries = [
|
||||||
create_resets_routine(routines_el, self.loader.rst, self.loader.epc, subsystem)
|
e for e in getattr(cfg, 'routine_plan', [])
|
||||||
print(" [SUCCESS] Created R012_RESETS routine", flush=True)
|
if e.enabled and e.program == 'SafetyProgram'
|
||||||
|
]
|
||||||
|
plan_entries.sort(key=lambda e: e.order)
|
||||||
|
|
||||||
# Only the 5 essential safety routines
|
def _apply_filters(df, flt: dict):
|
||||||
create_outputs_routine(routines_el, self.loader.zones, self.loader.sto)
|
if df is None or not isinstance(flt, dict) or df.empty:
|
||||||
create_estops_routine(routines_el, self.loader.epc, self.loader.rst, subsystem, ignore_estop1ok=self.ignore_estop1ok)
|
return df
|
||||||
print(" [SUCCESS] Created R020_ESTOPS routine", flush=True)
|
result = df
|
||||||
|
# Global-style includes/excludes
|
||||||
create_zones_routine(routines_el, self.loader.zones, self.loader.epc, ignore_estop1ok=self.ignore_estop1ok)
|
inc = flt.get('desca_include')
|
||||||
print(" [SUCCESS] Created R030_ZONES routine", flush=True)
|
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')
|
ET.SubElement(controller, 'SafetySignaturesHmac')
|
||||||
|
|
||||||
@ -112,12 +191,21 @@ class LimitedSafetyProgramGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_main_routine(routines_el: ET.Element) -> None:
|
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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
text = ET.SubElement(rung, 'Text')
|
||||||
# Only JSR calls for the 5 essential safety routines
|
# Only JSR calls for the 5 essential safety routines (config-driven names)
|
||||||
text.text = '[JSR(R010_INPUTS,0) ,JSR(R011_OUTPUTS,0) ,JSR(R012_RESETS,0) ,JSR(R020_ESTOPS,0) ,JSR(R030_ZONES,0) ];'
|
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:
|
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_resets_routine(routines_el, rst_data)
|
||||||
self._create_desc_ip_estops_routine(routines_el, epc_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)
|
# ZONES routine removed
|
||||||
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}")
|
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_main_routine(routines_el: ET.Element) -> None:
|
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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
rung = ET.SubElement(rll_content, 'Rung', Number='0', Type='N')
|
||||||
text = ET.SubElement(rung, 'Text')
|
text = ET.SubElement(rung, 'Text')
|
||||||
# Only JSR calls for essential safety routines
|
# Only JSR calls for essential safety routines (config-driven names)
|
||||||
text.text = '[JSR(R010_INPUTS,0) ,JSR(R011_OUTPUTS,0) ,JSR(R012_RESETS,0) ,JSR(R020_ESTOPS,0) ,JSR(R030_ZONES,0) ];'
|
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:
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
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:
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
rung_num = 0
|
||||||
|
|
||||||
# Get zones data to determine proper zone OK tags
|
|
||||||
zones_df = self.data_loader.zones
|
|
||||||
|
|
||||||
# Process STO data for safety outputs
|
# Process STO data for safety outputs
|
||||||
for _, row in sto_data.iterrows():
|
for _, row in sto_data.iterrows():
|
||||||
if pd.notna(row['IO_PATH']) and 'VFD' in str(row['TAGNAME']):
|
if pd.notna(row['IO_PATH']) and 'VFD' in str(row['TAGNAME']):
|
||||||
@ -233,13 +326,8 @@ class DescIPSafetyProgramGenerator:
|
|||||||
vfd_name = str(row['TAGNAME'])
|
vfd_name = str(row['TAGNAME'])
|
||||||
sto_tag = f"STO_{vfd_name}"
|
sto_tag = f"STO_{vfd_name}"
|
||||||
|
|
||||||
# Use proper zone OK tag - for MCM zone it should be EStop_MCM_OK
|
# Zone logic removed: gate by MCM OK
|
||||||
zone_ok_tag = "EStop_MCM_OK" # Default to MCM zone
|
zone_ok_tag = "EStop_MCM_OK"
|
||||||
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"
|
|
||||||
|
|
||||||
text.text = f"XIC({zone_ok_tag})OTE({sto_tag});"
|
text.text = f"XIC({zone_ok_tag})OTE({sto_tag});"
|
||||||
rung_num += 1
|
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:
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
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:
|
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."""
|
"""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')
|
rll_content = ET.SubElement(routine, 'RLLContent')
|
||||||
|
|
||||||
rung_num = 0
|
rung_num = 0
|
||||||
@ -285,42 +377,7 @@ class DescIPSafetyProgramGenerator:
|
|||||||
|
|
||||||
print(f" - Added {rung_num} E-stop rungs from DESC_IP data", flush=True)
|
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:
|
# ZONES routine removed
|
||||||
"""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)
|
|
||||||
|
|
||||||
def build_xml_string(self) -> str:
|
def build_xml_string(self) -> str:
|
||||||
"""Build and return the complete XML string."""
|
"""Build and return the complete XML string."""
|
||||||
|
|||||||
@ -42,6 +42,10 @@ class RoutinePlugin(ABC):
|
|||||||
def __init__(self, context: RoutineContext):
|
def __init__(self, context: RoutineContext):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.logger = get_logger(f"{self.__class__.__module__}.{self.__class__.__name__}")
|
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
|
@abstractmethod
|
||||||
def can_generate(self) -> bool:
|
def can_generate(self) -> bool:
|
||||||
@ -236,7 +240,12 @@ def get_default_registry() -> RoutineRegistry:
|
|||||||
"""Get the default plugin registry, creating it if necessary."""
|
"""Get the default plugin registry, creating it if necessary."""
|
||||||
global _default_registry
|
global _default_registry
|
||||||
if _default_registry is None:
|
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
|
return _default_registry
|
||||||
|
|
||||||
def register_plugin(plugin_class: Type[RoutinePlugin]) -> None:
|
def register_plugin(plugin_class: Type[RoutinePlugin]) -> None:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Routines Generator/src/routines/__pycache__/fpe.cpython-312.pyc
Normal file
BIN
Routines Generator/src/routines/__pycache__/fpe.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Routines Generator/src/routines/__pycache__/mcm.cpython-312.pyc
Normal file
BIN
Routines Generator/src/routines/__pycache__/mcm.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Routines Generator/src/routines/__pycache__/rack.cpython-312.pyc
Normal file
BIN
Routines Generator/src/routines/__pycache__/rack.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
116
Routines Generator/src/routines/apf.py
Normal file
116
Routines Generator/src/routines/apf.py
Normal 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)
|
||||||
83
Routines Generator/src/routines/cb_monitor.py
Normal file
83
Routines Generator/src/routines/cb_monitor.py
Normal 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)
|
||||||
184
Routines Generator/src/routines/d2c_chute.py
Normal file
184
Routines Generator/src/routines/d2c_chute.py
Normal 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)
|
||||||
@ -7,6 +7,7 @@ DPM modules are identified by PARTNUMBER containing 'OS30-002404-2S'.
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from ..utils.common import format_xml_to_match_original, natural_sort_key
|
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:
|
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.drop_duplicates(subset=['TAGNAME'])
|
||||||
dpm_df = dpm_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
dpm_df = dpm_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
||||||
|
|
||||||
# Create routine XML structure
|
# Create routine XML structure (name from config)
|
||||||
routine = ET.Element("Routine", attrib={"Name": "R020_DPM", "Type": "RLL"})
|
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")
|
rll_content = ET.SubElement(routine, "RLLContent")
|
||||||
|
|
||||||
# Rung 0: Documentation comment
|
# 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")
|
text = ET.SubElement(rung, "Text")
|
||||||
|
|
||||||
# Generate AOI_DPM call with standard parameters
|
# 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,MCM.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);"
|
|
||||||
text.text = aoi_call
|
text.text = aoi_call
|
||||||
|
|
||||||
rung_number += 1
|
rung_number += 1
|
||||||
@ -104,4 +109,27 @@ def extract_dpm_from_desc_ip(desc_ip_df: pd.DataFrame) -> pd.DataFrame:
|
|||||||
# Sort for deterministic output
|
# Sort for deterministic output
|
||||||
dpm_df = dpm_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
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)
|
||||||
@ -11,6 +11,7 @@ from ..utils.tag_utils import (
|
|||||||
device_base_from_desca,
|
device_base_from_desca,
|
||||||
is_estop_desca,
|
is_estop_desca,
|
||||||
)
|
)
|
||||||
|
from ..plugin_system import RoutinePlugin, register_plugin
|
||||||
|
|
||||||
__all__ = ["create_estop_check_routine"]
|
__all__ = ["create_estop_check_routine"]
|
||||||
|
|
||||||
@ -30,49 +31,54 @@ def _collect_all_stos(sto_df: pd.DataFrame) -> list[tuple[str, str]]:
|
|||||||
return all_stos
|
return all_stos
|
||||||
|
|
||||||
|
|
||||||
def _split_stos_by_zone(
|
def _split_stos(
|
||||||
all_stos: list[tuple[str, str]],
|
all_stos: list[tuple[str, str]],
|
||||||
device_zone: pd.Series,
|
device_prefix: str,
|
||||||
|
device_num: int,
|
||||||
) -> tuple[list[str], list[str]]:
|
) -> tuple[list[str], list[str]]:
|
||||||
"""Return (zone_stos, other_stos) IO paths based on zone boundaries."""
|
"""Split STOs into same-device-group vs other-device-group based on prefix/number proximity.
|
||||||
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])
|
|
||||||
|
|
||||||
zone_stos: list[str] = []
|
Heuristic: same group if same prefix and |sto_num - device_num| <= 50. Otherwise other.
|
||||||
other_stos: list[str] = []
|
This replaces explicit zone ranges.
|
||||||
|
"""
|
||||||
|
same_group: list[str] = []
|
||||||
|
other_group: list[str] = []
|
||||||
for sto_tag, sto_path in all_stos:
|
for sto_tag, sto_path in all_stos:
|
||||||
sto_parts = sto_tag.split("_")
|
parts = sto_tag.split('_')
|
||||||
sto_prefix = sto_parts[0]
|
if len(parts) < 2:
|
||||||
sto_num_part = sto_parts[1]
|
other_group.append(sto_path)
|
||||||
sto_num = int(sto_num_part[:-1]) if sto_num_part[-1] in ["A", "B"] else int(sto_num_part)
|
continue
|
||||||
|
sto_prefix = parts[0]
|
||||||
in_zone = False
|
num_part = parts[1]
|
||||||
if start_prefix == end_prefix:
|
try:
|
||||||
if sto_prefix == start_prefix and start_num <= sto_num <= end_num:
|
sto_num = int(num_part[:-1]) if num_part[-1] in ('A', 'B') else int(num_part)
|
||||||
in_zone = True
|
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:
|
else:
|
||||||
if (
|
other_group.append(sto_path)
|
||||||
(sto_prefix == start_prefix and sto_num >= start_num)
|
return same_group, other_group
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def create_estop_check_routine(
|
def create_estop_check_routine(
|
||||||
routines: ET.Element,
|
routines: ET.Element,
|
||||||
epc_df: pd.DataFrame,
|
epc_df: pd.DataFrame,
|
||||||
sto_df: pd.DataFrame,
|
sto_df: pd.DataFrame,
|
||||||
zones_df: pd.DataFrame,
|
zones_df: pd.DataFrame | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Append the ESTOP_CHECK routine (R100_ESTOP_CHECK) to the `<Routines>` element."""
|
"""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 = ET.SubElement(routines, "Routine")
|
||||||
routine.set("Name", "R100_ESTOP_CHECK")
|
routine.set("Name", routine_name)
|
||||||
routine.set("Type", "RLL")
|
routine.set("Type", "RLL")
|
||||||
|
|
||||||
rll_content = ET.SubElement(routine, "RLLContent")
|
rll_content = ET.SubElement(routine, "RLLContent")
|
||||||
@ -91,48 +97,10 @@ def create_estop_check_routine(
|
|||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
continue # Skip devices that don't follow numeric pattern
|
continue # Skip devices that don't follow numeric pattern
|
||||||
|
|
||||||
# Identify the zone this device belongs to
|
# Determine related STOs without zones (heuristic proximity)
|
||||||
device_zone = None
|
zone_stos, other_stos = _split_stos(all_stos, device_prefix, device_num)
|
||||||
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
|
|
||||||
|
|
||||||
estop_device = is_estop_desca(group.iloc[0]["DESCA"])
|
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
|
# Deduplicate STOs to ensure each appears only once
|
||||||
zone_stos = sorted(list(set(zone_stos)))
|
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});"
|
text.text = f"{raw_inputs}{sto_checks}OTL({checked_tag});"
|
||||||
rung_num += 1
|
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)
|
||||||
@ -34,7 +34,7 @@ def create_estops_routine(routines: ET.Element, epc_df: pd.DataFrame, rst_df: pd
|
|||||||
text = ET.SubElement(rung, "Text")
|
text = ET.SubElement(rung, "Text")
|
||||||
text.text = (
|
text.text = (
|
||||||
f"DCS(MCM_EPB_DCS_CTRL,EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
|
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
|
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:
|
if not rst_row.empty:
|
||||||
reset_tag = f"SFT_{rst_row.iloc[0]['DESCA']}"
|
reset_tag = f"SFT_{rst_row.iloc[0]['DESCA']}"
|
||||||
else:
|
else:
|
||||||
reset_tag = f"SFT_{subsystem}_S_PB"
|
reset_tag = f"SFT_MCM_S_PB"
|
||||||
|
|
||||||
text.text = (
|
text.text = (
|
||||||
f"DCS({dcs_tag},EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
|
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"DCS({dcs_tag},EMERGENCY STOP,EQUIVALENT - ACTIVE HIGH,500,MANUAL,"
|
||||||
f"AUTOMATIC,{input1},{input2},{status_tag},{reset_tag});"
|
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
|
||||||
116
Routines Generator/src/routines/extendo.py
Normal file
116
Routines Generator/src/routines/extendo.py
Normal 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)
|
||||||
@ -7,6 +7,7 @@ FIOH modules are identified by PARTNUMBER containing '5032-8IOLM12DR' and DESCA
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from ..utils.common import format_xml_to_match_original, natural_sort_key
|
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:
|
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.drop_duplicates(subset=['TAGNAME'])
|
||||||
fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
||||||
|
|
||||||
# Create routine XML structure
|
# Create routine XML structure (config-driven name)
|
||||||
routine = ET.Element("Routine", attrib={"Name": "R031_FIOH", "Type": "RLL"})
|
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")
|
rll_content = ET.SubElement(routine, "RLLContent")
|
||||||
|
|
||||||
# Rung 0: Documentation comment
|
# 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']):
|
elif 'TAGNAME' in fioh_row and pd.notna(fioh_row['TAGNAME']):
|
||||||
fio_name = str(fioh_row['TAGNAME']).strip()
|
fio_name = str(fioh_row['TAGNAME']).strip()
|
||||||
else:
|
else:
|
||||||
fio_name = "MCM01" # Default fallback
|
fio_name = "MCM" # Default fallback
|
||||||
|
|
||||||
# Validate both names exist and are not empty
|
# Validate both names exist and are not empty
|
||||||
if not fioh_name or not fio_name:
|
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
|
# 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);
|
# 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
|
text.text = aoi_call
|
||||||
|
|
||||||
rung_number += 1
|
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.drop_duplicates(subset=['TAGNAME'])
|
||||||
fioh_df = fioh_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
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)
|
||||||
@ -7,6 +7,7 @@ FIOM modules are identified by PARTNUMBER containing '5032-8IOLM12DR'.
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from ..utils.common import format_xml_to_match_original, natural_sort_key
|
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:
|
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.drop_duplicates(subset=['TAGNAME'])
|
||||||
fiom_df = fiom_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
fiom_df = fiom_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
||||||
|
|
||||||
# Create routine XML structure
|
# Create routine XML structure (config-driven name)
|
||||||
routine = ET.Element("Routine", attrib={"Name": "R030_FIOM", "Type": "RLL"})
|
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")
|
rll_content = ET.SubElement(routine, "RLLContent")
|
||||||
|
|
||||||
# Rung 0: Documentation comment
|
# 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'
|
dpm_name = '_'.join(dpm_parts) + '_DPM1'
|
||||||
else:
|
else:
|
||||||
# Default fallback
|
# Default fallback
|
||||||
dpm_name = "MCM01"
|
dpm_name = "MCM"
|
||||||
|
|
||||||
print(f" FIOM {fiom_name} -> DPM {dpm_name}")
|
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
|
# 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);
|
# 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
|
text.text = aoi_call
|
||||||
|
|
||||||
rung_number += 1
|
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.drop_duplicates(subset=['TAGNAME'])
|
||||||
fiom_df = fiom_df.sort_values('TAGNAME', key=lambda x: [natural_sort_key(name) for name in x])
|
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)
|
||||||
189
Routines Generator/src/routines/flow_control.py
Normal file
189
Routines Generator/src/routines/flow_control.py
Normal 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)
|
||||||
98
Routines Generator/src/routines/fpe.py
Normal file
98
Routines Generator/src/routines/fpe.py
Normal 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)
|
||||||
@ -119,4 +119,29 @@ def create_inputs_routine(routines: ET.Element, epc_df: pd.DataFrame, ignore_est
|
|||||||
norm_desca = normalize_desca(str(row["DESCA"]))
|
norm_desca = normalize_desca(str(row["DESCA"]))
|
||||||
estop_ok_tag = f"{norm_desca}_ESTOP_OK"
|
estop_ok_tag = f"{norm_desca}_ESTOP_OK"
|
||||||
text.text = f"XIC({row['IO_PATH']})OTE({estop_ok_tag});"
|
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
|
||||||
100
Routines Generator/src/routines/jpe.py
Normal file
100
Routines Generator/src/routines/jpe.py
Normal 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)
|
||||||
@ -31,29 +31,85 @@ class MainRoutinePlugin(RoutinePlugin):
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
def generate(self) -> bool:
|
def generate(self) -> bool:
|
||||||
"""Generate the MainRoutine."""
|
"""Generate the MainRoutine with JSR calls to created routines."""
|
||||||
try:
|
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
|
# Create the MainRoutine
|
||||||
routine = ET.SubElement(self.context.routines_element, "Routine")
|
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")
|
routine.set("Type", "RLL")
|
||||||
|
|
||||||
# Create RLL content
|
# Create RLL content
|
||||||
rll_content = ET.SubElement(routine, "RLLContent")
|
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 = ET.SubElement(rll_content, "Rung")
|
||||||
rung.set("Number", "0")
|
rung.set("Number", "0")
|
||||||
rung.set("Type", "N")
|
rung.set("Type", "N")
|
||||||
|
|
||||||
text = ET.SubElement(rung, "Text")
|
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")
|
self.logger.info("Successfully generated MainRoutine")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to generate MainRoutine: {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
Loading…
x
Reference in New Issue
Block a user