Agregado del grupo de Scripts para convertir LAD a SCL - Falta adaptar a logica de directorios de trabajo
This commit is contained in:
parent
b018e82848
commit
d13dd89fcb
Binary file not shown.
|
@ -0,0 +1,38 @@
|
|||
--- Log de Ejecución: x2.py ---
|
||||
Grupo: ObtainIOFromProjectTia
|
||||
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
|
||||
Inicio: 2025-05-02 23:34:21
|
||||
Fin: 2025-05-02 23:36:20
|
||||
Duración: 0:01:58.373747
|
||||
Estado: SUCCESS (Código de Salida: 0)
|
||||
|
||||
--- SALIDA ESTÁNDAR (STDOUT) ---
|
||||
--- TIA Portal Project CAx Exporter and Analyzer ---
|
||||
|
||||
Selected Project: C:/Trabajo/SIDEL/06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)/InLavoro/PLC/SAE196_c0.2/SAE196_c0.2.ap18
|
||||
Using Output Directory (Working Directory): C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
|
||||
Will export CAx data to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
|
||||
Will generate summary to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Summary.md
|
||||
Export log file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.log
|
||||
|
||||
Connecting to TIA Portal V18.0...
|
||||
2025-05-02 23:34:30,132 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
|
||||
2025-05-02 23:34:30,155 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
|
||||
Connected.
|
||||
Opening project: SAE196_c0.2.ap18...
|
||||
2025-05-02 23:35:01,950 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\InLavoro\PLC\SAE196_c0.2\SAE196_c0.2.ap18
|
||||
Project opened.
|
||||
Exporting CAx data for the project to C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml...
|
||||
CAx data exported successfully.
|
||||
|
||||
Closing TIA Portal...
|
||||
2025-05-02 23:36:15,947 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
|
||||
TIA Portal closed.
|
||||
Parsing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
|
||||
Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Summary.md
|
||||
|
||||
Script finished.
|
||||
|
||||
--- ERRORES (STDERR) ---
|
||||
Ninguno
|
||||
--- FIN DEL LOG ---
|
|
@ -0,0 +1,48 @@
|
|||
--- Log de Ejecución: x3.py ---
|
||||
Grupo: ObtainIOFromProjectTia
|
||||
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
|
||||
Inicio: 2025-05-02 23:43:07
|
||||
Fin: 2025-05-02 23:43:12
|
||||
Duración: 0:00:05.235415
|
||||
Estado: SUCCESS (Código de Salida: 0)
|
||||
|
||||
--- SALIDA ESTÁNDAR (STDOUT) ---
|
||||
--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v28 - Working Directory Integration) ---
|
||||
Using Working Directory for Output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
|
||||
Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
|
||||
Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
|
||||
Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
|
||||
Output Main Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md
|
||||
Output IO Debug Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
|
||||
Processing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
|
||||
Pass 1: Found 203 InternalElement(s). Populating device dictionary...
|
||||
Pass 2: Identifying PLCs and Networks (Refined v2)...
|
||||
Identified Network: PROFIBUS_1 (bcc6f2bd-3d71-4407-90f2-bccff6064051) Type: Profibus
|
||||
Identified Network: ETHERNET_1 (c6d49787-a076-4592-994d-876eea123dfd) Type: Ethernet/Profinet
|
||||
Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0
|
||||
Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
|
||||
Found 118 InternalLink(s).
|
||||
Mapping Device/Node 'E1' (NodeID:1643b51f-7067-4565-8f8e-109a1a775fed, Addr:10.1.33.11) to Network 'ETHERNET_1'
|
||||
--> Associating Network 'ETHERNET_1' with PLC 'PLC' (via Node 'E1' Addr: 10.1.33.11)
|
||||
Mapping Device/Node 'P1' (NodeID:5aff409b-2573-485f-82bf-0e08c9200086, Addr:1) to Network 'PROFIBUS_1'
|
||||
--> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1)
|
||||
Mapping Device/Node 'PB1' (NodeID:c796e175-c770-43f0-8191-fc91996c0147, Addr:12) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:0b44f55a-63c1-49e8-beea-24dc5d3226e3, Addr:20) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:25cfc251-f946-40c5-992d-ad6387677acb, Addr:21) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:57999375-ec72-46ef-8ec2-6c3178e8acf8, Addr:22) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:54e8db6a-9443-41a4-a85b-cf0722c1d299, Addr:10) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:4786bab6-4097-4651-ac19-6cadfc7ea735, Addr:8) to Network 'PROFIBUS_1'
|
||||
Mapping Device/Node 'PB1' (NodeID:1f08afcb-111f-428f-915e-69363af1b09a, Addr:40) to Network 'PROFIBUS_1'
|
||||
Data extraction and structuring complete.
|
||||
Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
|
||||
JSON data written successfully.
|
||||
|
||||
Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md
|
||||
|
||||
IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
|
||||
|
||||
Script finished.
|
||||
|
||||
--- ERRORES (STDERR) ---
|
||||
Ninguno
|
||||
--- FIN DEL LOG ---
|
|
@ -6,6 +6,7 @@ generar un archivo Markdown con la información.
|
|||
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
import traceback
|
||||
from lxml import etree as ET
|
||||
|
@ -732,9 +733,9 @@ def generate_markdown_tree(project_data, md_file_path):
|
|||
end_byte = start_byte + length_bytes - 1
|
||||
prefix = "P?"
|
||||
if io_type.lower() == "input":
|
||||
prefix = "PE"
|
||||
prefix = "EW"
|
||||
elif io_type.lower() == "output":
|
||||
prefix = "PA"
|
||||
prefix = "AW"
|
||||
siemens_addr = f"{prefix} {start_byte}..{end_byte}"
|
||||
except Exception: # Catch any error during calc/format
|
||||
siemens_addr = (
|
||||
|
@ -963,8 +964,8 @@ def select_cax_file(initial_dir=None): # Add initial_dir parameter
|
|||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select CAx Export File (XML)",
|
||||
filetypes=[("XML Files", "*.xml"), ("AML Files", "*.aml"), ("All Files", "*.*")], # Added AML
|
||||
title="Select CAx Export File (AML)",
|
||||
filetypes=[ ("AML Files", "*.aml"), ("All Files", "*.*")], # Added AML
|
||||
initialdir=initial_dir # Set the initial directory
|
||||
)
|
||||
root.destroy()
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
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.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,28 @@
|
|||
# generators/generate_md_tag_table.py
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
def generate_tag_table_markdown(data):
|
||||
"""Genera contenido Markdown para una tabla de tags."""
|
||||
md_lines = []
|
||||
table_name = data.get("block_name", "UnknownTagTable")
|
||||
tags = data.get("tags", [])
|
||||
|
||||
md_lines.append(f"# Tag Table: {table_name}")
|
||||
md_lines.append("")
|
||||
|
||||
if tags:
|
||||
md_lines.append("| Name | Datatype | Address | Comment |")
|
||||
md_lines.append("|---|---|---|---|")
|
||||
for tag in tags:
|
||||
name = tag.get("name", "N/A")
|
||||
datatype = tag.get("datatype", "N/A")
|
||||
address = tag.get("address", "N/A") or " "
|
||||
comment_raw = tag.get("comment")
|
||||
comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else ""
|
||||
md_lines.append(f"| `{name}` | `{datatype}` | `{address}` | {comment} |")
|
||||
md_lines.append("")
|
||||
else:
|
||||
md_lines.append("No tags found in this table.")
|
||||
md_lines.append("")
|
||||
|
||||
return md_lines
|
|
@ -0,0 +1,46 @@
|
|||
# generators/generate_md_udt.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from .generator_utils import format_scl_start_value # Importar utilidad necesaria
|
||||
|
||||
def generate_markdown_member_rows(members, level=0):
|
||||
"""Genera filas Markdown para miembros de UDT (recursivo)."""
|
||||
md_rows = []; prefix = " " * level
|
||||
for member in members:
|
||||
name = member.get("name", "N/A"); datatype = member.get("datatype", "N/A")
|
||||
start_value_raw = member.get("start_value")
|
||||
start_value_fmt = format_scl_start_value(start_value_raw, datatype) if start_value_raw is not None else ""
|
||||
comment_raw = member.get("comment"); comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else ""
|
||||
md_rows.append(f"| {prefix}`{name}` | `{datatype}` | `{start_value_fmt}` | {comment} |")
|
||||
children = member.get("children")
|
||||
if children: md_rows.extend(generate_markdown_member_rows(children, level + 1))
|
||||
array_elements = member.get("array_elements")
|
||||
if array_elements:
|
||||
base_type_for_init = datatype
|
||||
if isinstance(datatype, str) and datatype.lower().startswith("array["):
|
||||
match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", datatype, re.IGNORECASE)
|
||||
if match: base_type_for_init = match.group(2).strip()
|
||||
md_rows.append(f"| {prefix} *(Initial Values)* | | | |")
|
||||
try:
|
||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
||||
sorted_indices_str = [str(k) for k in sorted(indices_numeric.keys())]
|
||||
except ValueError: sorted_indices_str = sorted(array_elements.keys())
|
||||
for idx_str in sorted_indices_str:
|
||||
val_raw = array_elements[idx_str]
|
||||
val_fmt = format_scl_start_value(val_raw, base_type_for_init) if val_raw is not None else ""
|
||||
md_rows.append(f"| {prefix} `[{idx_str}]` | | `{val_fmt}` | |")
|
||||
return md_rows
|
||||
|
||||
def generate_udt_markdown(data):
|
||||
"""Genera contenido Markdown para un UDT."""
|
||||
md_lines = []; udt_name = data.get("block_name", "UnknownUDT"); udt_comment = data.get("block_comment", "")
|
||||
md_lines.append(f"# UDT: {udt_name}"); md_lines.append("")
|
||||
if udt_comment: md_lines.append(f"**Comment:**"); [md_lines.append(f"> {line}") for line in udt_comment.splitlines()]; md_lines.append("")
|
||||
members = data.get("interface", {}).get("None", [])
|
||||
if members:
|
||||
md_lines.append("## Members"); md_lines.append("")
|
||||
md_lines.append("| Name | Datatype | Start Value | Comment |"); md_lines.append("|---|---|---|---|")
|
||||
md_lines.extend(generate_markdown_member_rows(members))
|
||||
md_lines.append("")
|
||||
else: md_lines.append("No members found in the UDT interface."); md_lines.append("")
|
||||
return md_lines
|
|
@ -0,0 +1,285 @@
|
|||
# ToUpload/generators/generate_scl_code_block.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import os # Importar os
|
||||
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
|
||||
# ... (_generate_scl_header sin cambios)...
|
||||
def _generate_scl_header(data, scl_block_name):
|
||||
scl_output = []
|
||||
block_type = data.get("block_type", "Unknown")
|
||||
block_name = data.get("block_name", "UnknownBlock")
|
||||
block_number = data.get("block_number")
|
||||
block_comment = data.get("block_comment", "")
|
||||
scl_block_keyword = "FUNCTION_BLOCK"
|
||||
if block_type == "FC":
|
||||
scl_block_keyword = "FUNCTION"
|
||||
elif block_type == "OB":
|
||||
scl_block_keyword = "ORGANIZATION_BLOCK"
|
||||
scl_output.append(f"// Block Type: {block_type}")
|
||||
if block_name != scl_block_name:
|
||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
||||
if block_number:
|
||||
scl_output.append(f"// Block Number: {block_number}")
|
||||
original_net_langs = set(
|
||||
n.get("language", "Unknown") for n in data.get("networks", [])
|
||||
)
|
||||
scl_output.append(
|
||||
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
|
||||
)
|
||||
if block_comment:
|
||||
scl_output.append(f"// Block Comment:")
|
||||
[scl_output.append(f"// {line}") for line in block_comment.splitlines()]
|
||||
scl_output.append("")
|
||||
if block_type == "FC":
|
||||
return_type = "Void"
|
||||
interface_data = data.get("interface", {})
|
||||
if interface_data.get("Return"):
|
||||
return_member = interface_data["Return"][0]
|
||||
return_type_raw = return_member.get("datatype", "Void")
|
||||
return_type = (
|
||||
return_type_raw[1:-1]
|
||||
if isinstance(return_type_raw, str)
|
||||
and return_type_raw.startswith('"')
|
||||
and return_type_raw.endswith('"')
|
||||
else return_type_raw
|
||||
)
|
||||
if return_type != return_type_raw and not (
|
||||
isinstance(return_type_raw, str)
|
||||
and return_type_raw.lower().startswith("array")
|
||||
):
|
||||
return_type = f'"{return_type}"'
|
||||
else:
|
||||
return_type = return_type_raw
|
||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
|
||||
else:
|
||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
|
||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||
scl_output.append("VERSION : 0.1")
|
||||
scl_output.append("")
|
||||
return scl_output
|
||||
|
||||
|
||||
# Modificar _generate_scl_interface para pasar project_root_dir
|
||||
def _generate_scl_interface(interface_data, project_root_dir): # <-- Nuevo argumento
|
||||
"""Genera las secciones VAR_* de la interfaz SCL para FC/FB/OB."""
|
||||
scl_output = []
|
||||
section_order = [
|
||||
"Input",
|
||||
"Output",
|
||||
"InOut",
|
||||
"Static",
|
||||
"Temp",
|
||||
"Constant",
|
||||
"Return",
|
||||
] # Incluir Return
|
||||
declared_temps = set() # Para _generate_scl_temp_vars
|
||||
|
||||
for section_name in section_order:
|
||||
vars_in_section = interface_data.get(section_name, [])
|
||||
if vars_in_section:
|
||||
scl_section_keyword = f"VAR_{section_name.upper()}"
|
||||
end_keyword = "END_VAR"
|
||||
if section_name == "Static":
|
||||
scl_section_keyword = "VAR_STAT"
|
||||
if section_name == "Temp":
|
||||
scl_section_keyword = "VAR_TEMP"
|
||||
if section_name == "Constant":
|
||||
scl_section_keyword = "CONSTANT"
|
||||
end_keyword = "END_CONSTANT"
|
||||
if section_name == "Return":
|
||||
scl_section_keyword = "VAR_OUTPUT"
|
||||
# Retorno va en Output para FB/OB, implícito en FC
|
||||
|
||||
# Para FC, la sección Return no se declara explícitamente aquí
|
||||
if (
|
||||
interface_data.get("parent_block_type") == "FC"
|
||||
and section_name == "Return"
|
||||
):
|
||||
continue
|
||||
|
||||
scl_output.append(scl_section_keyword)
|
||||
# Pasar project_root_dir a generate_scl_declarations
|
||||
scl_output.extend(
|
||||
generate_scl_declarations(
|
||||
vars_in_section, indent_level=1, project_root_dir=project_root_dir
|
||||
)
|
||||
) # <-- Pasar ruta raíz
|
||||
scl_output.append(end_keyword)
|
||||
scl_output.append("")
|
||||
|
||||
if section_name == "Temp":
|
||||
declared_temps.update(
|
||||
format_variable_name(v.get("name"))
|
||||
for v in vars_in_section
|
||||
if v.get("name")
|
||||
)
|
||||
return scl_output, declared_temps
|
||||
|
||||
|
||||
# ... (_generate_scl_temp_vars y _generate_scl_body sin cambios) ...
|
||||
def _generate_scl_temp_vars(data, declared_temps):
|
||||
scl_output = []
|
||||
temp_vars_detected = set()
|
||||
temp_pattern = re.compile(r'"?(#\w+)"?')
|
||||
for network in data.get("networks", []):
|
||||
for instruction in network.get("logic", []):
|
||||
scl_code = instruction.get("scl", "")
|
||||
edge_update_code = instruction.get("_edge_mem_update_scl", "")
|
||||
code_to_scan = (
|
||||
(scl_code if scl_code else "")
|
||||
+ "\n"
|
||||
+ (edge_update_code if edge_update_code else "")
|
||||
)
|
||||
if code_to_scan:
|
||||
found_temps = temp_pattern.findall(code_to_scan)
|
||||
[temp_vars_detected.add(t) for t in found_temps if t]
|
||||
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||
if additional_temps:
|
||||
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
||||
temp_section_exists = any(
|
||||
"VAR_TEMP" in s for s in data.get("generated_scl", [])
|
||||
) # Check if VAR_TEMP already exists
|
||||
if not temp_section_exists and not declared_temps:
|
||||
scl_output.append("VAR_TEMP") # Only add if no temps were declared before
|
||||
for temp_name in additional_temps:
|
||||
scl_name = format_variable_name(temp_name)
|
||||
inferred_type = "Bool"
|
||||
scl_output.append(
|
||||
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
||||
)
|
||||
if not temp_section_exists and not declared_temps:
|
||||
scl_output.append("END_VAR")
|
||||
scl_output.append("")
|
||||
return scl_output
|
||||
|
||||
|
||||
def _generate_scl_body(networks):
|
||||
scl_output = ["BEGIN", ""]
|
||||
network_logic_added = False
|
||||
for i, network in enumerate(networks):
|
||||
network_title = network.get("title", f'Network {network.get("id", i+1)}')
|
||||
network_comment = network.get("comment", "")
|
||||
network_lang = network.get("language", "LAD")
|
||||
scl_output.append(
|
||||
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
||||
)
|
||||
if network_comment:
|
||||
[
|
||||
scl_output.append(f" // {line}")
|
||||
for line in network_comment.splitlines()
|
||||
]
|
||||
scl_output.append("")
|
||||
network_has_code = False
|
||||
logic_in_network = network.get("logic", [])
|
||||
if not logic_in_network:
|
||||
scl_output.append(f" // Network {i+1} has no logic elements.")
|
||||
scl_output.append("")
|
||||
continue
|
||||
if network_lang == "STL":
|
||||
if logic_in_network and logic_in_network[0].get("type") == "RAW_STL_CHUNK":
|
||||
network_has_code = True
|
||||
raw_stl_code = logic_in_network[0].get(
|
||||
"stl", "// ERROR: STL code missing"
|
||||
)
|
||||
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
|
||||
scl_output.append(f" ```stl ")
|
||||
[
|
||||
scl_output.append(f" {stl_line}") # scl_output.append(f" // {stl_line}")
|
||||
for stl_line in raw_stl_code.splitlines()
|
||||
]
|
||||
scl_output.append(f" ``` ")
|
||||
scl_output.append(f" // --- END STL Network {i+1} ---")
|
||||
scl_output.append("")
|
||||
else:
|
||||
scl_output.append(
|
||||
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
||||
)
|
||||
scl_output.append("")
|
||||
else:
|
||||
for instruction in logic_in_network:
|
||||
instruction_type = instruction.get("type", "")
|
||||
scl_code = instruction.get("scl", "")
|
||||
is_grouped = instruction.get("grouped", False)
|
||||
edge_update_scl = instruction.get("_edge_mem_update_scl", "")
|
||||
if is_grouped:
|
||||
continue
|
||||
code_to_print = []
|
||||
if scl_code:
|
||||
code_to_print.extend(scl_code.splitlines())
|
||||
if edge_update_scl:
|
||||
code_to_print.extend(
|
||||
edge_update_scl.splitlines()
|
||||
) # Append edge update SCL
|
||||
if code_to_print:
|
||||
is_only_comment = all(
|
||||
line.strip().startswith("//")
|
||||
for line in code_to_print
|
||||
if line.strip()
|
||||
)
|
||||
is_if_block = any(
|
||||
line.strip().startswith("IF") for line in code_to_print
|
||||
)
|
||||
if (
|
||||
not is_only_comment
|
||||
or is_if_block
|
||||
or "_error" in instruction_type
|
||||
or instruction_type
|
||||
in [
|
||||
"UNSUPPORTED_LANG",
|
||||
"UNSUPPORTED_CONTENT",
|
||||
"PARSING_ERROR",
|
||||
"RAW_SCL_CHUNK",
|
||||
]
|
||||
): # Print RAW_SCL chunks too
|
||||
network_has_code = True
|
||||
[scl_output.append(f" {line}") for line in code_to_print]
|
||||
scl_output.append("")
|
||||
if not network_has_code and network_lang != "STL":
|
||||
scl_output.append(f" // Network {i+1} did not produce printable SCL code.")
|
||||
scl_output.append("")
|
||||
if network_has_code:
|
||||
network_logic_added = True # Mark if any network had code
|
||||
# Add a default comment if no logic was generated at all
|
||||
if not network_logic_added:
|
||||
scl_output.append(" // No executable logic generated by script.")
|
||||
scl_output.append("")
|
||||
return scl_output
|
||||
|
||||
|
||||
# Modificar generate_scl_for_code_block para aceptar y pasar project_root_dir
|
||||
def generate_scl_for_code_block(data, project_root_dir): # <-- Nuevo argumento
|
||||
"""Genera el contenido SCL completo para un FC/FB/OB."""
|
||||
scl_output = []
|
||||
block_type = data.get("block_type", "Unknown")
|
||||
scl_block_name = format_variable_name(data.get("block_name", "UnknownBlock"))
|
||||
scl_block_keyword = "FUNCTION_BLOCK" # Default for FB
|
||||
if block_type == "FC":
|
||||
scl_block_keyword = "FUNCTION"
|
||||
elif block_type == "OB":
|
||||
scl_block_keyword = "ORGANIZATION_BLOCK"
|
||||
|
||||
scl_output.extend(_generate_scl_header(data, scl_block_name))
|
||||
|
||||
interface_data = data.get("interface", {})
|
||||
interface_data["parent_block_type"] = block_type # Ayuda a _generate_scl_interface
|
||||
# Pasar project_root_dir a _generate_scl_interface
|
||||
interface_lines, declared_temps = _generate_scl_interface(
|
||||
interface_data, project_root_dir
|
||||
) # <-- Pasar ruta raíz
|
||||
scl_output.extend(interface_lines)
|
||||
|
||||
# Generar VAR_TEMP adicionales (no necesita project_root_dir)
|
||||
scl_output.extend(_generate_scl_temp_vars(data, declared_temps))
|
||||
|
||||
# Generar cuerpo (no necesita project_root_dir)
|
||||
scl_output.extend(_generate_scl_body(data.get("networks", [])))
|
||||
scl_output.append(f"END_{scl_block_keyword}")
|
||||
|
||||
# Guardar SCL generado en data para _generate_scl_temp_vars
|
||||
data["generated_scl"] = scl_output
|
||||
|
||||
return scl_output
|
|
@ -0,0 +1,54 @@
|
|||
# ToUpload/generators/generate_scl_db.py
|
||||
# -*- coding: utf-8 -*-
|
||||
# No necesita importar json/os aquí, lo hará generate_scl_declarations
|
||||
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||
|
||||
# Modificar _generate_scl_header si es necesario, pero parece ok
|
||||
def _generate_scl_header(data, scl_block_name):
|
||||
# ... (código sin cambios) ...
|
||||
scl_output = []
|
||||
block_type = data.get("block_type", "Unknown")
|
||||
block_name = data.get("block_name", "UnknownBlock")
|
||||
block_number = data.get("block_number")
|
||||
block_comment = data.get("block_comment", "")
|
||||
scl_output.append(f"// Block Type: {block_type}")
|
||||
if block_name != scl_block_name: scl_output.append(f"// Block Name (Original): {block_name}")
|
||||
if block_number: scl_output.append(f"// Block Number: {block_number}")
|
||||
if block_comment: scl_output.append(f"// Block Comment:"); [scl_output.append(f"// {line}") for line in block_comment.splitlines()]
|
||||
scl_output.append(""); scl_output.append(f'DATA_BLOCK "{scl_block_name}"'); scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||
scl_output.append("VERSION : 0.1"); scl_output.append("")
|
||||
return scl_output
|
||||
|
||||
# Modificar _generate_scl_interface para pasar project_root_dir
|
||||
def _generate_scl_interface(interface_data, project_root_dir): # <-- Nuevo argumento
|
||||
"""Genera la sección VAR para DB (basada en 'Static')."""
|
||||
scl_output = []
|
||||
static_vars = interface_data.get("Static", [])
|
||||
if static_vars:
|
||||
scl_output.append("VAR")
|
||||
# Pasar project_root_dir a generate_scl_declarations
|
||||
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1, project_root_dir=project_root_dir)) # <-- Pasar ruta raíz
|
||||
scl_output.append("END_VAR")
|
||||
else:
|
||||
print("Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB.")
|
||||
scl_output.append("VAR\nEND_VAR") # Añadir vacío
|
||||
scl_output.append("")
|
||||
return scl_output
|
||||
|
||||
# Modificar generate_scl_for_db para aceptar y pasar project_root_dir
|
||||
def generate_scl_for_db(data, project_root_dir): # <-- Nuevo argumento
|
||||
"""Genera el contenido SCL completo para un DATA_BLOCK."""
|
||||
scl_output = []
|
||||
scl_block_name = format_variable_name(data.get("block_name", "UnknownDB"))
|
||||
|
||||
scl_output.extend(_generate_scl_header(data, scl_block_name))
|
||||
|
||||
interface_data = data.get("interface", {})
|
||||
# Pasar project_root_dir a _generate_scl_interface
|
||||
scl_output.extend(_generate_scl_interface(interface_data, project_root_dir)) # <-- Pasar ruta raíz
|
||||
|
||||
scl_output.append("BEGIN")
|
||||
scl_output.append(" // Data Blocks have no executable code")
|
||||
scl_output.append("END_DATA_BLOCK")
|
||||
|
||||
return scl_output
|
|
@ -0,0 +1,278 @@
|
|||
# ToUpload/generators/generator_utils.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import traceback # Para depuración si es necesario
|
||||
import sys
|
||||
|
||||
# --- Importar format_variable_name desde processors ---
|
||||
try:
|
||||
# Asumiendo que este script está en 'generators' y 'processors' está al mismo nivel
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_base_dir = os.path.dirname(current_dir)
|
||||
processors_dir = os.path.join(project_base_dir, 'processors')
|
||||
if processors_dir not in sys.path:
|
||||
sys.path.insert(0, processors_dir) # Añadir al path si no está
|
||||
from processor_utils import format_variable_name
|
||||
except ImportError:
|
||||
print("Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils.")
|
||||
print("Usando una implementación local básica.")
|
||||
def format_variable_name(name): # Fallback
|
||||
if not name: return "_INVALID_NAME_"
|
||||
if name.startswith('"') and name.endswith('"'): return name
|
||||
prefix = "#" if name.startswith("#") else ""
|
||||
if prefix: name = name[1:]
|
||||
if name and name[0].isdigit(): name = "_" + name
|
||||
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
||||
return prefix + name
|
||||
# --- Fin Fallback ---
|
||||
|
||||
# --- format_scl_start_value (Sin cambios respecto a la versión anterior) ---
|
||||
def format_scl_start_value(value, datatype):
|
||||
if value is None: return None
|
||||
# Convertir complex dict a string para procesar
|
||||
if isinstance(value, dict):
|
||||
# Si tiene 'value', usar ese. Si no, representar el dict como comentario
|
||||
value_to_process = value.get('value')
|
||||
if value_to_process is None:
|
||||
return f"/* Init: {json.dumps(value)} */" # Representar dict como comentario
|
||||
value = value_to_process # Usar el valor interno
|
||||
|
||||
datatype_lower = datatype.lower() if isinstance(datatype, str) else ""
|
||||
value_str = str(value)
|
||||
|
||||
# Determinar si es tipo complejo (no estrictamente básico)
|
||||
is_complex_type = (
|
||||
('"' in datatype_lower) or ('array' in datatype_lower) or ('struct' in datatype_lower) or
|
||||
datatype_lower not in {
|
||||
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
|
||||
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
|
||||
"s5time", "date", "dt", "dtl", "tod", "string", "char", "wstring", "wchar", "variant",
|
||||
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer" # Añadir otros tipos IEC comunes
|
||||
}
|
||||
)
|
||||
|
||||
if is_complex_type:
|
||||
# Para tipos complejos, solo permitir constantes simbólicas o inicializadores básicos (0, FALSE, '')
|
||||
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str # Constante simbólica
|
||||
if value_str == '0': return '0' # Cero numérico
|
||||
if value_str.lower() == 'false': return 'FALSE' # Booleano Falso
|
||||
if value_str == "''" or value_str == "": return "''" # String vacío
|
||||
# Ignorar otros valores iniciales para tipos complejos (incluye JSON de arrays)
|
||||
# print(f"INFO: Start value '{value_str}' for complex type '{datatype}' skipped.")
|
||||
return None
|
||||
|
||||
# Quitar comillas simples/dobles externas si las hay
|
||||
value_str_unquoted = value_str
|
||||
if len(value_str) > 1:
|
||||
if value_str.startswith('"') and value_str.endswith('"'): value_str_unquoted = value_str[1:-1]
|
||||
elif value_str.startswith("'") and value_str.endswith("'"): value_str_unquoted = value_str[1:-1]
|
||||
|
||||
# Formateo por tipo básico
|
||||
if any(t in datatype_lower for t in ["int","byte","word","dint","dword","lint","lword","sint","usint","uint","udint","ulint"]):
|
||||
try: return str(int(value_str_unquoted))
|
||||
except ValueError: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
|
||||
elif "bool" in datatype_lower:
|
||||
val_low = value_str_unquoted.lower();
|
||||
if val_low in ['true', '1']: return "TRUE"
|
||||
elif val_low in ['false', '0']: return "FALSE"
|
||||
else: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else "FALSE" # Default FALSE
|
||||
elif "string" in datatype_lower or "char" in datatype_lower:
|
||||
escaped_value = value_str_unquoted.replace("'", "''") # Escapar comillas simples
|
||||
prefix = "WSTRING#" if "wstring" in datatype_lower else ("WCHAR#" if "wchar" in datatype_lower else "")
|
||||
return f"{prefix}'{escaped_value}'" # Usar comillas simples SCL
|
||||
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
||||
try:
|
||||
f_val = float(value_str_unquoted)
|
||||
s_val = "{:.7g}".format(f_val) # Notación científica si es necesario, precisión limitada
|
||||
return s_val + (".0" if "." not in s_val and "e" not in s_val.lower() else "") # Añadir .0 si es entero
|
||||
except ValueError: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
|
||||
elif "time" in datatype_lower: # Incluye TIME, LTIME, S5TIME
|
||||
prefix, val_to_use = "", value_str_unquoted
|
||||
# Extraer prefijo si ya existe (T#, LT#, S5T#)
|
||||
match_prefix = re.match(r"^(T#|LT#|S5T#)(.*)", val_to_use, re.IGNORECASE)
|
||||
if match_prefix: prefix, val_to_use = match_prefix.groups()
|
||||
# Validar formato del valor de tiempo (simplificado)
|
||||
if re.match(r'^-?(\d+d_)?(\d+h_)?(\d+m_)?(\d+s_)?(\d+ms)?$', val_to_use, re.IGNORECASE):
|
||||
target_prefix = "S5T#" if "s5time" in datatype_lower else ("LT#" if "ltime" in datatype_lower else "T#")
|
||||
return f"{target_prefix}{val_to_use}"
|
||||
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
|
||||
else: return None # Formato inválido
|
||||
elif any(t in datatype_lower for t in ["date", "dtl", "dt", "tod", "time_of_day"]):
|
||||
val_to_use = value_str_unquoted; prefix = ""
|
||||
# Extraer prefijo si ya existe (DTL#, D#, DT#, TOD#)
|
||||
match_prefix = re.match(r"^(DTL#|D#|DT#|TOD#)(.*)", val_to_use, re.IGNORECASE)
|
||||
if match_prefix: prefix, val_to_use = match_prefix.groups()
|
||||
# Determinar prefijo SCL correcto
|
||||
target_prefix="DTL#" if "dtl" in datatype_lower or "date_and_time" in datatype_lower else ("DT#" if "dt" in datatype_lower else ("TOD#" if "tod" in datatype_lower or "time_of_day" in datatype_lower else "D#"))
|
||||
# Validar formato (simplificado)
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}(-\d{2}:\d{2}:\d{2}(\.\d+)?)?$', val_to_use) or re.match(r'^\d{2}:\d{2}:\d{2}(\.\d+)?$', val_to_use):
|
||||
return f"{target_prefix}{val_to_use}"
|
||||
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
|
||||
else: return None # Formato inválido
|
||||
else: # Otros tipos o desconocidos
|
||||
return value_str if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str) else None # Solo permitir constantes simbólicas
|
||||
|
||||
|
||||
# <-- MODIFICADO: generate_scl_declarations -->
|
||||
def generate_scl_declarations(variables, indent_level=1, project_root_dir=None):
|
||||
"""
|
||||
Genera líneas SCL para declarar variables, manejando UDTs, FBs (InstanceOfName),
|
||||
Arrays y Structs.
|
||||
"""
|
||||
scl_lines = []
|
||||
indent = " " * indent_level
|
||||
# Lista de tipos básicos simples (en minúsculas) - ampliada
|
||||
basic_types = {
|
||||
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
|
||||
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
|
||||
"s5time", "date", "dt", "dtl", "tod", "time_of_day", # TOD sinónimos
|
||||
"char", "wchar", "variant",
|
||||
# Tipos IEC comunes
|
||||
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer"
|
||||
}
|
||||
|
||||
# Patrones para tipos básicos parametrizados (ignorando mayúsculas/minúsculas)
|
||||
string_pattern = re.compile(r"^(W?STRING)(\[\s*\d+\s*\])?$", re.IGNORECASE)
|
||||
array_pattern = re.compile(r'^(Array\[.*\]\s+of\s+)(.*)', re.IGNORECASE)
|
||||
|
||||
for var in variables:
|
||||
var_name_scl = format_variable_name(var.get("name"))
|
||||
var_dtype_raw = var.get("datatype", "VARIANT")
|
||||
# <-- NUEVO: Obtener instance_of_name -->
|
||||
instance_of_name = var.get("instance_of_name") # Puede ser None
|
||||
# <-- FIN NUEVO -->
|
||||
var_comment = var.get("comment")
|
||||
start_value_raw = var.get("start_value")
|
||||
children = var.get("children") # Para STRUCT anidados
|
||||
array_elements = var.get("array_elements") # Para inicialización de ARRAY
|
||||
|
||||
declaration_dtype = var_dtype_raw # Tipo a usar en la declaración SCL
|
||||
base_type_for_init = var_dtype_raw # Tipo base para formatear valor inicial
|
||||
is_array = False
|
||||
is_struct_inline = bool(children) # Es un STRUCT definido inline
|
||||
is_potential_udt_or_fb = False # Flag para comprobar si buscar archivo .json
|
||||
type_to_check = None # Nombre limpio del tipo a buscar (UDT o FB)
|
||||
|
||||
# --- Lógica Principal de Determinación de Tipo ---
|
||||
if is_struct_inline:
|
||||
# Si tiene hijos, se declara como STRUCT ... END_STRUCT
|
||||
declaration_dtype = "STRUCT"
|
||||
base_type_for_init = "STRUCT" # Valor inicial no aplica a STRUCT directamente
|
||||
elif isinstance(var_dtype_raw, str):
|
||||
# 1. Comprobar si es FB Instance usando InstanceOfName
|
||||
if instance_of_name:
|
||||
# Si InstanceOfName existe, usarlo como tipo (entre comillas)
|
||||
declaration_dtype = f'"{instance_of_name}"'
|
||||
base_type_for_init = instance_of_name # Usar nombre limpio para init/check
|
||||
is_potential_udt_or_fb = True # Marcar para buscar archivo FB
|
||||
type_to_check = instance_of_name
|
||||
else:
|
||||
# 2. No es FB Instance directo, comprobar si es Array
|
||||
array_match = array_pattern.match(var_dtype_raw)
|
||||
if array_match:
|
||||
is_array = True
|
||||
array_prefix_for_decl = array_match.group(1)
|
||||
base_type_raw = array_match.group(2).strip()
|
||||
base_type_for_init = base_type_raw # Tipo base para init/check
|
||||
|
||||
# Limpiar tipo base para comprobar si es básico/UDT/String
|
||||
base_type_clean = base_type_raw[1:-1] if base_type_raw.startswith('"') and base_type_raw.endswith('"') else base_type_raw
|
||||
base_type_lower = base_type_clean.lower()
|
||||
|
||||
# ¿El tipo base es UDT/FB conocido o un tipo básico/paramétrico?
|
||||
if (base_type_lower not in basic_types and
|
||||
not string_pattern.match(base_type_clean)):
|
||||
# Asumir UDT/FB si no es básico ni String[N]/Char
|
||||
declaration_dtype = f'{array_prefix_for_decl}"{base_type_clean}"' # Poner comillas
|
||||
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
|
||||
type_to_check = base_type_clean
|
||||
else:
|
||||
# Es básico o String[N]/Char
|
||||
declaration_dtype = f'{array_prefix_for_decl}{base_type_raw}' # Usar como viene (puede tener comillas si era así)
|
||||
else:
|
||||
# 3. No es FB ni Array, ¿es UDT, String, Char o Básico?
|
||||
base_type_clean = var_dtype_raw[1:-1] if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"') else var_dtype_raw
|
||||
base_type_lower = base_type_clean.lower()
|
||||
base_type_for_init = base_type_clean # Tipo base para init/check
|
||||
|
||||
if (base_type_lower not in basic_types and
|
||||
not string_pattern.match(base_type_clean)):
|
||||
# Asumir UDT/FB si no es básico ni String[N]/Char
|
||||
declaration_dtype = f'"{base_type_clean}"' # Poner comillas
|
||||
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
|
||||
type_to_check = base_type_clean
|
||||
else:
|
||||
# Es básico o String[N]/Char
|
||||
declaration_dtype = var_dtype_raw # Usar como viene
|
||||
|
||||
# --- Búsqueda Opcional de Archivo de Definición (UDT o FB) ---
|
||||
if is_potential_udt_or_fb and type_to_check and project_root_dir:
|
||||
# Buscar tanto en 'PLC data types' como en 'Program blocks'
|
||||
found_path = None
|
||||
type_scl_name = format_variable_name(type_to_check)
|
||||
possible_paths = [
|
||||
os.path.join(project_root_dir, 'PLC data types', 'parsing', f'{type_scl_name}_processed.json'),
|
||||
os.path.join(project_root_dir, 'Program blocks', 'parsing', f'{type_scl_name}_processed.json')
|
||||
# Añadir más rutas si la estructura del proyecto varía
|
||||
]
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
found_path = path
|
||||
break
|
||||
|
||||
if found_path:
|
||||
print(f" INFO: Definición '{type_to_check}' localizada en: '{os.path.relpath(found_path, project_root_dir)}'")
|
||||
else:
|
||||
print(f" WARNING: No se encontró definición para '{type_to_check}'. Se buscó en directorios estándar.")
|
||||
|
||||
# --- Construir Línea de Declaración SCL ---
|
||||
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
||||
init_value_scl_part = ""
|
||||
|
||||
if is_struct_inline:
|
||||
# Generar STRUCT anidado
|
||||
scl_lines.append(declaration_line) # Añade "VarName : STRUCT"
|
||||
# Llamada recursiva para los hijos
|
||||
scl_lines.extend(generate_scl_declarations(children, indent_level + 1, project_root_dir))
|
||||
scl_lines.append(f"{indent}END_STRUCT;")
|
||||
# Añadir comentario al END_STRUCT si existe
|
||||
if var_comment: scl_lines[-1] += f" // {var_comment}"
|
||||
scl_lines.append("") # Línea en blanco después del struct
|
||||
continue # Pasar a la siguiente variable del nivel actual
|
||||
|
||||
# --- Manejo de Valor Inicial (para no-STRUCTs) ---
|
||||
init_value_scl = None
|
||||
if is_array and array_elements:
|
||||
# Inicialización de Array
|
||||
init_values = []
|
||||
try: # Intentar ordenar índices numéricamente
|
||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
||||
sorted_indices_str = [str(k) for k in sorted(indices_numeric.keys())]
|
||||
except ValueError: # Ordenar como strings si no son numéricos
|
||||
print(f"Advertencia: Índices array no numéricos para '{var_name_scl}', ordenando como strings.")
|
||||
sorted_indices_str = sorted(array_elements.keys())
|
||||
|
||||
for idx_str in sorted_indices_str:
|
||||
val_info = array_elements[idx_str] # val_info puede ser dict o valor directo
|
||||
# Formatear valor usando el tipo base del array
|
||||
formatted_val = format_scl_start_value(val_info, base_type_for_init)
|
||||
# Usar 'NULL' o comentario si el formateo falla o es complejo
|
||||
init_values.append(formatted_val if formatted_val is not None else f"/* Array[{idx_str}] unsupported init */")
|
||||
|
||||
if init_values: init_value_scl = f"[{', '.join(init_values)}]"
|
||||
elif not is_array and not is_struct_inline and start_value_raw is not None:
|
||||
# Inicialización de variable simple
|
||||
init_value_scl = format_scl_start_value(start_value_raw, base_type_for_init)
|
||||
|
||||
# Añadir parte del valor inicial si existe
|
||||
if init_value_scl is not None:
|
||||
init_value_scl_part = f" := {init_value_scl}"
|
||||
|
||||
# Combinar todo para la línea final
|
||||
declaration_line += f"{init_value_scl_part};"
|
||||
if var_comment: declaration_line += f" // {var_comment}"
|
||||
scl_lines.append(declaration_line)
|
||||
|
||||
return scl_lines
|
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,548 @@
|
|||
# ToUpload/parsers/parse_lad_fbd.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import (
|
||||
ns,
|
||||
parse_access,
|
||||
parse_part,
|
||||
parse_call,
|
||||
get_multilingual_text,
|
||||
)
|
||||
|
||||
# Sufijo usado en x2 para identificar instrucciones procesadas (útil para EN/ENO)
|
||||
SCL_SUFFIX = "_sympy_processed" # Asumimos que este es el sufijo de x2
|
||||
|
||||
|
||||
def parse_lad_fbd_network(network_element):
|
||||
"""
|
||||
Parsea una red LAD/FBD/GRAPH, extrae lógica y añade conexiones EN/ENO implícitas.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
if network_element is None:
|
||||
return {
|
||||
"id": "ERROR",
|
||||
"title": "Invalid Network Element",
|
||||
"logic": [],
|
||||
"error": "Input element was None",
|
||||
}
|
||||
|
||||
network_id = network_element.get("ID")
|
||||
# Usar get_multilingual_text de utils
|
||||
title_element = network_element.xpath(
|
||||
".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
|
||||
)
|
||||
network_title = (
|
||||
get_multilingual_text(title_element[0])
|
||||
if title_element
|
||||
else f"Network {network_id}"
|
||||
)
|
||||
comment_element = network_element.xpath(
|
||||
"./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # OJO: Path relativo a CompileUnit?
|
||||
if not comment_element: # Intentar path alternativo si el anterior falla
|
||||
comment_element = network_element.xpath(
|
||||
".//MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # Más genérico dentro de la red
|
||||
network_comment = (
|
||||
get_multilingual_text(comment_element[0]) if comment_element else ""
|
||||
)
|
||||
|
||||
# --- Determinar Lenguaje (ya que este parser maneja varios) ---
|
||||
network_lang = "Unknown"
|
||||
attr_list_net = network_element.xpath("./AttributeList")
|
||||
if attr_list_net:
|
||||
lang_node_net = attr_list_net[0].xpath("./ProgrammingLanguage/text()")
|
||||
if lang_node_net:
|
||||
network_lang = lang_node_net[0].strip()
|
||||
|
||||
# --- Buscar FlgNet ---
|
||||
# Buscar NetworkSource y luego FlgNet (ambos usan namespace flg)
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
flgnet = None
|
||||
if network_source_node:
|
||||
flgnet_list = network_source_node[0].xpath("./flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
else: # Intentar buscar FlgNet directamente si no hay NetworkSource
|
||||
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
|
||||
if flgnet is None:
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang,
|
||||
"logic": [],
|
||||
"error": "FlgNet not found inside NetworkSource or CompileUnit",
|
||||
}
|
||||
|
||||
# 1. Parse Access, Parts, Calls (usan utils)
|
||||
access_map = {}
|
||||
# Corregir XPath para buscar Access dentro de FlgNet/Parts
|
||||
for acc in flgnet.xpath(".//flg:Parts/flg:Access", namespaces=ns):
|
||||
acc_info = parse_access(acc)
|
||||
if acc_info and acc_info.get("uid") and "error" not in acc_info.get("type", ""):
|
||||
access_map[acc_info["uid"]] = acc_info
|
||||
elif acc_info:
|
||||
print(
|
||||
f"Advertencia: Ignorando Access inválido o con error UID={acc_info.get('uid')} en red {network_id}"
|
||||
)
|
||||
|
||||
parts_and_calls_map = {}
|
||||
# Corregir XPath para buscar Part y Call dentro de FlgNet/Parts
|
||||
instruction_elements = flgnet.xpath(
|
||||
".//flg:Parts/flg:Part | .//flg:Parts/flg:Call", namespaces=ns
|
||||
)
|
||||
for element in instruction_elements:
|
||||
parsed_info = None
|
||||
tag_name = etree.QName(element.tag).localname
|
||||
if tag_name == "Part":
|
||||
parsed_info = parse_part(element) # Usa utils
|
||||
elif tag_name == "Call":
|
||||
parsed_info = parse_call(element) # Usa utils
|
||||
|
||||
if (
|
||||
parsed_info
|
||||
and parsed_info.get("uid")
|
||||
and "error" not in parsed_info.get("type", "")
|
||||
):
|
||||
parts_and_calls_map[parsed_info["uid"]] = parsed_info
|
||||
elif parsed_info:
|
||||
# Si parse_call/parse_part devolvió error, lo guardamos para tener el UID
|
||||
print(
|
||||
f"Advertencia: {tag_name} con error UID={parsed_info.get('uid')} en red {network_id}. Error: {parsed_info.get('error')}"
|
||||
)
|
||||
parts_and_calls_map[parsed_info["uid"]] = (
|
||||
parsed_info # Guardar aunque tenga error
|
||||
)
|
||||
|
||||
# 2. Parse Wires (lógica compleja, mantener aquí)
|
||||
wire_connections = defaultdict(list) # destination -> [source1, source2]
|
||||
source_connections = defaultdict(list) # source -> [dest1, dest2]
|
||||
eno_outputs = defaultdict(list)
|
||||
qname_powerrail = etree.QName(ns["flg"], "Powerrail")
|
||||
qname_identcon = etree.QName(
|
||||
ns["flg"], "IdentCon"
|
||||
) # Conexión a/desde Access (variable/constante)
|
||||
qname_namecon = etree.QName(
|
||||
ns["flg"], "NameCon"
|
||||
) # Conexión a/desde Part/Call (pin con nombre)
|
||||
qname_openbranch = etree.QName(
|
||||
ns["flg"], "Openbranch"
|
||||
) # Rama abierta (normalmente ignorada o tratada como TRUE?)
|
||||
qname_opencon = etree.QName(
|
||||
ns["flg"], "OpenCon"
|
||||
) # Conexión abierta (pin no conectado)
|
||||
|
||||
# Corregir XPath para buscar Wire dentro de FlgNet/Wires
|
||||
for wire in flgnet.xpath(".//flg:Wires/flg:Wire", namespaces=ns):
|
||||
children = wire.getchildren()
|
||||
if len(children) < 2:
|
||||
continue # Necesita al menos origen y destino
|
||||
|
||||
source_elem = children[0]
|
||||
source_uid, source_pin = None, None
|
||||
|
||||
# Determinar origen
|
||||
if source_elem.tag == qname_powerrail:
|
||||
source_uid, source_pin = "POWERRAIL", "out"
|
||||
elif source_elem.tag == qname_identcon: # Origen es una variable/constante
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = "value" # Salida implícita de un Access
|
||||
elif source_elem.tag == qname_namecon: # Origen es pin de instrucción
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = source_elem.get("Name")
|
||||
elif source_elem.tag == qname_openbranch:
|
||||
# ¿Cómo manejar OpenBranch como fuente? Podría ser TRUE o una condición OR implícita
|
||||
source_uid = "OPENBRANCH_" + wire.get(
|
||||
"UId", "Unknown"
|
||||
) # UID único para la rama
|
||||
source_pin = "out"
|
||||
print(
|
||||
f"Advertencia: OpenBranch encontrado como fuente en Wire UID={wire.get('UId')} (Red {network_id}). Tratando como fuente especial."
|
||||
)
|
||||
# No lo añadimos a parts_and_calls_map, get_sympy_representation necesitará manejarlo
|
||||
# Ignorar OpenCon como fuente (no tiene sentido)
|
||||
if source_uid is None or source_pin is None:
|
||||
# print(f"Advertencia: Fuente de wire inválida o no soportada: {source_elem.tag} en Wire UID={wire.get('UId')}")
|
||||
continue
|
||||
|
||||
source_info = (source_uid, source_pin)
|
||||
|
||||
# Procesar destinos
|
||||
for dest_elem in children[1:]:
|
||||
dest_uid, dest_pin = None, None
|
||||
|
||||
if (
|
||||
dest_elem.tag == qname_identcon
|
||||
): # Destino es una variable/constante (asignación)
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = "value" # Entrada implícita de un Access
|
||||
elif dest_elem.tag == qname_namecon: # Destino es pin de instrucción
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = dest_elem.get("Name")
|
||||
# Ignorar Powerrail, OpenBranch, OpenCon como destinos válidos de conexión lógica principal
|
||||
|
||||
if dest_uid is not None and dest_pin is not None:
|
||||
dest_key = (dest_uid, dest_pin)
|
||||
if source_info not in wire_connections[dest_key]:
|
||||
wire_connections[dest_key].append(source_info)
|
||||
|
||||
# Mapa inverso: source -> list of destinations
|
||||
source_key = (source_uid, source_pin)
|
||||
dest_info = (dest_uid, dest_pin)
|
||||
if dest_info not in source_connections[source_key]:
|
||||
source_connections[source_key].append(dest_info)
|
||||
|
||||
# Trackear salidas ENO específicamente si la fuente es una instrucción
|
||||
if source_pin == "eno" and source_uid in parts_and_calls_map:
|
||||
if dest_info not in eno_outputs[source_uid]:
|
||||
eno_outputs[source_uid].append(dest_info)
|
||||
|
||||
# 3. Build Initial Logic Structure (incorporando errores)
|
||||
all_logic_steps = {}
|
||||
# Lista de tipos funcionales (usados para inferencia EN)
|
||||
# Estos son los tipos *originales* de las instrucciones
|
||||
functional_block_types = [
|
||||
"Move",
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Convert",
|
||||
"Call", # Call ya está aquí
|
||||
"TON",
|
||||
"TOF",
|
||||
"TP",
|
||||
"CTU",
|
||||
"CTD",
|
||||
"CTUD",
|
||||
"BLKMOV", # Añadidos
|
||||
"Se",
|
||||
"Sd", # Estos son tipos LAD que se mapearán a timers SCL
|
||||
]
|
||||
# Lista de generadores RLO (usados para inferencia EN)
|
||||
rlo_generators = [
|
||||
"Contact",
|
||||
"O",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
"And",
|
||||
"Xor",
|
||||
"PBox",
|
||||
"NBox",
|
||||
"Not",
|
||||
]
|
||||
|
||||
# Iterar sobre UIDs válidos (los que se pudieron parsear, aunque sea con error)
|
||||
valid_instruction_uids = list(parts_and_calls_map.keys())
|
||||
|
||||
for instruction_uid in valid_instruction_uids:
|
||||
instruction_info = parts_and_calls_map[instruction_uid]
|
||||
# Hacer copia profunda para no modificar el mapa original
|
||||
instruction_repr = copy.deepcopy(instruction_info)
|
||||
instruction_repr["instruction_uid"] = instruction_uid # Asegurar UID
|
||||
instruction_repr["inputs"] = {}
|
||||
instruction_repr["outputs"] = {}
|
||||
|
||||
# Si la instrucción ya tuvo un error de parseo, añadirlo aquí
|
||||
if "error" in instruction_info:
|
||||
instruction_repr["parsing_error"] = instruction_info["error"]
|
||||
# No intentar poblar inputs/outputs si el parseo base falló
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
continue
|
||||
|
||||
original_type = instruction_repr.get("type", "") # Tipo de la instrucción
|
||||
|
||||
# --- Poblar Entradas ---
|
||||
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
|
||||
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
|
||||
# Añadir pines dinámicamente basados en el tipo de instrucción
|
||||
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
|
||||
possible_input_pins.add("operand")
|
||||
elif original_type in [
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
]:
|
||||
possible_input_pins.update(["in1", "in2"])
|
||||
elif original_type in ["TON", "TOF", "TP"]:
|
||||
possible_input_pins.update(["IN", "PT"]) # Pines SCL
|
||||
elif original_type in ["Se", "Sd"]:
|
||||
possible_input_pins.update(["s", "tv", "timer"]) # Pines LAD
|
||||
elif original_type in ["CTU", "CTD", "CTUD"]:
|
||||
possible_input_pins.update(["CU", "CD", "R", "LD", "PV"]) # Pines SCL/LAD
|
||||
elif original_type in ["PBox", "NBox"]:
|
||||
possible_input_pins.update(
|
||||
["bit", "clk", "in"]
|
||||
) # PBox/NBox usa 'in' y 'bit'
|
||||
elif original_type == "BLKMOV":
|
||||
possible_input_pins.add("SRCBLK")
|
||||
elif original_type == "Move":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Convert":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Call":
|
||||
# Para Calls, los nombres de los parámetros reales se definen en el XML
|
||||
# El Xpath busca Parameter DENTRO de CallInfo, que está DENTRO de Call
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_xml_element = call_xml_element_list[0]
|
||||
call_info_node_list = call_xml_element.xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
call_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter/@Name", namespaces=ns
|
||||
)
|
||||
possible_input_pins.update(call_param_names)
|
||||
# print(f"DEBUG Call UID={instruction_uid}: Params={call_param_names}")
|
||||
else: # Fallback si no hay namespace (menos probable)
|
||||
call_info_node_list_no_ns = call_xml_element.xpath("./CallInfo")
|
||||
if call_info_node_list_no_ns:
|
||||
possible_input_pins.update(
|
||||
call_info_node_list_no_ns[0].xpath("./Parameter/@Name")
|
||||
)
|
||||
|
||||
# Iterar sobre pines posibles y buscar conexiones
|
||||
for pin_name in possible_input_pins:
|
||||
dest_key = (instruction_uid, pin_name)
|
||||
if dest_key in wire_connections:
|
||||
sources_list = wire_connections[dest_key]
|
||||
input_sources_repr = []
|
||||
for source_uid, source_pin in sources_list:
|
||||
source_repr = None
|
||||
if source_uid == "POWERRAIL":
|
||||
source_repr = {"type": "powerrail"}
|
||||
elif source_uid.startswith("OPENBRANCH_"):
|
||||
source_repr = {
|
||||
"type": "openbranch",
|
||||
"uid": source_uid,
|
||||
} # Fuente especial
|
||||
elif source_uid in access_map:
|
||||
source_repr = copy.deepcopy(access_map[source_uid])
|
||||
elif source_uid in parts_and_calls_map:
|
||||
source_instr_info = parts_and_calls_map[source_uid]
|
||||
source_repr = {
|
||||
"type": "connection",
|
||||
"source_instruction_type": source_instr_info.get(
|
||||
"type", "Unknown"
|
||||
), # Usar tipo base
|
||||
"source_instruction_uid": source_uid,
|
||||
"source_pin": source_pin,
|
||||
}
|
||||
else:
|
||||
# Fuente desconocida (ni Access, ni Part/Call válido)
|
||||
print(
|
||||
f"Advertencia: Fuente desconocida UID={source_uid} conectada a {instruction_uid}.{pin_name}"
|
||||
)
|
||||
source_repr = {"type": "unknown_source", "uid": source_uid}
|
||||
input_sources_repr.append(source_repr)
|
||||
|
||||
# Guardar la representación de la entrada (lista o dict)
|
||||
instruction_repr["inputs"][pin_name] = (
|
||||
input_sources_repr[0]
|
||||
if len(input_sources_repr) == 1
|
||||
else input_sources_repr
|
||||
)
|
||||
|
||||
# --- Poblar Salidas (simplificado: solo conexiones a Access) ---
|
||||
possible_output_pins = set(
|
||||
[
|
||||
"out",
|
||||
"out1",
|
||||
"Q",
|
||||
"q",
|
||||
"eno",
|
||||
"RET_VAL",
|
||||
"DSTBLK",
|
||||
"rt",
|
||||
"cv",
|
||||
"QU",
|
||||
"QD",
|
||||
"ET", # Añadir pines de salida estándar SCL
|
||||
]
|
||||
)
|
||||
if original_type == "BLKMOV":
|
||||
possible_output_pins.add("DSTBLK")
|
||||
if (
|
||||
original_type == "Call"
|
||||
): # Para Calls, las salidas dependen del bloque llamado
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_info_node_list = call_xml_element_list[0].xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
# Buscar parámetros con Section="Output" o "InOut" o "Return"
|
||||
output_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter[@Section='Output' or @Section='InOut' or @Section='Return']/@Name",
|
||||
namespaces=ns,
|
||||
)
|
||||
possible_output_pins.update(output_param_names)
|
||||
|
||||
for pin_name in possible_output_pins:
|
||||
source_key = (instruction_uid, pin_name)
|
||||
if source_key in source_connections:
|
||||
if pin_name not in instruction_repr["outputs"]:
|
||||
instruction_repr["outputs"][pin_name] = []
|
||||
for dest_uid, dest_pin in source_connections[source_key]:
|
||||
if (
|
||||
dest_uid in access_map
|
||||
): # Solo registrar si va a una variable/constante
|
||||
dest_operand_copy = copy.deepcopy(access_map[dest_uid])
|
||||
if (
|
||||
dest_operand_copy
|
||||
not in instruction_repr["outputs"][pin_name]
|
||||
):
|
||||
instruction_repr["outputs"][pin_name].append(
|
||||
dest_operand_copy
|
||||
)
|
||||
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
|
||||
# 4. Inferencia EN (modificado para usar tipos originales)
|
||||
processed_blocks_en_inference = set()
|
||||
try:
|
||||
# Ordenar UIDs numéricamente si es posible
|
||||
sorted_uids_for_en = sorted(
|
||||
all_logic_steps.keys(),
|
||||
key=lambda x: (
|
||||
int(x) if isinstance(x, str) and x.isdigit() else float("inf")
|
||||
),
|
||||
)
|
||||
except ValueError:
|
||||
sorted_uids_for_en = sorted(all_logic_steps.keys()) # Fallback sort
|
||||
|
||||
ordered_logic_list_for_en = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
for i, instruction in enumerate(ordered_logic_list_for_en):
|
||||
part_uid = instruction["instruction_uid"]
|
||||
# Usar el tipo original para la lógica de inferencia
|
||||
part_type_original = (
|
||||
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
|
||||
)
|
||||
|
||||
# Inferencia solo para tipos funcionales que no tengan EN explícito
|
||||
if (
|
||||
part_type_original in functional_block_types
|
||||
and "en" not in instruction.get("inputs", {})
|
||||
and part_uid not in processed_blocks_en_inference
|
||||
and "error" not in part_type_original
|
||||
): # No inferir para errores
|
||||
|
||||
inferred_en_source = None
|
||||
# Buscar hacia atrás en la lista ordenada
|
||||
if i > 0:
|
||||
for j in range(i - 1, -1, -1):
|
||||
prev_instr = ordered_logic_list_for_en[j]
|
||||
if "error" in prev_instr.get("type", ""):
|
||||
continue # Saltar errores previos
|
||||
|
||||
prev_uid = prev_instr["instruction_uid"]
|
||||
prev_type_original = (
|
||||
prev_instr.get("type", "")
|
||||
.replace(SCL_SUFFIX, "")
|
||||
.replace("_error", "")
|
||||
)
|
||||
|
||||
if prev_type_original in rlo_generators: # Fuente RLO encontrada
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "out",
|
||||
}
|
||||
break # Detener búsqueda
|
||||
elif (
|
||||
prev_type_original in functional_block_types
|
||||
): # Bloque funcional previo
|
||||
# Comprobar si este bloque tiene salida ENO conectada
|
||||
if (prev_uid, "eno") in source_connections:
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "eno",
|
||||
}
|
||||
# Si no tiene ENO conectado, el flujo RLO se detiene aquí
|
||||
break # Detener búsqueda
|
||||
elif prev_type_original in [
|
||||
"Coil",
|
||||
"SCoil",
|
||||
"RCoil",
|
||||
"SdCoil",
|
||||
"SetCoil",
|
||||
"ResetCoil",
|
||||
]:
|
||||
# Bobinas terminan el flujo RLO
|
||||
break # Detener búsqueda
|
||||
|
||||
# Si no se encontró fuente, conectar a PowerRail
|
||||
if inferred_en_source is None:
|
||||
inferred_en_source = {"type": "powerrail"}
|
||||
|
||||
# Actualizar la instrucción EN el diccionario principal
|
||||
if part_uid in all_logic_steps:
|
||||
# Asegurar que inputs exista
|
||||
if "inputs" not in all_logic_steps[part_uid]:
|
||||
all_logic_steps[part_uid]["inputs"] = {}
|
||||
all_logic_steps[part_uid]["inputs"]["en"] = inferred_en_source
|
||||
processed_blocks_en_inference.add(part_uid)
|
||||
|
||||
# 5. Lógica ENO (añadir destinos ENO si existen)
|
||||
for source_instr_uid, eno_destinations in eno_outputs.items():
|
||||
if source_instr_uid in all_logic_steps and "error" not in all_logic_steps[
|
||||
source_instr_uid
|
||||
].get("type", ""):
|
||||
all_logic_steps[source_instr_uid]["eno_destinations"] = eno_destinations
|
||||
|
||||
# 6. Ordenar y Devolver
|
||||
final_logic_list = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang, # Lenguaje original de la red
|
||||
"logic": final_logic_list,
|
||||
# No añadir 'error' aquí a menos que el parseo completo falle
|
||||
}
|
||||
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
# Este parser maneja LAD, FBD y GRAPH
|
||||
return {
|
||||
"language": ["LAD", "FBD", "GRAPH"], # Lista de lenguajes soportados
|
||||
"parser_func": parse_lad_fbd_network, # Función a llamar
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
# ToUpload/parsers/parse_scl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import ns, get_multilingual_text
|
||||
|
||||
def reconstruct_scl_from_tokens(st_node):
|
||||
"""
|
||||
Reconstruye SCL desde <StructuredText>, mejorando el manejo de
|
||||
variables, constantes literales, tokens básicos, espacios y saltos de línea.
|
||||
"""
|
||||
if st_node is None:
|
||||
return "// Error: StructuredText node not found.\n"
|
||||
|
||||
scl_parts = []
|
||||
# Usar st:* para obtener todos los elementos hijos dentro del namespace st
|
||||
children = st_node.xpath("./st:*", namespaces=ns)
|
||||
|
||||
for elem in children:
|
||||
tag = etree.QName(elem.tag).localname
|
||||
|
||||
if tag == "Token":
|
||||
scl_parts.append(elem.get("Text", ""))
|
||||
elif tag == "Blank":
|
||||
# Añadir espacios solo si es necesario o más de uno
|
||||
num_spaces = int(elem.get("Num", 1))
|
||||
if not scl_parts or not scl_parts[-1].endswith(" "):
|
||||
scl_parts.append(" " * num_spaces)
|
||||
elif num_spaces > 1:
|
||||
scl_parts.append(" " * (num_spaces -1))
|
||||
|
||||
elif tag == "NewLine":
|
||||
# Quitar espacios finales antes del salto de línea
|
||||
if scl_parts:
|
||||
scl_parts[-1] = scl_parts[-1].rstrip()
|
||||
scl_parts.append("\n")
|
||||
elif tag == "Access":
|
||||
scope = elem.get("Scope")
|
||||
access_str = f"/*_ERR_Scope_{scope}_*/" # Placeholder
|
||||
|
||||
# --- Variables ---
|
||||
if scope in [
|
||||
"GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable",
|
||||
"InputVariable", "OutputVariable", "ConstantVariable",
|
||||
"GlobalConstant", "LocalConstant" # Añadir constantes simbólicas
|
||||
]:
|
||||
symbol_elem = elem.xpath("./st:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./st:Component", namespaces=ns)
|
||||
symbol_text_parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
if i > 0: symbol_text_parts.append(".")
|
||||
|
||||
# Check for HasQuotes attribute (adjust namespace if needed)
|
||||
# El atributo está en el Component o en el Access padre? Probar ambos
|
||||
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true" # Check directly on Component
|
||||
has_quotes_access = False
|
||||
access_parent = comp.xpath("ancestor::st:Access[1]", namespaces=ns) # Get immediate Access parent
|
||||
if access_parent:
|
||||
has_quotes_attr = access_parent[0].xpath("./st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns)
|
||||
has_quotes_access = has_quotes_attr and has_quotes_attr[0].lower() == 'true'
|
||||
|
||||
has_quotes = has_quotes_comp or has_quotes_access
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
# Apply quotes based on HasQuotes or if it's the first component and not temp
|
||||
if has_quotes or (i == 0 and not is_temp and '"' not in name): # Avoid double quotes
|
||||
symbol_text_parts.append(f'"{name}"')
|
||||
else:
|
||||
symbol_text_parts.append(name)
|
||||
|
||||
# --- Array Index Access ---
|
||||
index_access_nodes = comp.xpath("./st:Access", namespaces=ns)
|
||||
if index_access_nodes:
|
||||
# Llamada recursiva para cada índice
|
||||
indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access_nodes]
|
||||
# Limpiar saltos de línea dentro de los corchetes
|
||||
indices_cleaned = [idx.replace('\n', '').strip() for idx in indices_text]
|
||||
symbol_text_parts.append(f"[{','.join(indices_cleaned)}]")
|
||||
|
||||
access_str = "".join(symbol_text_parts)
|
||||
else:
|
||||
access_str = f"/*_ERR_NO_SYMBOL_IN_{scope}_*/"
|
||||
|
||||
# --- Constantes Literales ---
|
||||
elif scope == "LiteralConstant":
|
||||
constant_elem = elem.xpath("./st:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns)
|
||||
const_type = type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else ""
|
||||
const_val = val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONSTVAL_"
|
||||
|
||||
# Formatear según tipo
|
||||
if const_type == "bool": access_str = const_val.upper()
|
||||
elif const_type.lower() == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type.lower() == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WSTRING#'{replaced_val}'"
|
||||
elif const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WCHAR#'{replaced_val}'"
|
||||
elif const_type == "time": access_str = f"T#{const_val}"
|
||||
elif const_type == "ltime": access_str = f"LT#{const_val}"
|
||||
elif const_type == "s5time": access_str = f"S5T#{const_val}"
|
||||
elif const_type == "date": access_str = f"D#{const_val}"
|
||||
elif const_type == "dtl": access_str = f"DTL#{const_val}"
|
||||
elif const_type == "dt": access_str = f"DT#{const_val}"
|
||||
elif const_type == "tod": access_str = f"TOD#{const_val}"
|
||||
elif const_type in ["int", "dint", "sint", "usint", "uint", "udint", "real", "lreal", "word", "dword", "byte"]:
|
||||
# Añadir .0 para reales si no tienen decimal
|
||||
if const_type in ["real", "lreal"] and '.' not in const_val and 'e' not in const_val.lower():
|
||||
access_str = f"{const_val}.0"
|
||||
else:
|
||||
access_str = const_val
|
||||
else: # Otros tipos (LWORD, etc.) o desconocidos
|
||||
access_str = const_val
|
||||
else:
|
||||
access_str = "/*_ERR_NOCONST_*/"
|
||||
|
||||
# --- Llamadas a Funciones/Bloques (Scope=Call) ---
|
||||
elif scope == "Call":
|
||||
call_info_node = elem.xpath("./st:CallInfo", namespaces=ns)
|
||||
if call_info_node:
|
||||
ci = call_info_node[0]
|
||||
call_name = ci.get("Name", "_ERR_CALLNAME_")
|
||||
call_type = ci.get("BlockType") # FB, FC, etc.
|
||||
|
||||
# Parámetros (están como Access o Token dentro de CallInfo/Parameter)
|
||||
params = ci.xpath("./st:Parameter", namespaces=ns)
|
||||
param_parts = []
|
||||
for p in params:
|
||||
p_name = p.get("Name", "_ERR_PARAMNAME_")
|
||||
# El valor del parámetro está dentro del nodo Parameter
|
||||
p_value_node = p.xpath("./st:Access | ./st:Token", namespaces=ns) # Buscar Access o Token
|
||||
p_value_scl = ""
|
||||
if p_value_node:
|
||||
p_value_scl = reconstruct_scl_from_tokens(p) # Parsear el contenido del parámetro
|
||||
p_value_scl = p_value_scl.replace('\n', '').strip() # Limpiar SCL resultante
|
||||
param_parts.append(f"{p_name} := {p_value_scl}")
|
||||
|
||||
# Manejar FB vs FC
|
||||
if call_type == "FB":
|
||||
instance_node = ci.xpath("./st:Instance/st:Component/@Name", namespaces=ns)
|
||||
if instance_node:
|
||||
instance_name = f'"{instance_node[0]}"'
|
||||
access_str = f"{instance_name}({', '.join(param_parts)})"
|
||||
else: # FB sin instancia? Podría ser STAT
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* FB sin instancia explícita? *)'
|
||||
elif call_type == "FC":
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)})'
|
||||
else: # Otros tipos de llamada
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* Tipo: {call_type} *)'
|
||||
else:
|
||||
access_str = "/*_ERR_NO_CALLINFO_*/"
|
||||
|
||||
# Añadir más scopes si son necesarios (e.g., Address, Label, Reference)
|
||||
|
||||
scl_parts.append(access_str)
|
||||
|
||||
elif tag == "Comment" or tag == "LineComment":
|
||||
# Usar get_multilingual_text del parser_utils
|
||||
comment_text = get_multilingual_text(elem)
|
||||
if tag == "Comment":
|
||||
scl_parts.append(f"(* {comment_text} *)")
|
||||
else:
|
||||
scl_parts.append(f"// {comment_text}")
|
||||
# Ignorar otros tipos de nodos si no son relevantes para el SCL
|
||||
|
||||
full_scl = "".join(scl_parts)
|
||||
|
||||
# --- Re-indentación Simple ---
|
||||
output_lines = []
|
||||
indent_level = 0
|
||||
indent_str = " " # Dos espacios
|
||||
for line in full_scl.splitlines():
|
||||
trimmed_line = line.strip()
|
||||
if not trimmed_line:
|
||||
# Mantener líneas vacías? Opcional.
|
||||
# output_lines.append("")
|
||||
continue
|
||||
|
||||
# Reducir indentación ANTES de imprimir para END, ELSE, etc.
|
||||
if trimmed_line.upper().startswith(("END_", "UNTIL", "}")) or \
|
||||
trimmed_line.upper() in ["ELSE", "ELSIF"]:
|
||||
indent_level = max(0, indent_level - 1)
|
||||
|
||||
output_lines.append(indent_str * indent_level + trimmed_line)
|
||||
|
||||
# Aumentar indentación DESPUÉS de imprimir para IF, FOR, etc.
|
||||
# Ser más específico con las palabras clave que aumentan indentación
|
||||
# Usar .upper() para ignorar mayúsculas/minúsculas
|
||||
line_upper = trimmed_line.upper()
|
||||
if line_upper.endswith(("THEN", "DO", "OF", "{")) or \
|
||||
line_upper.startswith(("IF ", "FOR ", "WHILE ", "CASE ", "REPEAT", "STRUCT")) or \
|
||||
line_upper == "ELSE":
|
||||
# Excepción: No indentar después de ELSE IF
|
||||
if not (line_upper == "ELSE" and "IF" in output_lines[-1].upper()):
|
||||
indent_level += 1
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def parse_scl_network(network_element):
|
||||
"""
|
||||
Parsea una red SCL extrayendo el código fuente reconstruido.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSCL_ID")
|
||||
network_lang = "SCL" # Sabemos que es SCL
|
||||
|
||||
# Buscar NetworkSource y luego StructuredText
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
structured_text_node = None
|
||||
if network_source_node:
|
||||
structured_text_node_list = network_source_node[0].xpath("./st:StructuredText", namespaces=ns)
|
||||
if structured_text_node_list:
|
||||
structured_text_node = structured_text_node_list[0]
|
||||
|
||||
reconstructed_scl = "// SCL extraction failed: StructuredText node not found.\n"
|
||||
if structured_text_node is not None:
|
||||
reconstructed_scl = reconstruct_scl_from_tokens(structured_text_node)
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"logic": [ # SCL se guarda como un único bloque lógico
|
||||
{
|
||||
"instruction_uid": f"SCL_{network_id}", # UID sintético
|
||||
"type": "RAW_SCL_CHUNK", # Tipo especial para SCL crudo
|
||||
"scl": reconstructed_scl, # El código SCL reconstruido
|
||||
}
|
||||
],
|
||||
# No añadimos error aquí, reconstruct_scl_from_tokens ya incluye comentarios de error
|
||||
}
|
||||
return parsed_network_data
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {
|
||||
'language': ['SCL'], # Lista de lenguajes soportados
|
||||
'parser_func': parse_scl_network # Función a llamar
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
# ToUpload/parsers/parse_stl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import traceback
|
||||
import re # Needed for substitutions in get_access_text_stl
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
# ns y get_multilingual_text son necesarios
|
||||
from .parser_utils import ns, get_multilingual_text
|
||||
|
||||
# --- Funciones Auxiliares de Reconstrucción STL ---
|
||||
|
||||
|
||||
def get_access_text_stl(access_element):
|
||||
"""
|
||||
Reconstruye una representación textual simple de un Access en STL.
|
||||
Intenta manejar los diferentes tipos de acceso definidos en el XSD.
|
||||
"""
|
||||
if access_element is None:
|
||||
return "_ERR_ACCESS_"
|
||||
|
||||
# --- Símbolo (Variable, Constante Simbólica) ---
|
||||
# Busca <Symbol> dentro del <Access> usando el namespace stl
|
||||
symbol_elem = access_element.xpath("./stl:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./stl:Component", namespaces=ns)
|
||||
parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
# Comprobar HasQuotes (puede estar en el Access o Componente, priorizar Componente)
|
||||
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true"
|
||||
has_quotes_access = False
|
||||
access_parent = comp.xpath("ancestor::stl:Access[1]", namespaces=ns)
|
||||
if access_parent:
|
||||
has_quotes_attr = access_parent[0].xpath(
|
||||
"./stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns
|
||||
)
|
||||
has_quotes_access = (
|
||||
has_quotes_attr and has_quotes_attr[0].lower() == "true"
|
||||
)
|
||||
|
||||
has_quotes = has_quotes_comp or has_quotes_access
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
if i > 0:
|
||||
parts.append(".") # Separador para estructuras
|
||||
|
||||
# Aplicar comillas si es necesario
|
||||
if has_quotes or (
|
||||
i == 0 and not is_temp and '"' not in name and "." not in name
|
||||
):
|
||||
# Añadir comillas si HasQuotes es true, o si es el primer componente,
|
||||
# no es temporal, no tiene ya comillas, y no es parte de una DB (ej. DB10.DBX0.0)
|
||||
parts.append(f'"{name}"')
|
||||
else:
|
||||
parts.append(name)
|
||||
|
||||
# Índices de Array (Access anidado dentro de Component)
|
||||
index_access = comp.xpath("./stl:Access", namespaces=ns)
|
||||
if index_access:
|
||||
indices = [get_access_text_stl(ia) for ia in index_access]
|
||||
# Limpiar índices (quitar saltos de línea, etc.)
|
||||
indices_cleaned = [idx.replace("\n", "").strip() for idx in indices]
|
||||
parts.append(f"[{','.join(indices_cleaned)}]")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
# --- Constante Literal ---
|
||||
# Busca <Constant> dentro del <Access> usando el namespace stl
|
||||
constant_elem = access_element.xpath("./stl:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
# Obtener valor y tipo
|
||||
val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns)
|
||||
const_type = (
|
||||
type_elem[0].strip().lower()
|
||||
if type_elem and type_elem[0] is not None
|
||||
else ""
|
||||
)
|
||||
const_val = (
|
||||
val_elem[0].strip()
|
||||
if val_elem and val_elem[0] is not None
|
||||
else "_ERR_CONST_"
|
||||
)
|
||||
|
||||
# Añadir prefijos estándar STL
|
||||
if const_type == "time":
|
||||
return f"T#{const_val}"
|
||||
if const_type == "s5time":
|
||||
return f"S5T#{const_val}"
|
||||
if const_type == "date":
|
||||
return f"D#{const_val}"
|
||||
if const_type == "dt":
|
||||
return f"DT#{const_val}"
|
||||
if const_type == "time_of_day" or const_type == "tod":
|
||||
return f"TOD#{const_val}"
|
||||
if const_type == "ltime":
|
||||
return f"LT#{const_val}" # Añadido LTIME
|
||||
if const_type == "dtl":
|
||||
return f"DTL#{const_val}" # Añadido DTL
|
||||
|
||||
# Strings y Chars (Escapar comillas simples internas)
|
||||
if const_type == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
return f"'{replaced_val}'"
|
||||
if const_type == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
return f"'{replaced_val}'"
|
||||
if const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
return f"WSTRING#'{replaced_val}'"
|
||||
if const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
return f"WCHAR#'{replaced_val}'"
|
||||
|
||||
# Tipos numéricos con prefijo opcional (Hexadecimal)
|
||||
if const_val.startswith("16#"):
|
||||
if const_type == "byte":
|
||||
return f"B#{const_val}"
|
||||
if const_type == "word":
|
||||
return f"W#{const_val}"
|
||||
if const_type == "dword":
|
||||
return f"DW#{const_val}"
|
||||
if const_type == "lword":
|
||||
return f"LW#{const_val}" # Añadido LWORD
|
||||
|
||||
# Formato Real (añadir .0 si es necesario)
|
||||
if (
|
||||
const_type in ["real", "lreal"]
|
||||
and "." not in const_val
|
||||
and "e" not in const_val.lower()
|
||||
):
|
||||
# Verificar si es un número antes de añadir .0
|
||||
try:
|
||||
float(const_val) # Intenta convertir a float
|
||||
return f"{const_val}.0"
|
||||
except ValueError:
|
||||
return const_val # No es número, devolver tal cual
|
||||
# Otros tipos numéricos o desconocidos
|
||||
return const_val # Valor por defecto
|
||||
|
||||
# --- Etiqueta (Label) ---
|
||||
# Busca <Label> dentro del <Access> usando el namespace stl
|
||||
label_elem = access_element.xpath("./stl:Label", namespaces=ns)
|
||||
if label_elem:
|
||||
return label_elem[0].get("Name", "_ERR_LABEL_")
|
||||
|
||||
# --- Acceso Indirecto (Punteros) ---
|
||||
# Busca <Indirect> dentro del <Access> usando el namespace stl
|
||||
indirect_elem = access_element.xpath("./stl:Indirect", namespaces=ns)
|
||||
if indirect_elem:
|
||||
reg = indirect_elem[0].get("Register", "AR?") # AR1, AR2
|
||||
offset_str = indirect_elem[0].get("BitOffset", "0")
|
||||
area = indirect_elem[0].get("Area", "DB") # DB, DI, L, etc.
|
||||
width = indirect_elem[0].get("Width", "X") # Bit, Byte, Word, Double, Long
|
||||
try:
|
||||
bit_offset = int(offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
p_format_offset = f"P#{byte_offset}.{bit_in_byte}"
|
||||
except ValueError:
|
||||
p_format_offset = "P#?.?"
|
||||
|
||||
width_map = {
|
||||
"Bit": "X",
|
||||
"Byte": "B",
|
||||
"Word": "W",
|
||||
"Double": "D",
|
||||
"Long": "D",
|
||||
} # Mapeo XSD a STL
|
||||
width_char = width_map.get(
|
||||
width, width[0] if width else "?"
|
||||
) # Usar primera letra como fallback
|
||||
|
||||
# Área: DB, DI, L son comunes. Otras podrían necesitar mapeo.
|
||||
area_char = (
|
||||
area[0] if area else "?"
|
||||
) # Usar primera letra (I, O, M, L, T, C, DB, DI...)
|
||||
|
||||
# Formato: AREAREG[puntero], ej. DBX[AR1,P#0.0] o LX[AR2,P#10.5]
|
||||
return f"{area}{width_char}[{reg},{p_format_offset}]"
|
||||
|
||||
# --- Dirección Absoluta ---
|
||||
# Busca <Address> dentro del <Access> usando el namespace stl
|
||||
address_elem = access_element.xpath("./stl:Address", namespaces=ns)
|
||||
if address_elem:
|
||||
area = address_elem[0].get(
|
||||
"Area", "??"
|
||||
) # Input, Output, Memory, DB, DI, Local, Timer, Counter...
|
||||
bit_offset_str = address_elem[0].get("BitOffset", "0")
|
||||
# El tipo (Type) del Address define el ancho por defecto
|
||||
addr_type_str = address_elem[0].get(
|
||||
"Type", "Bool"
|
||||
) # Bool, Byte, Word, DWord, Int, DInt, Real...
|
||||
block_num_str = address_elem[0].get(
|
||||
"BlockNumber"
|
||||
) # Para DB10.DBX0.0 o DI5.DIW2
|
||||
|
||||
try:
|
||||
bit_offset = int(bit_offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
|
||||
# Determinar ancho (X, B, W, D) basado en Type
|
||||
addr_width = "X" # Default bit (Bool)
|
||||
type_lower = addr_type_str.lower()
|
||||
if type_lower in ["byte", "sint", "usint"]:
|
||||
addr_width = "B"
|
||||
elif type_lower in ["word", "int", "uint", "timer", "counter"]:
|
||||
addr_width = "W" # T y C usan W para direccionamiento base
|
||||
elif type_lower in [
|
||||
"dword",
|
||||
"dint",
|
||||
"udint",
|
||||
"real",
|
||||
"time",
|
||||
"dt",
|
||||
"tod",
|
||||
"date_and_time",
|
||||
]:
|
||||
addr_width = "D"
|
||||
elif type_lower in [
|
||||
"lreal",
|
||||
"ltime",
|
||||
"lword",
|
||||
"lint",
|
||||
"ulint",
|
||||
"ltod",
|
||||
"ldt",
|
||||
"date_and_ltime",
|
||||
]:
|
||||
addr_width = "D" # Asumir que direccionamiento base usa D para L*
|
||||
|
||||
# Mapear Área XML a Área STL
|
||||
area_map = {
|
||||
"Input": "I",
|
||||
"Output": "Q",
|
||||
"Memory": "M",
|
||||
"PeripheryInput": "PI",
|
||||
"PeripheryOutput": "PQ",
|
||||
"DB": "DB",
|
||||
"DI": "DI",
|
||||
"Local": "L",
|
||||
"Timer": "T",
|
||||
"Counter": "C",
|
||||
}
|
||||
stl_area = area_map.get(area, area) # Usar nombre XML si no está en el mapa
|
||||
|
||||
if stl_area in ["T", "C"]:
|
||||
# Temporizadores y Contadores usan solo el número (offset de byte)
|
||||
return f"{stl_area}{byte_offset}" # T 5, C 10
|
||||
|
||||
elif stl_area in ["DB", "DI"]:
|
||||
block_num = (
|
||||
block_num_str if block_num_str else ""
|
||||
) # Número de bloque si existe
|
||||
# Formato: DBNum.DBAnchoByte.Bit o DINum.DIAnchoByte.Bit o DBAnchoByte.Bit (si BlockNum es None)
|
||||
db_prefix = f"{stl_area}{block_num}." if block_num else ""
|
||||
return f"{db_prefix}{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
else: # I, Q, M, L, PI, PQ
|
||||
# Formato: AreaAnchoByte.Bit (ej: M B 10 . 1 -> MB10.1 ; I W 0 . 0 -> IW0.0)
|
||||
# Corrección: No añadir bit si el ancho no es X
|
||||
if addr_width == "X":
|
||||
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
else:
|
||||
return f"{stl_area}{addr_width}{byte_offset}" # ej: MB10, IW0, QW4
|
||||
|
||||
except ValueError:
|
||||
return f"{area}?{bit_offset_str}?" # Error de formato
|
||||
|
||||
# --- CallInfo (para operando de CALL) ---
|
||||
# Busca <CallInfo> dentro del <Access> usando el namespace stl
|
||||
call_info_elem = access_element.xpath("./stl:CallInfo", namespaces=ns)
|
||||
if call_info_elem:
|
||||
name = call_info_elem[0].get("Name", "_ERR_CALL_")
|
||||
btype = call_info_elem[0].get("BlockType", "FC") # FC, FB
|
||||
|
||||
# El operando de CALL depende del tipo de bloque
|
||||
if btype == "FB":
|
||||
# Para CALL FB, el operando es el DB de instancia
|
||||
instance_node = call_info_elem[0].xpath(
|
||||
".//stl:Component/@Name", namespaces=ns
|
||||
) # Buscar nombre dentro de Instance/Component
|
||||
if instance_node:
|
||||
db_name_raw = instance_node[0]
|
||||
# Añadir comillas si no las tiene
|
||||
return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw
|
||||
else:
|
||||
return f'"_ERR_FB_INSTANCE_NAME_({name})_"'
|
||||
else: # FC o desconocido
|
||||
# Para CALL FC, el operando es el nombre del FC
|
||||
# Añadir comillas si no las tiene
|
||||
return f'"{name}"' if '"' not in name else name
|
||||
|
||||
# Fallback si no se reconoce el tipo de Access
|
||||
scope = access_element.get("Scope", "UnknownScope")
|
||||
return f"_{scope}_?"
|
||||
|
||||
|
||||
def get_comment_text_stl(comment_element):
|
||||
"""
|
||||
Extrae texto de un LineComment o Comment para STL usando get_multilingual_text.
|
||||
Se asume que get_multilingual_text ya está importado y maneja <Comment> y <LineComment>.
|
||||
"""
|
||||
return get_multilingual_text(comment_element) if comment_element is not None else ""
|
||||
|
||||
|
||||
def reconstruct_stl_from_statementlist(statement_list_node):
|
||||
"""
|
||||
Reconstruye el código STL como una cadena de texto desde <StatementList>.
|
||||
Usa las funciones auxiliares get_access_text_stl y get_comment_text_stl.
|
||||
"""
|
||||
if statement_list_node is None:
|
||||
return "// Error: StatementList node not found.\n"
|
||||
|
||||
stl_lines = []
|
||||
# Buscar todos los StlStatement hijos usando el namespace 'stl'
|
||||
statements = statement_list_node.xpath("./stl:StlStatement", namespaces=ns)
|
||||
|
||||
for stmt in statements:
|
||||
line_parts = []
|
||||
inline_comment = "" # Comentarios en la misma línea
|
||||
|
||||
# 1. Comentarios iniciales (línea completa //)
|
||||
# Buscar <Comment> o <LineComment> que sean hijos directos de StlStatement
|
||||
# y NO tengan el atributo Inserted="true" (o no tengan Inserted)
|
||||
initial_comments = stmt.xpath(
|
||||
"child::stl:Comment[not(@Inserted='true')] | child::stl:LineComment[not(@Inserted='true')]",
|
||||
namespaces=ns,
|
||||
)
|
||||
for comm in initial_comments:
|
||||
comment_text = get_comment_text_stl(comm) # Usa la función auxiliar
|
||||
if comment_text:
|
||||
for comment_line in comment_text.splitlines():
|
||||
stl_lines.append(
|
||||
f"// {comment_line.strip()}"
|
||||
) # Añadir como comentario SCL
|
||||
|
||||
# 2. Etiqueta (LabelDeclaration)
|
||||
# Buscar <LabelDeclaration> hijo directo
|
||||
label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns)
|
||||
label_str = ""
|
||||
if label_decl:
|
||||
label_name_node = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns)
|
||||
if label_name_node:
|
||||
label_str = f"{label_name_node[0]}:" # Añadir dos puntos
|
||||
# Comentarios después de la etiqueta (inline) - Tienen Inserted="true"
|
||||
label_comments = label_decl[0].xpath(
|
||||
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
|
||||
namespaces=ns,
|
||||
)
|
||||
for lcomm in label_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(lcomm).strip()}" # Acumular comentarios inline
|
||||
|
||||
if label_str:
|
||||
line_parts.append(
|
||||
label_str
|
||||
) # Añadir etiqueta (si existe) a las partes de la línea
|
||||
|
||||
# 3. Instrucción (StlToken)
|
||||
# Buscar <StlToken> hijo directo
|
||||
instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns)
|
||||
instruction_str = ""
|
||||
if instruction_token:
|
||||
token_text = instruction_token[0].get("Text", "_ERR_TOKEN_")
|
||||
# Manejar casos especiales definidos en el XSD
|
||||
if token_text == "EMPTY_LINE":
|
||||
if (
|
||||
not stl_lines or stl_lines[-1]
|
||||
): # Evitar múltiples líneas vacías seguidas
|
||||
stl_lines.append("") # Añadir línea vacía
|
||||
continue # Saltar resto del statement (no hay instrucción ni operando)
|
||||
elif token_text == "COMMENT":
|
||||
# Ya manejado por initial_comments. Si hubiera comentarios SÓLO aquí, se necesitaría extraerlos.
|
||||
pass # Asumir manejado antes
|
||||
elif token_text == "Assign":
|
||||
instruction_str = "=" # Mapear Assign a '='
|
||||
elif token_text == "OPEN_DB":
|
||||
instruction_str = "AUF" # Mapear OPEN_DB a AUF
|
||||
elif token_text == "OPEN_DI":
|
||||
instruction_str = "AUF DI" # Mapear OPEN_DI a AUF DI
|
||||
# Añadir más mapeos si son necesarios (ej. EQ_I a ==I)
|
||||
else:
|
||||
instruction_str = token_text # Usar el texto del token como instrucción
|
||||
|
||||
# Comentarios asociados al token (inline) - Tienen Inserted="true"
|
||||
token_comments = instruction_token[0].xpath(
|
||||
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
|
||||
namespaces=ns,
|
||||
)
|
||||
for tcomm in token_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(tcomm).strip()}"
|
||||
|
||||
if instruction_str:
|
||||
# Añadir tabulación si hubo etiqueta para alinear instrucciones
|
||||
line_parts.append("\t" + instruction_str if label_str else instruction_str)
|
||||
|
||||
# 4. Operando (Access)
|
||||
# Buscar <Access> hijo directo
|
||||
access_elem = stmt.xpath("./stl:Access", namespaces=ns)
|
||||
access_str = ""
|
||||
if access_elem:
|
||||
# Usar la función auxiliar para reconstruir el texto del operando
|
||||
access_text = get_access_text_stl(access_elem[0])
|
||||
access_str = access_text
|
||||
# Comentarios asociados al Access (inline) - Tienen Inserted="true"
|
||||
# Buscar DENTRO del Access
|
||||
access_comments = access_elem[0].xpath(
|
||||
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
|
||||
namespaces=ns,
|
||||
)
|
||||
for acc_comm in access_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(acc_comm).strip()}"
|
||||
|
||||
if access_str:
|
||||
line_parts.append(access_str) # Añadir operando (si existe)
|
||||
|
||||
# Construir línea final si hay partes (etiqueta, instrucción u operando)
|
||||
if line_parts:
|
||||
# Unir partes con tabulación si hay más de una (etiqueta+instrucción o instrucción+operando)
|
||||
# Ajustar espacios/tabulaciones para legibilidad
|
||||
if len(line_parts) > 1:
|
||||
# Caso Etiqueta + Instrucción + (Operando opcional)
|
||||
if label_str and instruction_str:
|
||||
current_line = f"{line_parts[0]:<8}\t{line_parts[1]}" # Etiqueta alineada, tab, instrucción
|
||||
if access_str:
|
||||
current_line += f"\t{line_parts[2]}" # Tab, operando
|
||||
# Caso Instrucción + Operando (sin etiqueta)
|
||||
elif instruction_str and access_str:
|
||||
current_line = f"\t{line_parts[0]}\t{line_parts[1]}" # Tab, instrucción, tab, operando
|
||||
# Caso solo Instrucción (sin etiqueta ni operando)
|
||||
elif instruction_str:
|
||||
current_line = f"\t{line_parts[0]}" # Tab, instrucción
|
||||
else: # Otros casos (solo etiqueta, solo operando? improbable)
|
||||
current_line = "\t".join(line_parts)
|
||||
else: # Solo una parte (instrucción sin operando o solo etiqueta?)
|
||||
current_line = line_parts[0] if label_str else f"\t{line_parts[0]}"
|
||||
|
||||
# Añadir comentario inline al final si existe, con tabulación
|
||||
if inline_comment:
|
||||
current_line += f"\t{inline_comment.strip()}"
|
||||
|
||||
# Añadir la línea construida si no está vacía
|
||||
if current_line.strip():
|
||||
stl_lines.append(current_line.rstrip()) # Quitar espacios finales
|
||||
|
||||
# Añadir BE al final si es necesario (lógica específica del bloque, no generalizable aquí)
|
||||
# stl_lines.append("BE") # Ejemplo - QUITAR O ADAPTAR
|
||||
|
||||
return "\n".join(stl_lines)
|
||||
|
||||
|
||||
# --- Función Principal del Parser STL (Corregida v4) ---
|
||||
|
||||
|
||||
def parse_stl_network(network_element):
|
||||
"""
|
||||
Parsea una red STL extrayendo el código fuente reconstruido. (v4)
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSTL_ID")
|
||||
network_lang = "STL"
|
||||
reconstructed_stl = "// STL extraction failed: Reason unknown.\n" # Default error
|
||||
parsing_error_msg = None
|
||||
network_title = f"Network {network_id}" # Default title
|
||||
network_comment = "" # Default comment
|
||||
|
||||
try:
|
||||
# Buscar NetworkSource usando local-name()
|
||||
network_source_node_list = network_element.xpath(
|
||||
".//*[local-name()='NetworkSource']"
|
||||
)
|
||||
|
||||
statement_list_node = None
|
||||
if network_source_node_list:
|
||||
network_source_node = network_source_node_list[0]
|
||||
# Buscar StatementList dentro del NetworkSource encontrado, usando local-name()
|
||||
statement_list_node_list = network_source_node.xpath(
|
||||
".//*[local-name()='StatementList']"
|
||||
)
|
||||
if statement_list_node_list:
|
||||
statement_list_node = statement_list_node_list[0]
|
||||
else:
|
||||
parsing_error_msg = "StatementList node not found inside NetworkSource."
|
||||
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
|
||||
else:
|
||||
parsing_error_msg = "NetworkSource node not found using local-name()."
|
||||
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
|
||||
|
||||
# Intentar reconstruir SOLO si encontramos el nodo StatementList
|
||||
if statement_list_node is not None:
|
||||
# La función reconstruct_stl_from_statementlist debe estar definida arriba
|
||||
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node)
|
||||
elif parsing_error_msg:
|
||||
reconstructed_stl = f"// STL extraction failed: {parsing_error_msg}\n"
|
||||
|
||||
except Exception as e_parse:
|
||||
parsing_error_msg = f"Exception during STL network parsing: {e_parse}"
|
||||
print(f" ERROR parseando Red {network_id} (STL): {parsing_error_msg}")
|
||||
traceback.print_exc()
|
||||
reconstructed_stl = f"// ERROR durante el parseo de STL: {e_parse}\n"
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"logic": [
|
||||
{
|
||||
"instruction_uid": f"STL_{network_id}",
|
||||
"type": "RAW_STL_CHUNK",
|
||||
"stl": reconstructed_stl,
|
||||
}
|
||||
],
|
||||
}
|
||||
if parsing_error_msg:
|
||||
parsed_network_data["error"] = f"Parser failed: {parsing_error_msg}"
|
||||
|
||||
return parsed_network_data
|
||||
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {"language": ["STL"], "parser_func": parse_stl_network}
|
|
@ -0,0 +1,467 @@
|
|||
# ToUpload/parsers/parser_utils.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import traceback
|
||||
|
||||
# Definición de 'ns' (asegúrate de que esté definida correctamente en tu archivo)
|
||||
ns = {
|
||||
"iface": "http://www.siemens.com/automation/Openness/SW/Interface/v5",
|
||||
"flg": "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4",
|
||||
"st": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StructuredText/v3",
|
||||
"stl": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StatementList/v4",
|
||||
# Añade otros namespaces si son necesarios
|
||||
}
|
||||
|
||||
|
||||
# --- Funciones Comunes de Extracción de Texto y Nodos ---
|
||||
def get_multilingual_text(element, default_lang="en-US-it-IT", fallback_lang=None):
|
||||
"""
|
||||
Extrae texto multilingüe de un elemento XML. (v5.2 - DEBUG + XPath ObjectList)
|
||||
"""
|
||||
# print(f"--- DEBUG get_multilingual_text v5.2: Iniciando para elemento {element.tag if element is not None else 'None'}, default='{default_lang}' ---")
|
||||
if element is None: return ""
|
||||
|
||||
combined_texts = []
|
||||
languages_to_try = []
|
||||
|
||||
# --- Lógica Combinada ---
|
||||
is_combined_mode = default_lang and '-' in default_lang and len(default_lang.split('-')) >= 2
|
||||
if is_combined_mode:
|
||||
# print(f"--- DEBUG v5.2: Detectado modo combinado: '{default_lang}' ---")
|
||||
parts = default_lang.split('-')
|
||||
target_langs = []
|
||||
if len(parts) % 2 == 0:
|
||||
for i in range(0, len(parts), 2): target_langs.append(f"{parts[i]}-{parts[i+1]}")
|
||||
else: target_langs = []
|
||||
|
||||
if target_langs:
|
||||
# print(f"--- DEBUG v5.2: Culturas combinadas a buscar: {target_langs} ---")
|
||||
try:
|
||||
for lang in target_langs:
|
||||
# --- CORRECCIÓN XPath v5.2: Añadir ObjectList ---
|
||||
xpath_find_item = f"./ObjectList/MultilingualTextItem[AttributeList/Culture='{lang}']"
|
||||
found_items = element.xpath(xpath_find_item, namespaces=ns)
|
||||
# print(f" DEBUG Combinado v5.2: Items encontrados para '{lang}': {len(found_items)}")
|
||||
|
||||
if found_items:
|
||||
xpath_get_text = "./AttributeList/Text/text()"
|
||||
text_nodes = found_items[0].xpath(xpath_get_text, namespaces=ns)
|
||||
# print(f" DEBUG Combinado v5.2: Nodos de texto encontrados: {len(text_nodes)}")
|
||||
if text_nodes:
|
||||
text_content = text_nodes[0].strip()
|
||||
# print(f" DEBUG Combinado v5.2: Texto encontrado para '{lang}': '{text_content[:50]}...'")
|
||||
if text_content: combined_texts.append(text_content)
|
||||
# --- FIN CORRECCIÓN XPath v5.2 ---
|
||||
if combined_texts:
|
||||
# print(f"--- DEBUG v5.2: Modo combinado retornando: '{' - '.join(combined_texts)}' ---")
|
||||
return " - ".join(combined_texts)
|
||||
else:
|
||||
# print(f"--- DEBUG v5.2: Modo combinado no encontró textos. Intentando fallback... ---")
|
||||
default_lang = None
|
||||
except Exception as e_comb:
|
||||
print(f" Advertencia: Error procesando modo combinado '{default_lang}': {e_comb}")
|
||||
default_lang = None
|
||||
else: default_lang = None
|
||||
# --- Fin Lógica Combinada ---
|
||||
|
||||
# --- Lógica Normal / Fallback ---
|
||||
# print("--- DEBUG v5.2: Iniciando lógica Normal/Fallback ---")
|
||||
if default_lang: languages_to_try.append(default_lang)
|
||||
if fallback_lang: languages_to_try.append(fallback_lang)
|
||||
# print(f" DEBUG v5.2: Idiomas específicos a probar: {languages_to_try}")
|
||||
|
||||
try:
|
||||
if languages_to_try:
|
||||
for lang in languages_to_try:
|
||||
# --- CORRECCIÓN XPath v5.2: Añadir ObjectList ---
|
||||
xpath_find_item = f"./ObjectList/MultilingualTextItem[AttributeList/Culture='{lang}']"
|
||||
found_items = element.xpath(xpath_find_item, namespaces=ns)
|
||||
# print(f" DEBUG Fallback v5.2: Items encontrados para '{lang}': {len(found_items)}")
|
||||
if found_items:
|
||||
xpath_get_text = "./AttributeList/Text/text()"
|
||||
text_nodes = found_items[0].xpath(xpath_get_text, namespaces=ns)
|
||||
# print(f" DEBUG Fallback v5.2: Nodos de texto encontrados: {len(text_nodes)}")
|
||||
if text_nodes:
|
||||
text_content = text_nodes[0].strip()
|
||||
# print(f" DEBUG Fallback v5.2: Texto encontrado para '{lang}': '{text_content[:50]}...'")
|
||||
if text_content:
|
||||
# print(f"--- DEBUG v5.2: Fallback retornando texto de '{lang}' ---")
|
||||
return text_content
|
||||
# --- FIN CORRECCIÓN XPath v5.2 ---
|
||||
|
||||
# Fallback final: buscar cualquier texto no vacío
|
||||
# print(" DEBUG v5.2: Probando cualquier idioma con texto no vacío...")
|
||||
xpath_any_text = "./ObjectList/MultilingualTextItem/AttributeList/Text/text()" # .// busca en cualquier nivel
|
||||
all_text_nodes = element.xpath(xpath_any_text, namespaces=ns)
|
||||
# print(f" DEBUG Fallback Any v5.2: Nodos de texto encontrados: {len(all_text_nodes)}")
|
||||
for text_content_raw in all_text_nodes:
|
||||
text_content = text_content_raw.strip()
|
||||
if text_content:
|
||||
# print(f"--- DEBUG v5.2: Fallback 'Any' retornando texto: '{text_content}' ---")
|
||||
return text_content
|
||||
|
||||
# print("--- DEBUG v5.2: No se encontró ningún texto no vacío. Retornando '' ---")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
# print(f"--- DEBUG v5.2: EXCEPCIÓN en lógica Normal/Fallback: {e} ---")
|
||||
# traceback.print_exc()
|
||||
return ""
|
||||
|
||||
|
||||
def get_symbol_name(symbol_element):
|
||||
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
||||
if symbol_element is None:
|
||||
return None
|
||||
try:
|
||||
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
||||
return (
|
||||
".".join(
|
||||
f'"{c}"' if not c.startswith("#") and '"' not in c else c
|
||||
for c in components
|
||||
)
|
||||
if components
|
||||
else None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Excepción en get_symbol_name: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_access(access_element):
|
||||
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
|
||||
if access_element is None:
|
||||
return None
|
||||
uid = access_element.get("UId")
|
||||
scope = access_element.get("Scope")
|
||||
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
||||
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
||||
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
||||
|
||||
if symbol:
|
||||
info["type"] = "variable"
|
||||
info["name"] = get_symbol_name(symbol[0])
|
||||
if info["name"] is None:
|
||||
info["type"] = "error_parsing_symbol"
|
||||
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
|
||||
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
|
||||
info["name"] = (
|
||||
f'"_ERR_PARSING_{raw_text[:20]}"'
|
||||
if raw_text
|
||||
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
|
||||
)
|
||||
elif constant:
|
||||
info["type"] = "constant"
|
||||
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
||||
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
||||
info["datatype"] = (
|
||||
const_type_elem[0].text.strip()
|
||||
if const_type_elem and const_type_elem[0].text is not None
|
||||
else "Unknown"
|
||||
)
|
||||
value_str = (
|
||||
const_val_elem[0].text.strip()
|
||||
if const_val_elem and const_val_elem[0].text is not None
|
||||
else None
|
||||
)
|
||||
if value_str is None:
|
||||
info["type"] = "error_parsing_constant"
|
||||
info["value"] = None
|
||||
print(f"Error: Constante sin valor Access UID={uid}")
|
||||
if info["datatype"] == "Unknown" and value_str:
|
||||
val_lower = value_str.lower()
|
||||
if val_lower in ["true", "false"]:
|
||||
info["datatype"] = "Bool"
|
||||
elif value_str.isdigit() or (
|
||||
value_str.startswith("-") and value_str[1:].isdigit()
|
||||
):
|
||||
info["datatype"] = "Int"
|
||||
elif "." in value_str:
|
||||
try:
|
||||
float(value_str)
|
||||
info["datatype"] = "Real"
|
||||
except ValueError:
|
||||
pass
|
||||
elif "#" in value_str:
|
||||
parts = value_str.split("#", 1)
|
||||
prefix = parts[0].upper()
|
||||
if prefix == "T":
|
||||
info["datatype"] = "Time"
|
||||
elif prefix == "LT":
|
||||
info["datatype"] = "LTime"
|
||||
elif prefix == "S5T":
|
||||
info["datatype"] = "S5Time"
|
||||
elif prefix == "D":
|
||||
info["datatype"] = "Date"
|
||||
elif prefix == "DT":
|
||||
info["datatype"] = "DT"
|
||||
elif prefix == "DTL":
|
||||
info["datatype"] = "DTL"
|
||||
elif prefix == "TOD":
|
||||
info["datatype"] = "Time_Of_Day"
|
||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||
info["datatype"] = "String"
|
||||
else:
|
||||
info["datatype"] = "TypedConstant"
|
||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||
info["datatype"] = "String"
|
||||
info["value"] = value_str
|
||||
dtype_lower = info["datatype"].lower()
|
||||
val_str_processed = value_str
|
||||
if isinstance(value_str, str):
|
||||
if "#" in value_str:
|
||||
val_str_processed = value_str.split("#", 1)[-1]
|
||||
if (
|
||||
val_str_processed.startswith("'")
|
||||
and val_str_processed.endswith("'")
|
||||
and len(val_str_processed) > 1
|
||||
):
|
||||
val_str_processed = val_str_processed[1:-1]
|
||||
try:
|
||||
if dtype_lower in [
|
||||
"int",
|
||||
"dint",
|
||||
"udint",
|
||||
"sint",
|
||||
"usint",
|
||||
"lint",
|
||||
"ulint",
|
||||
"word",
|
||||
"dword",
|
||||
"lword",
|
||||
"byte",
|
||||
]:
|
||||
info["value"] = int(val_str_processed)
|
||||
elif dtype_lower == "bool":
|
||||
info["value"] = (
|
||||
val_str_processed.lower() == "true" or val_str_processed == "1"
|
||||
)
|
||||
elif dtype_lower in ["real", "lreal"]:
|
||||
info["value"] = float(val_str_processed)
|
||||
except (ValueError, TypeError):
|
||||
info["value"] = value_str
|
||||
else:
|
||||
info["type"] = "unknown_structure"
|
||||
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
||||
if info["type"] == "variable" and info.get("name") is None:
|
||||
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
|
||||
info["type"] = "error_no_name"
|
||||
return info
|
||||
|
||||
|
||||
def parse_part(part_element):
|
||||
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
||||
if part_element is None:
|
||||
return None
|
||||
uid = part_element.get("UId")
|
||||
name = part_element.get("Name")
|
||||
if not uid or not name:
|
||||
print(
|
||||
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
||||
)
|
||||
return None
|
||||
template_values = {}
|
||||
negated_pins = {}
|
||||
try:
|
||||
for tv in part_element.xpath("./TemplateValue"):
|
||||
tv_name = tv.get("Name")
|
||||
tv_type = tv.get("Type")
|
||||
if tv_name and tv_type:
|
||||
template_values[tv_name] = tv_type
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
|
||||
try:
|
||||
for negated_elem in part_element.xpath("./Negated"):
|
||||
negated_pin_name = negated_elem.get("Name")
|
||||
if negated_pin_name:
|
||||
negated_pins[negated_pin_name] = True
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Error extrayendo Negated Pins Part UID={uid}: {e}")
|
||||
return {
|
||||
"uid": uid,
|
||||
"type": name,
|
||||
"template_values": template_values,
|
||||
"negated_pins": negated_pins,
|
||||
}
|
||||
|
||||
|
||||
def parse_call(call_element):
|
||||
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
||||
if call_element is None:
|
||||
return None
|
||||
uid = call_element.get("UId")
|
||||
if not uid:
|
||||
print(
|
||||
f"Error: Call encontrado sin UID: {etree.tostring(call_element, encoding='unicode')}"
|
||||
)
|
||||
return None
|
||||
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
|
||||
if not call_info_elem:
|
||||
call_info_elem_no_ns = call_element.xpath("./CallInfo")
|
||||
if not call_info_elem_no_ns:
|
||||
print(f"Error: Call UID {uid} sin elemento CallInfo.")
|
||||
return {"uid": uid, "type": "Call_error", "error": "Missing CallInfo"}
|
||||
else:
|
||||
print(f"Advertencia: Call UID {uid} encontró CallInfo SIN namespace.")
|
||||
call_info = call_info_elem_no_ns[0]
|
||||
else:
|
||||
call_info = call_info_elem[0]
|
||||
block_name = call_info.get("Name")
|
||||
block_type = call_info.get("BlockType")
|
||||
if not block_name or not block_type:
|
||||
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
|
||||
return {
|
||||
"uid": uid,
|
||||
"type": "Call_error",
|
||||
"error": "Missing Name or BlockType in CallInfo",
|
||||
}
|
||||
instance_name, instance_scope = None, None
|
||||
if block_type == "FB":
|
||||
instance_elem_list = call_info.xpath("./flg:Instance", namespaces=ns)
|
||||
if instance_elem_list:
|
||||
instance_elem = instance_elem_list[0]
|
||||
instance_scope = instance_elem.get("Scope")
|
||||
component_elem_list = instance_elem.xpath("./flg:Component", namespaces=ns)
|
||||
if component_elem_list:
|
||||
component_elem = component_elem_list[0]
|
||||
db_name_raw = component_elem.get("Name")
|
||||
if db_name_raw:
|
||||
instance_name = (
|
||||
f'"{db_name_raw}"'
|
||||
if not db_name_raw.startswith('"')
|
||||
else db_name_raw
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: <flg:Component> en <flg:Instance> FB Call UID {uid} sin 'Name'."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: No se encontró <flg:Component> en <flg:Instance> FB Call UID {uid}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
|
||||
)
|
||||
call_scope = call_element.get("Scope")
|
||||
if call_scope == "LocalVariable":
|
||||
instance_name = f'"{block_name}"'
|
||||
instance_scope = "Static"
|
||||
print(
|
||||
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
||||
)
|
||||
call_data = {
|
||||
"uid": uid,
|
||||
"type": "Call",
|
||||
"block_name": block_name,
|
||||
"block_type": block_type,
|
||||
}
|
||||
if instance_name:
|
||||
call_data["instance_db"] = instance_name
|
||||
if instance_scope:
|
||||
call_data["instance_scope"] = instance_scope
|
||||
return call_data
|
||||
|
||||
|
||||
def parse_interface_members(member_elements):
|
||||
"""Parsea recursivamente miembros de interfaz/estructura, incluyendo InstanceOfName."""
|
||||
members_data = []
|
||||
if not member_elements:
|
||||
return members_data
|
||||
for member in member_elements:
|
||||
member_name = member.get("Name")
|
||||
member_dtype_raw = member.get("Datatype")
|
||||
member_version = member.get("Version")
|
||||
member_remanence = member.get("Remanence", "NonRetain")
|
||||
member_accessibility = member.get("Accessibility", "Public")
|
||||
# <-- NUEVO: Obtener InstanceOfName -->
|
||||
member_instance_of = member.get("InstanceOfName") # Puede ser None
|
||||
# <-- FIN NUEVO -->
|
||||
|
||||
if not member_name or not member_dtype_raw:
|
||||
print("Advertencia: Miembro sin nombre o tipo de dato. Saltando.")
|
||||
continue
|
||||
|
||||
# Combinar tipo y versión si existe versión
|
||||
member_dtype = (
|
||||
f"{member_dtype_raw}:v{member_version}"
|
||||
if member_version
|
||||
else member_dtype_raw
|
||||
)
|
||||
|
||||
member_info = {
|
||||
"name": member_name,
|
||||
"datatype": member_dtype,
|
||||
"remanence": member_remanence,
|
||||
"accessibility": member_accessibility,
|
||||
"start_value": None,
|
||||
"comment": None,
|
||||
"children": [],
|
||||
"array_elements": {},
|
||||
}
|
||||
# <-- NUEVO: Añadir instance_of_name si existe -->
|
||||
if member_instance_of:
|
||||
member_info["instance_of_name"] = member_instance_of
|
||||
# <-- FIN NUEVO -->
|
||||
|
||||
# Extraer comentario
|
||||
comment_node = member.xpath("./iface:Comment", namespaces=ns)
|
||||
if comment_node:
|
||||
member_info["comment"] = get_multilingual_text(comment_node[0])
|
||||
|
||||
# Extraer valor inicial
|
||||
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
|
||||
if start_value_node:
|
||||
constant_name = start_value_node[0].get("ConstantName")
|
||||
member_info["start_value"] = (
|
||||
constant_name
|
||||
if constant_name
|
||||
else (
|
||||
start_value_node[0].text
|
||||
if start_value_node[0].text is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
# Procesar miembros anidados (Struct)
|
||||
nested_sections = member.xpath(
|
||||
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
||||
)
|
||||
if nested_sections:
|
||||
member_info["children"] = parse_interface_members(nested_sections)
|
||||
|
||||
# Procesar valores iniciales de Array
|
||||
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
|
||||
subelements = member.xpath("./iface:Subelement", namespaces=ns)
|
||||
for sub in subelements:
|
||||
path = sub.get("Path") # Índice del array, ej "0", "1"
|
||||
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
|
||||
if path and sub_start_value_node:
|
||||
constant_name = sub_start_value_node[0].get("ConstantName")
|
||||
value = (
|
||||
constant_name
|
||||
if constant_name
|
||||
else (
|
||||
sub_start_value_node[0].text
|
||||
if sub_start_value_node[0].text is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
# Guardar valor y comentario del subelemento
|
||||
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
|
||||
sub_comment_text = (
|
||||
get_multilingual_text(sub_comment_node[0])
|
||||
if sub_comment_node
|
||||
else None
|
||||
)
|
||||
if sub_comment_text:
|
||||
member_info["array_elements"][path] = {
|
||||
"value": value,
|
||||
"comment": sub_comment_text,
|
||||
}
|
||||
else:
|
||||
member_info["array_elements"][path] = {
|
||||
"value": value
|
||||
} # Guardar como dict simple si no hay comment
|
||||
members_data.append(member_info)
|
||||
return members_data
|
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.
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.
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,87 @@
|
|||
# processors/process_add.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
import re # Importar re si se usa para formateo
|
||||
# Usar las nuevas utilidades
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, get_target_scl_name, format_variable_name
|
||||
from .symbol_manager import SymbolManager
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed" # Usar el nuevo sufijo
|
||||
|
||||
def process_add(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
"""Genera SCL para Add, simplificando la condición EN."""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "Add")
|
||||
current_type = instruction.get("type","")
|
||||
if current_type.endswith(SCL_SUFFIX) or "_error" in current_type:
|
||||
return False
|
||||
|
||||
# Obtener EN (SymPy), IN1, IN2 (SymPy o Constante/String)
|
||||
en_input = instruction["inputs"].get("en")
|
||||
in1_info = instruction["inputs"].get("in1")
|
||||
in2_info = instruction["inputs"].get("in2")
|
||||
sympy_en_expr = get_sympy_representation(en_input, network_id, sympy_map, symbol_manager) if en_input else sympy.true
|
||||
op1_sympy_or_const = get_sympy_representation(in1_info, network_id, sympy_map, symbol_manager)
|
||||
op2_sympy_or_const = get_sympy_representation(in2_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
# Obtener destino SCL
|
||||
target_scl_name = get_target_scl_name(instruction, "out", network_id, default_to_temp=True)
|
||||
|
||||
# Verificar dependencias
|
||||
if sympy_en_expr is None or op1_sympy_or_const is None or op2_sympy_or_const is None or target_scl_name is None:
|
||||
# print(f"DEBUG Add {instr_uid}: Dependency not ready")
|
||||
return False
|
||||
|
||||
# Convertir operandos SymPy/Constante a SCL strings
|
||||
op1_scl = sympy_expr_to_scl(op1_sympy_or_const, symbol_manager)
|
||||
op2_scl = sympy_expr_to_scl(op2_sympy_or_const, symbol_manager)
|
||||
|
||||
# Añadir paréntesis si contienen operadores (más seguro para SCL)
|
||||
op1_scl_formatted = f"({op1_scl})" if re.search(r'[+\-*/ ]', op1_scl) else op1_scl
|
||||
op2_scl_formatted = f"({op2_scl})" if re.search(r'[+\-*/ ]', op2_scl) else op2_scl
|
||||
|
||||
# Generar SCL Core
|
||||
scl_core = f"{target_scl_name} := {op1_scl_formatted} + {op2_scl_formatted};"
|
||||
|
||||
# Aplicar Condición EN (Simplificando EN)
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for {instr_type_original} {instr_uid}: {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
# Evitar IF TRUE THEN...
|
||||
if en_condition_scl == "TRUE":
|
||||
scl_final = scl_core
|
||||
# Evitar IF FALSE THEN...
|
||||
elif en_condition_scl == "FALSE":
|
||||
scl_final = f"// {instr_type_original} {instr_uid} condition simplified to FALSE."
|
||||
else:
|
||||
indented_core = "\n".join([f" {line}" for line in scl_core.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_core}\nEND_IF;"
|
||||
else:
|
||||
scl_final = scl_core
|
||||
|
||||
# Actualizar instrucción y mapa
|
||||
instruction["scl"] = scl_final # SCL final generado
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
|
||||
# Propagar valor de salida (nombre SCL del destino) y ENO (expresión SymPy)
|
||||
map_key_out = (network_id, instr_uid, "out")
|
||||
sympy_map[map_key_out] = target_scl_name # Guardar nombre del destino (string)
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||
|
||||
return True
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para el procesador Add."""
|
||||
# Asegurar que la clave coincida con el tipo en JSON ('add')
|
||||
return {'type_name': 'add', 'processor_func': process_add, 'priority': 4}
|
|
@ -0,0 +1,118 @@
|
|||
# processors/process_blkmov.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
import re
|
||||
# Usar las nuevas utilidades
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, get_target_scl_name, format_variable_name
|
||||
from .symbol_manager import SymbolManager, extract_plc_variable_name
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed" # Usar el nuevo sufijo
|
||||
|
||||
def process_blkmov(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
"""
|
||||
Genera SCL usando BLKMOV directamente como nombre de función,
|
||||
simplificando la condición EN.
|
||||
ADVERTENCIA: Sintaxis BLKMOV probablemente no compile en TIA estándar.
|
||||
"""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "BlkMov") # Asegurar que el tipo base sea correcto
|
||||
current_type = instruction.get("type","")
|
||||
if current_type.endswith(SCL_SUFFIX) or "_error" in current_type:
|
||||
return False
|
||||
|
||||
# --- Obtener Entradas ---
|
||||
en_input = instruction["inputs"].get("en")
|
||||
# Obtener EN como expresión SymPy
|
||||
sympy_en_expr = get_sympy_representation(en_input, network_id, sympy_map, symbol_manager) if en_input else sympy.true
|
||||
|
||||
srcblk_info = instruction["inputs"].get("SRCBLK")
|
||||
# Obtener nombre RAW de SRCBLK (como se hacía antes, si es necesario para BLKMOV)
|
||||
# Este nombre NO pasa por SymPy, se usa directo en el string SCL final
|
||||
raw_srcblk_name = srcblk_info.get("name") if srcblk_info else None
|
||||
|
||||
# Verificar dependencias (EN debe estar resuelto, SRCBLK debe tener nombre)
|
||||
if sympy_en_expr is None:
|
||||
# print(f"DEBUG BlkMov {instr_uid}: EN dependency not ready")
|
||||
return False
|
||||
if raw_srcblk_name is None:
|
||||
print(f"Error: BLKMOV {instr_uid} sin información válida para SRCBLK.")
|
||||
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} sin SRCBLK válido."
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# --- Obtener Destinos (Salidas) ---
|
||||
# RET_VAL (Obtener nombre SCL formateado)
|
||||
retval_target_scl = get_target_scl_name(instruction, "RET_VAL", network_id, default_to_temp=True)
|
||||
if retval_target_scl is None: # get_target_scl_name ya imprime error si falla y default_to_temp=True
|
||||
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} no pudo generar destino RET_VAL"
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# DSTBLK (Obtener nombre RAW como antes, si se necesita)
|
||||
raw_dstblk_name = None
|
||||
dstblk_output_list = instruction.get("outputs", {}).get("DSTBLK", [])
|
||||
if dstblk_output_list and isinstance(dstblk_output_list, list) and len(dstblk_output_list) == 1:
|
||||
dest_access = dstblk_output_list[0]
|
||||
if dest_access.get("type") == "variable":
|
||||
raw_dstblk_name = dest_access.get("name")
|
||||
# Manejar error si no se encuentra DSTBLK
|
||||
if raw_dstblk_name is None:
|
||||
print(f"Error: No se encontró un destino único y válido para DSTBLK en BLKMOV {instr_uid}.")
|
||||
instruction["scl"] = f"// ERROR: BLKMOV {instr_uid} sin destino DSTBLK válido."
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# --- Formateo especial (mantener nombres raw si es necesario para BLKMOV) ---
|
||||
# Estos nombres van directo al string SCL, no necesitan pasar por SymPy
|
||||
srcblk_final_str = raw_srcblk_name # Asumiendo que ya viene con comillas si las necesita
|
||||
dstblk_final_str = raw_dstblk_name # Asumiendo que ya viene con comillas si las necesita
|
||||
|
||||
# --- Generar SCL Core (Usando la sintaxis no estándar BLKMOV) ---
|
||||
scl_core = (
|
||||
f"{retval_target_scl} := BLKMOV(SRCBLK := {srcblk_final_str}, "
|
||||
f"DSTBLK => {dstblk_final_str}); " # Usar => para Out/InOut
|
||||
f"// ADVERTENCIA: BLKMOV usado directamente, probablemente no compile!"
|
||||
)
|
||||
|
||||
# --- Aplicar Condición EN (Simplificando EN) ---
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for {instr_type_original} {instr_uid}: {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
# Evitar IF TRUE/FALSE THEN...
|
||||
if en_condition_scl == "TRUE":
|
||||
scl_final = scl_core
|
||||
elif en_condition_scl == "FALSE":
|
||||
scl_final = f"// {instr_type_original} {instr_uid} condition simplified to FALSE."
|
||||
else:
|
||||
indented_core = "\n".join([f" {line}" for line in scl_core.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_core}\nEND_IF;"
|
||||
else:
|
||||
scl_final = scl_core
|
||||
|
||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
||||
instruction["scl"] = scl_final # SCL final generado
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
|
||||
# Propagar ENO (expresión SymPy)
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr
|
||||
|
||||
# Propagar el valor de retorno (nombre SCL string del destino de RET_VAL)
|
||||
map_key_ret_val = (network_id, instr_uid, "RET_VAL")
|
||||
sympy_map[map_key_ret_val] = retval_target_scl
|
||||
|
||||
return True
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para el procesador BLKMOV."""
|
||||
# Asegurarse que el type_name coincida con el JSON ('blkmov' parece probable)
|
||||
return {'type_name': 'blkmov', 'processor_func': process_blkmov, 'priority': 6}
|
|
@ -0,0 +1,131 @@
|
|||
# processors/process_call.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
# Asumiendo que estas funciones ahora existen y están adaptadas
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, format_variable_name, get_target_scl_name
|
||||
from .symbol_manager import SymbolManager # Necesitamos pasar el symbol_manager
|
||||
|
||||
# Definir sufijo globalmente o importar
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "") # Tipo antes de añadir sufijo
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
|
||||
block_type = instruction.get("block_type") # FC, FB
|
||||
instance_db = instruction.get("instance_db") # Nombre del DB de instancia (para FB)
|
||||
|
||||
# Formatear nombres SCL (para la llamada final)
|
||||
block_name_scl = format_variable_name(block_name)
|
||||
instance_db_scl = format_variable_name(instance_db) if instance_db else None
|
||||
|
||||
# --- Manejo de EN ---
|
||||
en_input = instruction["inputs"].get("en")
|
||||
sympy_en_expr = get_sympy_representation(en_input, network_id, sympy_map, symbol_manager) if en_input else sympy.true
|
||||
|
||||
if sympy_en_expr is None:
|
||||
# print(f"DEBUG Call {instr_uid}: EN dependency not ready.")
|
||||
return False # Dependencia EN no resuelta
|
||||
|
||||
# --- Procesar Parámetros de Entrada ---
|
||||
scl_call_params = []
|
||||
processed_inputs = {"en"}
|
||||
dependencies_resolved = True
|
||||
|
||||
# Ordenar para consistencia
|
||||
input_pin_names = sorted(instruction.get("inputs", {}).keys())
|
||||
|
||||
for pin_name in input_pin_names:
|
||||
if pin_name not in processed_inputs:
|
||||
source_info = instruction["inputs"][pin_name]
|
||||
# Obtener la representación de la fuente (puede ser SymPy o Constante/String)
|
||||
source_sympy_or_const = get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
if source_sympy_or_const is None:
|
||||
# print(f"DEBUG Call {instr_uid}: Input param '{pin_name}' dependency not ready.")
|
||||
dependencies_resolved = False
|
||||
break # Salir si una dependencia no está lista
|
||||
|
||||
# Convertir la expresión/constante a SCL para la llamada
|
||||
# Simplificar ANTES de convertir? Probablemente no necesario para parámetros de entrada
|
||||
# a menos que queramos optimizar el valor pasado. Por ahora, convertir directo.
|
||||
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
|
||||
|
||||
# El nombre del pin SÍ necesita formateo
|
||||
pin_name_scl = format_variable_name(pin_name)
|
||||
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
|
||||
processed_inputs.add(pin_name)
|
||||
|
||||
if not dependencies_resolved:
|
||||
return False
|
||||
|
||||
# --- Construcción de la Llamada SCL (similar a antes) ---
|
||||
scl_call_body = ""
|
||||
param_string = ", ".join(scl_call_params)
|
||||
|
||||
if block_type == "FB":
|
||||
if not instance_db_scl:
|
||||
print(f"Error: Call FB '{block_name_scl}' (UID {instr_uid}) sin instancia.")
|
||||
instruction["scl"] = f"// ERROR: FB Call {block_name_scl} sin instancia"
|
||||
instruction["type"] = f"Call_FB_error"
|
||||
return True
|
||||
scl_call_body = f"{instance_db_scl}({param_string});"
|
||||
elif block_type == "FC":
|
||||
scl_call_body = f"{block_name_scl}({param_string});"
|
||||
else:
|
||||
print(f"Advertencia: Tipo de bloque no soportado para Call UID {instr_uid}: {block_type}")
|
||||
scl_call_body = f"// ERROR: Call a bloque tipo '{block_type}' no soportado: {block_name_scl}"
|
||||
instruction["type"] = f"Call_{block_type}_error" # Marcar como error
|
||||
|
||||
# --- Aplicar Condición EN (usando la expresión SymPy EN) ---
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
# Simplificar la condición EN ANTES de convertirla a SCL
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for Call {instr_uid}: {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
indented_call = "\n".join([f" {line}" for line in scl_call_body.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_call}\nEND_IF;"
|
||||
else:
|
||||
scl_final = scl_call_body
|
||||
|
||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
||||
instruction["scl"] = scl_final # Guardar el SCL final generado
|
||||
instruction["type"] = (f"Call_{block_type}{SCL_SUFFIX}" if "_error" not in instruction["type"] else instruction["type"])
|
||||
|
||||
# Actualizar sympy_map con el estado ENO (es la expresión SymPy de EN)
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||
|
||||
# Propagar valores de salida (requiere info de interfaz o heurística)
|
||||
# Si se sabe que hay una salida 'MyOutput', se podría añadir su SCL al mapa
|
||||
# Ejemplo MUY simplificado:
|
||||
# for pin_name, dest_list in instruction.get("outputs", {}).items():
|
||||
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
||||
# map_key_out = (network_id, instr_uid, pin_name)
|
||||
# if block_type == "FB" and instance_db_scl:
|
||||
# sympy_map[map_key_out] = f"{instance_db_scl}.{format_variable_name(pin_name)}" # Guardar el *string* de acceso SCL
|
||||
# # Para FCs es más complejo, necesitaría asignación explícita a temp
|
||||
# # else: # FC output -> necesita temp var
|
||||
# # temp_var = generate_temp_var_name(...)
|
||||
# # sympy_map[map_key_out] = temp_var
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para las llamadas a FC y FB."""
|
||||
return [
|
||||
{'type_name': 'call_fc', 'processor_func': process_call, 'priority': 6},
|
||||
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6}
|
||||
]
|
|
@ -0,0 +1,82 @@
|
|||
# processors/process_coil.py
|
||||
import sympy
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, get_target_scl_name, format_variable_name
|
||||
from .symbol_manager import SymbolManager, extract_plc_variable_name
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
def process_coil(instruction, network_id, sympy_map, symbol_manager, data):
|
||||
"""Genera la asignación SCL para Coil, simplificando la entrada SymPy."""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "Coil")
|
||||
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
# Get input expression from SymPy map
|
||||
coil_input_info = instruction["inputs"].get("in")
|
||||
sympy_expr_in = get_sympy_representation(coil_input_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
# Get target variable SCL name
|
||||
target_scl_name = get_target_scl_name(instruction, "operand", network_id, default_to_temp=False) # Coil must have explicit target
|
||||
|
||||
# Check dependencies
|
||||
if sympy_expr_in is None:
|
||||
# print(f"DEBUG Coil {instr_uid}: Input dependency not ready.")
|
||||
return False
|
||||
if target_scl_name is None:
|
||||
print(f"Error: Coil {instr_uid} operando no es variable o falta info.")
|
||||
instruction["scl"] = f"// ERROR: Coil {instr_uid} operando no es variable."
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True # Processed with error
|
||||
|
||||
# *** Perform Simplification ***
|
||||
try:
|
||||
#simplified_expr = sympy.simplify_logic(sympy_expr_in, force=False)
|
||||
#simplified_expr = sympy_expr_in
|
||||
simplified_expr = sympy.logic.boolalg.to_dnf(sympy_expr_in, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error during SymPy simplification for Coil {instr_uid}: {e}")
|
||||
simplified_expr = sympy_expr_in # Fallback to original expression
|
||||
|
||||
# *** Convert simplified expression back to SCL string ***
|
||||
condition_scl = sympy_expr_to_scl(simplified_expr, symbol_manager)
|
||||
|
||||
# Generate the final SCL assignment
|
||||
scl_assignment = f"{target_scl_name} := {condition_scl};"
|
||||
scl_final = scl_assignment
|
||||
|
||||
# --- Handle Edge Detector Memory Update (Logic similar to before) ---
|
||||
# Check if input comes from PBox/NBox and append memory update
|
||||
mem_update_scl_combined = None
|
||||
if isinstance(coil_input_info, dict) and coil_input_info.get("type") == "connection":
|
||||
source_uid = coil_input_info.get("source_instruction_uid")
|
||||
source_pin = coil_input_info.get("source_pin")
|
||||
source_instruction = None
|
||||
network_logic = next((net["logic"] for net in data["networks"] if net["id"] == network_id), [])
|
||||
for instr in network_logic:
|
||||
if instr.get("instruction_uid") == source_uid:
|
||||
source_instruction = instr
|
||||
break
|
||||
if source_instruction:
|
||||
# Check for the original type before suffix was added
|
||||
orig_source_type = source_instruction.get("type", "").replace(SCL_SUFFIX, '').replace('_error', '')
|
||||
if orig_source_type in ["PBox", "NBox"] and '_edge_mem_update_scl' in source_instruction:
|
||||
mem_update_scl_combined = source_instruction.get('_edge_mem_update_scl')
|
||||
if mem_update_scl_combined:
|
||||
scl_final = f"{scl_assignment}\n{mem_update_scl_combined}"
|
||||
# Clear the source SCL?
|
||||
source_instruction['scl'] = f"// Edge Logic handled by Coil {instr_uid}"
|
||||
|
||||
|
||||
# Update instruction
|
||||
instruction["scl"] = scl_final
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
# Coil typically doesn't output to scl_map
|
||||
|
||||
return True
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para el procesador Coil."""
|
||||
return {'type_name': 'coil', 'processor_func': process_coil, 'priority': 3}
|
|
@ -0,0 +1,87 @@
|
|||
# processors/process_comparison.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
from .processor_utils import get_sympy_representation, format_variable_name # No necesita sympy_expr_to_scl aquí
|
||||
from .symbol_manager import SymbolManager # Necesita acceso al manager
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
def process_comparison(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
"""
|
||||
Genera la expresión SymPy para Comparadores (GT, LT, GE, LE, NE).
|
||||
El resultado se propaga por sympy_map['out'].
|
||||
"""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "") # GT, LT, GE, LE, NE
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
# Mapa de tipos a funciones/clases SymPy Relational
|
||||
# Nota: Asegúrate de que los tipos coincidan (ej. si son números o booleanos)
|
||||
op_map = {
|
||||
"GT": sympy.Gt, # Greater Than >
|
||||
"LT": sympy.Lt, # Less Than <
|
||||
"GE": sympy.Ge, # Greater or Equal >=
|
||||
"LE": sympy.Le, # Less or Equal <=
|
||||
"NE": sympy.Ne # Not Equal <> (sympy.Ne maneja esto)
|
||||
}
|
||||
sympy_relation_func = op_map.get(instr_type_original.upper())
|
||||
if not sympy_relation_func:
|
||||
instruction["scl"] = f"// ERROR: Tipo de comparación no soportado para SymPy: {instr_type_original}"
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# Obtener operandos como expresiones SymPy o constantes/strings
|
||||
in1_info = instruction["inputs"].get("in1")
|
||||
in2_info = instruction["inputs"].get("in2")
|
||||
op1_sympy = get_sympy_representation(in1_info, network_id, sympy_map, symbol_manager)
|
||||
op2_sympy = get_sympy_representation(in2_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
# Obtener 'pre' (RLO anterior) como expresión SymPy
|
||||
pre_input = instruction["inputs"].get("pre") # Asumiendo que 'pre' es la entrada RLO
|
||||
sympy_pre_rlo = get_sympy_representation(pre_input, network_id, sympy_map, symbol_manager) if pre_input else sympy.true
|
||||
|
||||
# Verificar dependencias
|
||||
if op1_sympy is None or op2_sympy is None or sympy_pre_rlo is None:
|
||||
# print(f"DEBUG Comparison {instr_uid}: Dependency not ready")
|
||||
return False
|
||||
|
||||
# Crear la expresión de comparación SymPy
|
||||
try:
|
||||
# Convertir constantes string a número si es posible (Sympy puede necesitarlo)
|
||||
# Esto es heurístico y puede fallar. Mejor si los tipos son conocidos.
|
||||
op1_eval = sympy.sympify(op1_sympy) if isinstance(op1_sympy, str) else op1_sympy
|
||||
op2_eval = sympy.sympify(op2_sympy) if isinstance(op2_sympy, str) else op2_sympy
|
||||
comparison_expr = sympy_relation_func(op1_eval, op2_eval)
|
||||
except (SyntaxError, TypeError, ValueError) as e:
|
||||
print(f"Error creating SymPy comparison for {instr_uid}: {e}")
|
||||
instruction["scl"] = f"// ERROR creando expr SymPy Comparison {instr_uid}: {e}"
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# Guardar resultado en el mapa para 'out' (es una expresión booleana SymPy)
|
||||
map_key_out = (network_id, instr_uid, "out")
|
||||
sympy_map[map_key_out] = comparison_expr
|
||||
|
||||
# Guardar el RLO de entrada ('pre') como ENO en el mapa SymPy
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_pre_rlo
|
||||
|
||||
# Marcar como procesado, SCL principal es solo comentario
|
||||
instruction["scl"] = f"// SymPy Comparison {instr_type_original}: {comparison_expr}" # Comentario opcional
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
return True
|
||||
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para los comparadores (excepto EQ, que debe ser similar)."""
|
||||
return [
|
||||
{'type_name': 'gt', 'processor_func': process_comparison, 'priority': 2},
|
||||
{'type_name': 'lt', 'processor_func': process_comparison, 'priority': 2},
|
||||
{'type_name': 'ge', 'processor_func': process_comparison, 'priority': 2},
|
||||
{'type_name': 'le', 'processor_func': process_comparison, 'priority': 2},
|
||||
{'type_name': 'ne', 'processor_func': process_comparison, 'priority': 2}
|
||||
# Asegúrate de tener también un procesador para 'eq' usando sympy.Eq
|
||||
]
|
|
@ -0,0 +1,60 @@
|
|||
# processors/process_contact.py
|
||||
import sympy
|
||||
from .processor_utils import get_sympy_representation, format_variable_name # Use new util
|
||||
from .symbol_manager import SymbolManager, extract_plc_variable_name # Need symbol manager access
|
||||
|
||||
# Define SCL_SUFFIX or import if needed globally
|
||||
SCL_SUFFIX = "_sympy_processed" # Indicate processing type
|
||||
|
||||
def process_contact(instruction, network_id, sympy_map, symbol_manager, data): # Pass symbol_manager
|
||||
"""Genera la expresión SymPy para Contact (normal o negado)."""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "Contact")
|
||||
|
||||
# Check if already processed with the new method
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
is_negated = instruction.get("negated_pins", {}).get("operand", False)
|
||||
|
||||
# Get incoming SymPy expression (RLO)
|
||||
in_input = instruction["inputs"].get("in")
|
||||
sympy_expr_in = get_sympy_representation(in_input, network_id, sympy_map, symbol_manager)
|
||||
|
||||
# Get operand SymPy Symbol
|
||||
operand_info = instruction["inputs"].get("operand")
|
||||
operand_plc_name = extract_plc_variable_name(operand_info)
|
||||
sympy_symbol_operand = symbol_manager.get_symbol(operand_plc_name) if operand_plc_name else None
|
||||
|
||||
# Check dependencies
|
||||
if sympy_expr_in is None or sympy_symbol_operand is None:
|
||||
# print(f"DEBUG Contact {instr_uid}: Dependency not ready (In: {sympy_expr_in is not None}, Op: {sympy_symbol_operand is not None})")
|
||||
return False # Dependencies not ready
|
||||
|
||||
# Apply negation using SymPy
|
||||
current_term = sympy.Not(sympy_symbol_operand) if is_negated else sympy_symbol_operand
|
||||
|
||||
# Combine with previous RLO using SymPy
|
||||
# Simplify common cases: TRUE AND X -> X
|
||||
if sympy_expr_in == sympy.true:
|
||||
sympy_expr_out = current_term
|
||||
else:
|
||||
# Could add FALSE AND X -> FALSE optimization here too
|
||||
sympy_expr_out = sympy.And(sympy_expr_in, current_term)
|
||||
|
||||
# Store the resulting SymPy expression object in the map
|
||||
map_key_out = (network_id, instr_uid, "out")
|
||||
sympy_map[map_key_out] = sympy_expr_out
|
||||
|
||||
# Mark instruction as processed (SCL field is now less relevant here)
|
||||
instruction["scl"] = f"// SymPy Contact: {sympy_expr_out}" # Optional debug comment
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX # Use the new suffix
|
||||
# Contact doesn't usually have ENO, it modifies the RLO ('out')
|
||||
|
||||
return True
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para el procesador Contact."""
|
||||
# Ensure 'data' argument is added if needed by the processor function signature change
|
||||
return {'type_name': 'contact', 'processor_func': process_contact, 'priority': 1}
|
|
@ -0,0 +1,90 @@
|
|||
# processors/process_convert.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, get_target_scl_name, format_variable_name
|
||||
from .symbol_manager import SymbolManager
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
def process_convert(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
"""Genera SCL para Convert, tratando la conversión como una asignación."""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "Convert")
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
# Obtener EN y IN
|
||||
en_input = instruction["inputs"].get("en")
|
||||
sympy_en_expr = get_sympy_representation(en_input, network_id, sympy_map, symbol_manager) if en_input else sympy.true
|
||||
in_info = instruction["inputs"].get("in")
|
||||
sympy_or_const_in = get_sympy_representation(in_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
# Obtener destino SCL
|
||||
target_scl_name = get_target_scl_name(instruction, "out", network_id, default_to_temp=True)
|
||||
|
||||
# Verificar dependencias
|
||||
if sympy_en_expr is None or sympy_or_const_in is None or target_scl_name is None:
|
||||
return False
|
||||
|
||||
# Convertir la entrada (SymPy o Constante) a SCL
|
||||
# La simplificación aquí no suele aplicar a la conversión en sí,
|
||||
# pero sí podría aplicar a la condición EN.
|
||||
input_scl = sympy_expr_to_scl(sympy_or_const_in, symbol_manager)
|
||||
|
||||
# Determinar el tipo de destino (esto sigue siendo un desafío sin info completa)
|
||||
# Usaremos funciones de conversión SCL explícitas si podemos inferirlas.
|
||||
target_type_hint = instruction.get("template_values", {}).get("destType", "").upper() # Ejemplo
|
||||
source_type_hint = "" # Necesitaríamos info del tipo de origen
|
||||
conversion_func_name = None
|
||||
|
||||
# Heurística MUY básica (necesita mejorar con info de tipos real)
|
||||
if target_type_hint and source_type_hint and target_type_hint != source_type_hint:
|
||||
conversion_func_name = f"{source_type_hint}_TO_{target_type_hint}"
|
||||
|
||||
# Generar SCL Core
|
||||
if conversion_func_name:
|
||||
# Usar función explícita si la inferimos
|
||||
scl_core = f"{target_scl_name} := {conversion_func_name}({input_scl});"
|
||||
else:
|
||||
# Asignación directa (MOVE implícito) si no hay conversión clara
|
||||
# ADVERTENCIA: Esto puede causar errores de tipo en el PLC si los tipos no coinciden.
|
||||
scl_core = f"{target_scl_name} := {input_scl};"
|
||||
if target_type_hint: # Añadir comentario si al menos conocemos el destino
|
||||
scl_core += f" // TODO: Verify implicit conversion to {target_type_hint}"
|
||||
|
||||
|
||||
# Aplicar Condición EN (Simplificando EN)
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for Convert {instr_uid}: {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
indented_core = "\n".join([f" {line}" for line in scl_core.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_core}\nEND_IF;"
|
||||
else:
|
||||
scl_final = scl_core
|
||||
|
||||
# Actualizar instrucción y mapa
|
||||
instruction["scl"] = scl_final # SCL final generado
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
|
||||
# Propagar valor de salida (el contenido del destino) y ENO
|
||||
map_key_out = (network_id, instr_uid, "out")
|
||||
# Guardar el *nombre* SCL del destino en el mapa, ya que contiene el valor
|
||||
# O podríamos crear un símbolo SymPy para ello si fuera necesario aguas abajo? Por ahora, string.
|
||||
sympy_map[map_key_out] = target_scl_name
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||
|
||||
return True
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para el procesador Convert."""
|
||||
return {'type_name': 'convert', 'processor_func': process_convert, 'priority': 4}
|
|
@ -0,0 +1,110 @@
|
|||
# processors/process_counter.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import traceback
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, format_variable_name, get_target_scl_name
|
||||
from .symbol_manager import SymbolManager
|
||||
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
|
||||
def process_counter(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
"""
|
||||
Genera SCL para Contadores (CTU, CTD, CTUD).
|
||||
Requiere datos de instancia (DB o STAT).
|
||||
"""
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
instr_type_original = instruction.get("type", "") # CTU, CTD, CTUD
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
# 1. Definir pines de entrada esperados
|
||||
input_pins_map = {
|
||||
"CTU": ["CU", "R", "PV"],
|
||||
"CTD": ["CD", "LD", "PV"],
|
||||
"CTUD": ["CU", "CD", "R", "LD", "PV"]
|
||||
}
|
||||
input_pins = input_pins_map.get(instr_type_original.upper())
|
||||
if not input_pins:
|
||||
instruction["scl"] = f"// ERROR: Tipo de contador no soportado: {instr_type_original}"
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
# 2. Procesar Parámetros de Entrada
|
||||
scl_call_params = []
|
||||
dependencies_resolved = True
|
||||
optional_pins = {"R", "LD"} # Estos pueden no estar conectados
|
||||
|
||||
for pin in input_pins:
|
||||
pin_info = instruction["inputs"].get(pin)
|
||||
if pin_info: # Si el pin está definido en el JSON
|
||||
source_sympy_or_const = get_sympy_representation(pin_info, network_id, sympy_map, symbol_manager)
|
||||
if source_sympy_or_const is None:
|
||||
# print(f"DEBUG Counter {instr_uid}: Input param '{pin}' dependency not ready.")
|
||||
dependencies_resolved = False
|
||||
break
|
||||
# Convertir a SCL para la llamada (sin simplificar aquí)
|
||||
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
|
||||
pin_name_scl = format_variable_name(pin) # Formatear nombre del parámetro
|
||||
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
|
||||
elif pin not in optional_pins: # Si falta un pin requerido
|
||||
print(f"Error: Falta entrada requerida '{pin}' para {instr_type_original} UID {instr_uid}.")
|
||||
instruction["scl"] = f"// ERROR: Falta entrada requerida '{pin}' para {instr_type_original} UID {instr_uid}."
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
return True
|
||||
|
||||
if not dependencies_resolved:
|
||||
return False
|
||||
|
||||
# 3. Obtener Nombre de Instancia
|
||||
# Asumiendo que x1 o una fase previa llena 'instance_db' si es un FB multi-instancia
|
||||
instance_name_raw = instruction.get("instance_db")
|
||||
if not instance_name_raw:
|
||||
# Asumiendo que es STAT si no hay DB instancia explícito (requiere declaración en x3)
|
||||
instance_name_raw = instruction.get("instance_name") # Buscar nombre directo si x1 lo provee
|
||||
if not instance_name_raw:
|
||||
instance_name_raw = f"#CTR_INSTANCE_{instr_uid}" # Placeholder final
|
||||
print(f"Advertencia: No se encontró nombre/instancia para {instr_type_original} UID {instr_uid}. Usando placeholder '{instance_name_raw}'.")
|
||||
instance_name_scl = format_variable_name(instance_name_raw)
|
||||
|
||||
# 4. Generar la llamada SCL
|
||||
param_string = ", ".join(scl_call_params)
|
||||
scl_call = f"{instance_name_scl}({param_string}); // TODO: Declarar {instance_name_scl} : {instr_type_original.upper()}; en VAR_STAT o VAR"
|
||||
|
||||
# Contadores no suelen tener EN/ENO explícito en LAD, se asume siempre habilitado
|
||||
instruction["scl"] = scl_call # SCL final generado
|
||||
instruction["type"] = instr_type_original + SCL_SUFFIX
|
||||
|
||||
# 4. Actualizar sympy_map para las salidas (QU, QD, CV)
|
||||
output_pins_map = {
|
||||
"CTU": ["QU", "CV"],
|
||||
"CTD": ["QD", "CV"],
|
||||
"CTUD": ["QU", "QD", "CV"]
|
||||
}
|
||||
output_pins = output_pins_map.get(instr_type_original.upper(), [])
|
||||
|
||||
for pin in output_pins:
|
||||
map_key = (network_id, instr_uid, pin)
|
||||
output_scl_access = f"{instance_name_scl}.{pin.upper()}"
|
||||
if pin.upper() in ["QU", "QD"]: # These are boolean outputs
|
||||
# *** Store SymPy Symbol for boolean outputs QU/QD ***
|
||||
sympy_out_symbol = symbol_manager.get_symbol(output_scl_access)
|
||||
if sympy_out_symbol:
|
||||
sympy_map[map_key] = sympy_out_symbol # Store SYMBOL
|
||||
else:
|
||||
print(f"Error: Could not create symbol for {output_scl_access} in {instr_type_original} {instr_uid}")
|
||||
sympy_map[map_key] = None
|
||||
else:
|
||||
# For non-boolean (like CV - count value), store SCL access string
|
||||
sympy_map[map_key] = output_scl_access
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para los contadores CTU, CTD, CTUD."""
|
||||
return [
|
||||
{'type_name': 'ctu', 'processor_func': process_counter, 'priority': 5},
|
||||
{'type_name': 'ctd', 'processor_func': process_counter, 'priority': 5},
|
||||
{'type_name': 'ctud', 'processor_func': process_counter, 'priority': 5}
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue