Uso del subdirectorio parsing para una implementacion mas limpia
This commit is contained in:
parent
546705f8ca
commit
60fea74ebf
|
@ -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,147 @@
|
||||||
|
# generators/generate_scl_code_block.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||||
|
|
||||||
|
# Definir SCL_SUFFIX aquí porque se usa en _generate_scl_body
|
||||||
|
SCL_SUFFIX = "_sympy_processed"
|
||||||
|
|
||||||
|
def _generate_scl_header(data, scl_block_name):
|
||||||
|
"""Genera el encabezado SCL para FC/FB/OB."""
|
||||||
|
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" # Default for FB
|
||||||
|
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:")
|
||||||
|
for line in block_comment.splitlines():
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
|
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 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: # FB, OB
|
||||||
|
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
|
||||||
|
|
||||||
|
def _generate_scl_interface(interface_data):
|
||||||
|
"""Genera las secciones VAR_* de la interfaz SCL para FC/FB/OB."""
|
||||||
|
scl_output = []
|
||||||
|
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
||||||
|
declared_temps = set()
|
||||||
|
|
||||||
|
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()}"
|
||||||
|
if section_name == "Static": scl_section_keyword = "VAR_STAT" # Para FBs
|
||||||
|
if section_name == "Temp": scl_section_keyword = "VAR_TEMP"
|
||||||
|
if section_name == "Constant": scl_section_keyword = "CONSTANT"
|
||||||
|
|
||||||
|
scl_output.append(scl_section_keyword)
|
||||||
|
scl_output.extend(generate_scl_declarations(vars_in_section, indent_level=1))
|
||||||
|
scl_output.append("END_VAR" if section_name != "Constant" else "END_CONSTANT")
|
||||||
|
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
|
||||||
|
|
||||||
|
def _generate_scl_temp_vars(data, declared_temps):
|
||||||
|
"""Detecta y genera declaraciones VAR_TEMP adicionales."""
|
||||||
|
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)
|
||||||
|
for temp_name in found_temps:
|
||||||
|
if temp_name: temp_vars_detected.add(temp_name)
|
||||||
|
|
||||||
|
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||||
|
if additional_temps:
|
||||||
|
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
||||||
|
if not declared_temps:
|
||||||
|
scl_output.append("VAR_TEMP")
|
||||||
|
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 declared_temps:
|
||||||
|
scl_output.append("END_VAR")
|
||||||
|
scl_output.append("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def _generate_scl_body(networks):
|
||||||
|
"""Genera el cuerpo SCL (BEGIN...END) con la lógica de las redes."""
|
||||||
|
scl_output = ["BEGIN", ""]
|
||||||
|
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_line}") for stl_line in raw_stl_code.splitlines()]; 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: # SCL/LAD/FBD
|
||||||
|
for instruction in logic_in_network:
|
||||||
|
instruction_type = instruction.get("type", ""); scl_code = instruction.get("scl", ""); is_grouped = instruction.get("grouped", False)
|
||||||
|
if is_grouped: continue
|
||||||
|
if (instruction_type.endswith(SCL_SUFFIX) or instruction_type in ["RAW_SCL_CHUNK","UNSUPPORTED_LANG","UNSUPPORTED_CONTENT","PARSING_ERROR"] or "_error" in instruction_type) and scl_code:
|
||||||
|
is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines() if line.strip())
|
||||||
|
is_if_block = scl_code.strip().startswith("IF")
|
||||||
|
if (not is_only_comment or is_if_block or "_error" in instruction_type or instruction_type in ["UNSUPPORTED_LANG","UNSUPPORTED_CONTENT","PARSING_ERROR"]):
|
||||||
|
network_has_code = True; [scl_output.append(f" {line}") for line in scl_code.splitlines()]; 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("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def generate_scl_for_code_block(data):
|
||||||
|
"""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_lines, declared_temps = _generate_scl_interface(interface_data)
|
||||||
|
scl_output.extend(interface_lines)
|
||||||
|
scl_output.extend(_generate_scl_temp_vars(data, declared_temps))
|
||||||
|
scl_output.extend(_generate_scl_body(data.get("networks", [])))
|
||||||
|
scl_output.append(f"END_{scl_block_keyword}")
|
||||||
|
|
||||||
|
return scl_output
|
|
@ -0,0 +1,60 @@
|
||||||
|
# generators/generate_scl_db.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||||
|
|
||||||
|
def _generate_scl_header(data, scl_block_name):
|
||||||
|
"""Genera el encabezado SCL para DB."""
|
||||||
|
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:")
|
||||||
|
for line in block_comment.splitlines():
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
|
scl_output.append("")
|
||||||
|
scl_output.append(f'DATA_BLOCK "{scl_block_name}"') # Keyword específica
|
||||||
|
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||||
|
scl_output.append("VERSION : 0.1")
|
||||||
|
scl_output.append("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def _generate_scl_interface(interface_data):
|
||||||
|
"""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")
|
||||||
|
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
||||||
|
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
|
||||||
|
|
||||||
|
def generate_scl_for_db(data):
|
||||||
|
"""Genera el contenido SCL completo para un DATA_BLOCK."""
|
||||||
|
scl_output = []
|
||||||
|
scl_block_name = format_variable_name(data.get("block_name", "UnknownDB"))
|
||||||
|
|
||||||
|
# Generar cabecera
|
||||||
|
scl_output.extend(_generate_scl_header(data, scl_block_name))
|
||||||
|
|
||||||
|
# Generar interfaz
|
||||||
|
interface_data = data.get("interface", {})
|
||||||
|
scl_output.extend(_generate_scl_interface(interface_data))
|
||||||
|
|
||||||
|
# Generar cuerpo (vacío para DB)
|
||||||
|
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,150 @@
|
||||||
|
# generators/generator_utils.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
|
||||||
|
# --- Importar format_variable_name desde processors ---
|
||||||
|
# Es mejor mantenerlo centralizado si se usa en varios pasos.
|
||||||
|
try:
|
||||||
|
from processors.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 (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!).")
|
||||||
|
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 ---
|
||||||
|
|
||||||
|
# para formatear valores iniciales
|
||||||
|
def format_scl_start_value(value, datatype):
|
||||||
|
"""Formatea un valor para la inicialización SCL/Markdown según el tipo."""
|
||||||
|
if value is None: return None
|
||||||
|
datatype_lower = datatype.lower() if datatype else ""
|
||||||
|
value_str = str(value); value_str_unquoted = value_str
|
||||||
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: value_str_unquoted = value_str[1:-1]
|
||||||
|
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1: value_str_unquoted = value_str[1:-1]
|
||||||
|
|
||||||
|
# Integer-like
|
||||||
|
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:
|
||||||
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): return value_str_unquoted
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", "")
|
||||||
|
return f"'{escaped_for_scl}'" # Fallback as string
|
||||||
|
# Bool
|
||||||
|
elif "bool" in datatype_lower: return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
||||||
|
# String/Char
|
||||||
|
elif "string" in datatype_lower: escaped_value = value_str_unquoted.replace("'", "''"); return f"'{escaped_value}'"
|
||||||
|
elif "char" in datatype_lower: escaped_value = value_str_unquoted.replace("'", "''"); return f"'{escaped_value}'"
|
||||||
|
# Real
|
||||||
|
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
||||||
|
try:
|
||||||
|
f_val = float(value_str_unquoted); s_val = str(f_val)
|
||||||
|
if "." not in s_val and "e" not in s_val.lower(): s_val += ".0"
|
||||||
|
return s_val
|
||||||
|
except ValueError:
|
||||||
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): return value_str_unquoted
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'" # Fallback
|
||||||
|
# Time
|
||||||
|
elif "time" in datatype_lower:
|
||||||
|
prefix, val_to_use = "", value_str_unquoted
|
||||||
|
if val_to_use.upper().startswith("T#"): prefix, val_to_use = "T#", val_to_use[2:]
|
||||||
|
elif val_to_use.upper().startswith("LT#"): prefix, val_to_use = "LT#", val_to_use[3:]
|
||||||
|
elif val_to_use.upper().startswith("S5T#"): prefix, val_to_use = "S5T#", val_to_use[4:]
|
||||||
|
if "s5time" in datatype_lower: return f"S5T#{val_to_use}"
|
||||||
|
elif "ltime" in datatype_lower: return f"LT#{val_to_use}"
|
||||||
|
else: return f"T#{val_to_use}"
|
||||||
|
# Date/Time Of Day
|
||||||
|
elif "date" in datatype_lower: # Must check DTL/DT/TOD first
|
||||||
|
val_to_use = value_str_unquoted
|
||||||
|
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
|
||||||
|
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"; val_to_use = val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
elif "dt" in datatype_lower:
|
||||||
|
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"; val_to_use = val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
||||||
|
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"; val_to_use = val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
else: # Default to Date D#
|
||||||
|
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"; val_to_use = val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
# Fallback
|
||||||
|
else:
|
||||||
|
if re.match(r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str): # Check if it looks like a symbol/path
|
||||||
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: return value_str[1:-1] # UDT literal?
|
||||||
|
if '"' in value_str and "." in value_str and value_str.count('"') == 2: return value_str # DB access?
|
||||||
|
if not value_str.startswith('"') and not value_str.startswith("'"):
|
||||||
|
if value_str.startswith("#") or value_str.startswith("%"): return value_str # Temp or Absolute
|
||||||
|
else: return value_str # Symbolic constant?
|
||||||
|
return value_str # Other complex string?
|
||||||
|
else: # Final fallback: treat as string literal
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'"
|
||||||
|
|
||||||
|
def generate_scl_declarations(variables, indent_level=1):
|
||||||
|
"""Genera las líneas SCL para declarar variables, structs y arrays."""
|
||||||
|
scl_lines = []
|
||||||
|
indent = " " * indent_level
|
||||||
|
for var in variables:
|
||||||
|
var_name_scl = format_variable_name(var.get("name"))
|
||||||
|
var_dtype_raw = var.get("datatype", "VARIANT")
|
||||||
|
var_comment = var.get("comment")
|
||||||
|
start_value = var.get("start_value")
|
||||||
|
children = var.get("children")
|
||||||
|
array_elements = var.get("array_elements")
|
||||||
|
|
||||||
|
# Limpiar y determinar tipo base
|
||||||
|
var_dtype_cleaned = var_dtype_raw
|
||||||
|
if isinstance(var_dtype_raw, str):
|
||||||
|
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'): var_dtype_cleaned = var_dtype_raw[1:-1]
|
||||||
|
array_match = re.match(r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE)
|
||||||
|
if array_match: var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}"
|
||||||
|
base_type_for_init = var_dtype_cleaned
|
||||||
|
array_prefix_for_decl = ""
|
||||||
|
if isinstance(var_dtype_cleaned, str) and var_dtype_cleaned.lower().startswith("array["): # Check if string before lower()
|
||||||
|
match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE)
|
||||||
|
if match: array_prefix_for_decl, base_type_for_init = match.group(1), match.group(2).strip()
|
||||||
|
|
||||||
|
# Construir tipo para declaración
|
||||||
|
declaration_dtype = var_dtype_raw
|
||||||
|
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl: # Simple UDT/Complex
|
||||||
|
if isinstance(base_type_for_init, str) and not base_type_for_init.startswith('"'): declaration_dtype = f'"{base_type_for_init}"'
|
||||||
|
else: declaration_dtype = base_type_for_init
|
||||||
|
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned: # Array of UDT/Complex
|
||||||
|
if isinstance(base_type_for_init, str) and not base_type_for_init.startswith('"'): declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
|
||||||
|
else: declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
|
||||||
|
|
||||||
|
|
||||||
|
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
||||||
|
init_value_scl = None
|
||||||
|
|
||||||
|
# Manejar Arrays / Structs / Simples
|
||||||
|
if array_elements:
|
||||||
|
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: print(f"Advertencia: Índices array no numéricos para '{var_name_scl}'."); sorted_indices_str = sorted(array_elements.keys())
|
||||||
|
init_values = []
|
||||||
|
for idx_str in sorted_indices_str:
|
||||||
|
try: formatted_val = format_scl_start_value(array_elements[idx_str], base_type_for_init); init_values.append(formatted_val)
|
||||||
|
except Exception as e_fmt: print(f"ERROR formato array idx {idx_str} de '{var_name_scl}': {e_fmt}"); init_values.append(f"/*ERR_FMT_{idx_str}*/")
|
||||||
|
valid_inits = [v for v in init_values if v is not None]
|
||||||
|
if valid_inits: init_value_scl = f"[{', '.join(valid_inits)}]"
|
||||||
|
elif array_elements: print(f"Advertencia: Valores iniciales array '{var_name_scl}' son None/inválidos.")
|
||||||
|
elif children:
|
||||||
|
scl_lines.append(declaration_line); scl_lines.append(f"{indent}STRUCT")
|
||||||
|
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
||||||
|
scl_lines.append(f"{indent}END_STRUCT;")
|
||||||
|
if var_comment: scl_lines.append(f"{indent}// {var_comment}")
|
||||||
|
scl_lines.append(""); continue
|
||||||
|
else: # Simple
|
||||||
|
if start_value is not None:
|
||||||
|
try: init_value_scl = format_scl_start_value(start_value, base_type_for_init)
|
||||||
|
except Exception as e_fmt_simple: print(f"ERROR formato simple '{var_name_scl}': {e_fmt_simple}"); init_value_scl = f"/*ERR_FMT_SIMPLE*/"
|
||||||
|
|
||||||
|
# Añadir inicialización y comentario
|
||||||
|
if init_value_scl is not None: declaration_line += f" := {init_value_scl}"
|
||||||
|
declaration_line += ";"
|
||||||
|
if var_comment: declaration_line += f" // {var_comment}"
|
||||||
|
scl_lines.append(declaration_line)
|
||||||
|
return scl_lines
|
|
@ -15,45 +15,44 @@ ns = {
|
||||||
|
|
||||||
|
|
||||||
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
|
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
|
||||||
"""Extrae texto multilingüe de un elemento XML."""
|
"""Extrae texto multilingüe de un elemento XML, asegurando devolver siempre string."""
|
||||||
if element is None:
|
if element is None:
|
||||||
return ""
|
return "" # Devolver cadena vacía si el elemento es None
|
||||||
try:
|
try:
|
||||||
# Intenta buscar el idioma por defecto
|
# Intenta buscar el idioma por defecto
|
||||||
xpath_expr_default = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{default_lang}']/iface:AttributeList/iface:Text"
|
xpath_expr_default = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{default_lang}']/iface:AttributeList/iface:Text"
|
||||||
text_items_default = element.xpath(xpath_expr_default, namespaces=ns)
|
text_items_default = element.xpath(xpath_expr_default, namespaces=ns)
|
||||||
|
# CORRECCIÓN: Devolver "" si .text es None
|
||||||
if text_items_default and text_items_default[0].text is not None:
|
if text_items_default and text_items_default[0].text is not None:
|
||||||
return text_items_default[0].text.strip()
|
return text_items_default[0].text.strip()
|
||||||
|
# Intentar buscar el idioma de fallback
|
||||||
# Intenta buscar el idioma de fallback
|
|
||||||
xpath_expr_fallback = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{fallback_lang}']/iface:AttributeList/iface:Text"
|
xpath_expr_fallback = f".//iface:MultilingualTextItem[iface:AttributeList/iface:Culture='{fallback_lang}']/iface:AttributeList/iface:Text"
|
||||||
text_items_fallback = element.xpath(xpath_expr_fallback, namespaces=ns)
|
text_items_fallback = element.xpath(xpath_expr_fallback, namespaces=ns)
|
||||||
|
# CORRECCIÓN: Devolver "" si .text es None
|
||||||
if text_items_fallback and text_items_fallback[0].text is not None:
|
if text_items_fallback and text_items_fallback[0].text is not None:
|
||||||
return text_items_fallback[0].text.strip()
|
return text_items_fallback[0].text.strip()
|
||||||
|
|
||||||
# Si no encuentra ninguno, toma el primer texto que encuentre
|
# Si no encuentra ninguno, toma el primer texto que encuentre
|
||||||
xpath_expr_any = ".//iface:MultilingualTextItem/iface:AttributeList/iface:Text"
|
xpath_expr_any = ".//iface:MultilingualTextItem/iface:AttributeList/iface:Text"
|
||||||
text_items_any = element.xpath(xpath_expr_any, namespaces=ns)
|
text_items_any = element.xpath(xpath_expr_any, namespaces=ns)
|
||||||
|
# CORRECCIÓN: Devolver "" si .text es None
|
||||||
if text_items_any and text_items_any[0].text is not None:
|
if text_items_any and text_items_any[0].text is not None:
|
||||||
return text_items_any[0].text.strip()
|
return text_items_any[0].text.strip()
|
||||||
|
|
||||||
# Fallback si MultilingualText está vacío o tiene una estructura inesperada
|
# Fallback final si no se encontró ningún MultilingualTextItem con texto
|
||||||
return ""
|
return "" # Asegurar retorno de string vacío
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
|
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
|
||||||
# traceback.print_exc() # Descomentar para más detalles del error
|
# traceback.print_exc() # Descomentar para más detalles del error
|
||||||
return ""
|
return "" # Devolver cadena vacía en caso de excepción
|
||||||
|
|
||||||
|
|
||||||
def get_symbol_name(symbol_element):
|
def get_symbol_name(symbol_element):
|
||||||
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
||||||
# Adaptado para usar namespace flg
|
|
||||||
if symbol_element is None:
|
if symbol_element is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
# Asume que Component está dentro de Symbol y ambos están en el namespace flg
|
|
||||||
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
||||||
# Formatear correctamente con comillas dobles si es necesario (ej. DBs)
|
|
||||||
return (
|
return (
|
||||||
".".join(
|
".".join(
|
||||||
f'"{c}"' if not c.startswith("#") and '"' not in c else c
|
f'"{c}"' if not c.startswith("#") and '"' not in c else c
|
||||||
|
@ -69,39 +68,30 @@ def get_symbol_name(symbol_element):
|
||||||
|
|
||||||
def parse_access(access_element):
|
def parse_access(access_element):
|
||||||
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
|
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
|
||||||
# Adaptado para usar namespace flg
|
|
||||||
if access_element is None:
|
if access_element is None:
|
||||||
return None
|
return None
|
||||||
uid = access_element.get("UId")
|
uid = access_element.get("UId")
|
||||||
scope = access_element.get("Scope")
|
scope = access_element.get("Scope")
|
||||||
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
||||||
|
|
||||||
# Buscar Symbol o Constant usando el namespace flg
|
|
||||||
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
||||||
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
||||||
|
|
||||||
if symbol:
|
if symbol:
|
||||||
info["type"] = "variable"
|
info["type"] = "variable"
|
||||||
# Llamar a get_symbol_name que ahora espera flg:Symbol
|
|
||||||
info["name"] = get_symbol_name(symbol[0])
|
info["name"] = get_symbol_name(symbol[0])
|
||||||
if info["name"] is None:
|
if info["name"] is None:
|
||||||
info["type"] = "error_parsing_symbol"
|
info["type"] = "error_parsing_symbol"
|
||||||
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
|
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
|
||||||
# Intentar extraer texto directamente como fallback muy básico
|
|
||||||
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
|
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
|
||||||
info["name"] = (
|
info["name"] = (
|
||||||
f'"_ERR_PARSING_{raw_text[:20]}"'
|
f'"_ERR_PARSING_{raw_text[:20]}"'
|
||||||
if raw_text
|
if raw_text
|
||||||
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
|
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
|
||||||
)
|
)
|
||||||
# return info # Podríamos devolver el error aquí
|
|
||||||
elif constant:
|
elif constant:
|
||||||
info["type"] = "constant"
|
info["type"] = "constant"
|
||||||
# Buscar ConstantType y ConstantValue usando el namespace flg
|
|
||||||
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
||||||
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
||||||
|
|
||||||
# Extraer texto
|
|
||||||
info["datatype"] = (
|
info["datatype"] = (
|
||||||
const_type_elem[0].text.strip()
|
const_type_elem[0].text.strip()
|
||||||
if const_type_elem and const_type_elem[0].text is not None
|
if const_type_elem and const_type_elem[0].text is not None
|
||||||
|
@ -112,14 +102,10 @@ def parse_access(access_element):
|
||||||
if const_val_elem and const_val_elem[0].text is not None
|
if const_val_elem and const_val_elem[0].text is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if value_str is None:
|
if value_str is None:
|
||||||
info["type"] = "error_parsing_constant"
|
info["type"] = "error_parsing_constant"
|
||||||
info["value"] = None
|
info["value"] = None
|
||||||
print(f"Error: Constante sin valor Access UID={uid}")
|
print(f"Error: Constante sin valor Access UID={uid}")
|
||||||
# return info
|
|
||||||
|
|
||||||
# Inferir tipo si es Unknown (igual que antes)
|
|
||||||
if info["datatype"] == "Unknown" and value_str:
|
if info["datatype"] == "Unknown" and value_str:
|
||||||
val_lower = value_str.lower()
|
val_lower = value_str.lower()
|
||||||
if val_lower in ["true", "false"]:
|
if val_lower in ["true", "false"]:
|
||||||
|
@ -127,15 +113,14 @@ def parse_access(access_element):
|
||||||
elif value_str.isdigit() or (
|
elif value_str.isdigit() or (
|
||||||
value_str.startswith("-") and value_str[1:].isdigit()
|
value_str.startswith("-") and value_str[1:].isdigit()
|
||||||
):
|
):
|
||||||
info["datatype"] = "Int" # O DInt? Int es más seguro
|
info["datatype"] = "Int"
|
||||||
elif "." in value_str:
|
elif "." in value_str:
|
||||||
try:
|
try:
|
||||||
float(value_str)
|
float(value_str)
|
||||||
info["datatype"] = "Real" # O LReal? Real es más seguro
|
info["datatype"] = "Real"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Podría ser string con punto
|
pass
|
||||||
elif "#" in value_str:
|
elif "#" in value_str:
|
||||||
# Inferir tipo desde prefijo (T#, DT#, '...', etc.)
|
|
||||||
parts = value_str.split("#", 1)
|
parts = value_str.split("#", 1)
|
||||||
prefix = parts[0].upper()
|
prefix = parts[0].upper()
|
||||||
if prefix == "T":
|
if prefix == "T":
|
||||||
|
@ -152,21 +137,14 @@ def parse_access(access_element):
|
||||||
info["datatype"] = "DTL"
|
info["datatype"] = "DTL"
|
||||||
elif prefix == "TOD":
|
elif prefix == "TOD":
|
||||||
info["datatype"] = "Time_Of_Day"
|
info["datatype"] = "Time_Of_Day"
|
||||||
# Añadir más prefijos si es necesario (WSTRING#, STRING#, etc.)
|
|
||||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||||
info["datatype"] = "String" # O Char? String es más probable
|
info["datatype"] = "String"
|
||||||
else:
|
else:
|
||||||
info["datatype"] = (
|
info["datatype"] = "TypedConstant"
|
||||||
"TypedConstant" # Genérico si no se reconoce prefijo
|
|
||||||
)
|
|
||||||
|
|
||||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||||
info["datatype"] = "String" # O Char?
|
info["datatype"] = "String"
|
||||||
|
info["value"] = value_str
|
||||||
info["value"] = value_str # Guardar valor original
|
|
||||||
# Intentar conversión numérica/booleana (igual que antes)
|
|
||||||
dtype_lower = info["datatype"].lower()
|
dtype_lower = info["datatype"].lower()
|
||||||
# Quitar prefijo y comillas para la conversión
|
|
||||||
val_str_processed = value_str
|
val_str_processed = value_str
|
||||||
if isinstance(value_str, str):
|
if isinstance(value_str, str):
|
||||||
if "#" in value_str:
|
if "#" in value_str:
|
||||||
|
@ -198,29 +176,19 @@ def parse_access(access_element):
|
||||||
)
|
)
|
||||||
elif dtype_lower in ["real", "lreal"]:
|
elif dtype_lower in ["real", "lreal"]:
|
||||||
info["value"] = float(val_str_processed)
|
info["value"] = float(val_str_processed)
|
||||||
# Mantener string para otros tipos (Time, Date, String, Char, TypedConstant)
|
except (ValueError, TypeError):
|
||||||
except (ValueError, TypeError) as e:
|
info["value"] = value_str
|
||||||
# Permitir que el valor sea un string si la conversión falla (podría ser una constante simbólica)
|
|
||||||
# print(f"Advertencia: No se pudo convertir valor constante '{val_str_processed}' a {dtype_lower} UID={uid}. Manteniendo string. Error: {e}")
|
|
||||||
info["value"] = value_str # Mantener string original
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
info["type"] = "unknown_structure"
|
info["type"] = "unknown_structure"
|
||||||
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
||||||
# return info
|
|
||||||
|
|
||||||
# Verificar nombre faltante después de intentar parsear
|
|
||||||
if info["type"] == "variable" and info.get("name") is None:
|
if info["type"] == "variable" and info.get("name") is None:
|
||||||
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
|
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
|
||||||
info["type"] = "error_no_name"
|
info["type"] = "error_no_name"
|
||||||
# return info
|
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
def parse_part(part_element):
|
def parse_part(part_element):
|
||||||
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
||||||
# Asume que Part está en namespace flg
|
|
||||||
if part_element is None:
|
if part_element is None:
|
||||||
return None
|
return None
|
||||||
uid = part_element.get("UId")
|
uid = part_element.get("UId")
|
||||||
|
@ -230,10 +198,9 @@ def parse_part(part_element):
|
||||||
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
template_values = {}
|
template_values = {}
|
||||||
|
negated_pins = {}
|
||||||
try:
|
try:
|
||||||
# TemplateValue parece NO tener namespace flg
|
|
||||||
for tv in part_element.xpath("./TemplateValue"):
|
for tv in part_element.xpath("./TemplateValue"):
|
||||||
tv_name = tv.get("Name")
|
tv_name = tv.get("Name")
|
||||||
tv_type = tv.get("Type")
|
tv_type = tv.get("Type")
|
||||||
|
@ -241,20 +208,16 @@ def parse_part(part_element):
|
||||||
template_values[tv_name] = tv_type
|
template_values[tv_name] = tv_type
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
|
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
|
||||||
|
|
||||||
negated_pins = {}
|
|
||||||
try:
|
try:
|
||||||
# Negated parece NO tener namespace flg
|
|
||||||
for negated_elem in part_element.xpath("./Negated"):
|
for negated_elem in part_element.xpath("./Negated"):
|
||||||
negated_pin_name = negated_elem.get("Name")
|
negated_pin_name = negated_elem.get("Name")
|
||||||
if negated_pin_name:
|
if negated_pin_name:
|
||||||
negated_pins[negated_pin_name] = True
|
negated_pins[negated_pin_name] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Advertencia: Error extrayendo Negated Pins Part UID={uid}: {e}")
|
print(f"Advertencia: Error extrayendo Negated Pins Part UID={uid}: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uid": uid,
|
"uid": uid,
|
||||||
"type": name, # El 'type' de la instrucción (e.g., 'Add', 'Contact')
|
"type": name,
|
||||||
"template_values": template_values,
|
"template_values": template_values,
|
||||||
"negated_pins": negated_pins,
|
"negated_pins": negated_pins,
|
||||||
}
|
}
|
||||||
|
@ -262,7 +225,6 @@ def parse_part(part_element):
|
||||||
|
|
||||||
def parse_call(call_element):
|
def parse_call(call_element):
|
||||||
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
||||||
# Asume que Call está en namespace flg
|
|
||||||
if call_element is None:
|
if call_element is None:
|
||||||
return None
|
return None
|
||||||
uid = call_element.get("UId")
|
uid = call_element.get("UId")
|
||||||
|
@ -271,31 +233,19 @@ def parse_call(call_element):
|
||||||
f"Error: Call encontrado sin UID: {etree.tostring(call_element, encoding='unicode')}"
|
f"Error: Call encontrado sin UID: {etree.tostring(call_element, encoding='unicode')}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# << CORRECCIÓN: CallInfo y sus hijos están en el namespace por defecto (flg) >>
|
|
||||||
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
|
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
|
||||||
if not call_info_elem:
|
if not call_info_elem:
|
||||||
print(f"Error: Call UID {uid} sin elemento flg:CallInfo.")
|
|
||||||
# Intentar sin namespace como fallback por si acaso
|
|
||||||
call_info_elem_no_ns = call_element.xpath("./CallInfo")
|
call_info_elem_no_ns = call_element.xpath("./CallInfo")
|
||||||
if not call_info_elem_no_ns:
|
if not call_info_elem_no_ns:
|
||||||
print(
|
print(f"Error: Call UID {uid} sin elemento CallInfo.")
|
||||||
f"Error: Call UID {uid} sin elemento CallInfo (probado sin NS tambien)."
|
return {"uid": uid, "type": "Call_error", "error": "Missing CallInfo"}
|
||||||
)
|
|
||||||
return {
|
|
||||||
"uid": uid,
|
|
||||||
"type": "Call_error",
|
|
||||||
"error": "Missing CallInfo",
|
|
||||||
} # Devolver error
|
|
||||||
else:
|
else:
|
||||||
# Si se encontró sin NS, usar ese (menos probable pero posible)
|
|
||||||
print(f"Advertencia: Call UID {uid} encontró CallInfo SIN namespace.")
|
print(f"Advertencia: Call UID {uid} encontró CallInfo SIN namespace.")
|
||||||
call_info = call_info_elem_no_ns[0]
|
call_info = call_info_elem_no_ns[0]
|
||||||
else:
|
else:
|
||||||
call_info = call_info_elem[0] # Usar el encontrado con namespace
|
call_info = call_info_elem[0]
|
||||||
|
|
||||||
block_name = call_info.get("Name")
|
block_name = call_info.get("Name")
|
||||||
block_type = call_info.get("BlockType") # FC, FB
|
block_type = call_info.get("BlockType")
|
||||||
if not block_name or not block_type:
|
if not block_name or not block_type:
|
||||||
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
|
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
|
||||||
return {
|
return {
|
||||||
|
@ -303,23 +253,17 @@ def parse_call(call_element):
|
||||||
"type": "Call_error",
|
"type": "Call_error",
|
||||||
"error": "Missing Name or BlockType in CallInfo",
|
"error": "Missing Name or BlockType in CallInfo",
|
||||||
}
|
}
|
||||||
|
instance_name, instance_scope = None, None
|
||||||
instance_name = None
|
|
||||||
instance_scope = None
|
|
||||||
# Buscar Instance y Component (que también deberían estar en namespace flg)
|
|
||||||
# Solo relevante si es FB
|
|
||||||
if block_type == "FB":
|
if block_type == "FB":
|
||||||
instance_elem_list = call_info.xpath("./flg:Instance", namespaces=ns)
|
instance_elem_list = call_info.xpath("./flg:Instance", namespaces=ns)
|
||||||
if instance_elem_list:
|
if instance_elem_list:
|
||||||
instance_elem = instance_elem_list[0]
|
instance_elem = instance_elem_list[0]
|
||||||
instance_scope = instance_elem.get("Scope") # GlobalDB, LocalVariable, etc.
|
instance_scope = instance_elem.get("Scope")
|
||||||
# Buscar Component dentro de Instance
|
|
||||||
component_elem_list = instance_elem.xpath("./flg:Component", namespaces=ns)
|
component_elem_list = instance_elem.xpath("./flg:Component", namespaces=ns)
|
||||||
if component_elem_list:
|
if component_elem_list:
|
||||||
component_elem = component_elem_list[0]
|
component_elem = component_elem_list[0]
|
||||||
db_name_raw = component_elem.get("Name")
|
db_name_raw = component_elem.get("Name")
|
||||||
if db_name_raw:
|
if db_name_raw:
|
||||||
# Asegurar comillas dobles para nombres de DB
|
|
||||||
instance_name = (
|
instance_name = (
|
||||||
f'"{db_name_raw}"'
|
f'"{db_name_raw}"'
|
||||||
if not db_name_raw.startswith('"')
|
if not db_name_raw.startswith('"')
|
||||||
|
@ -337,87 +281,62 @@ def parse_call(call_element):
|
||||||
print(
|
print(
|
||||||
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
|
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
|
||||||
)
|
)
|
||||||
# Aquí podríamos intentar buscar si el scope del Call es LocalVariable para inferir STAT
|
call_scope = call_element.get("Scope")
|
||||||
call_scope = call_element.get("Scope") # Scope del <Call> mismo
|
|
||||||
if call_scope == "LocalVariable":
|
if call_scope == "LocalVariable":
|
||||||
# Si la llamada es local y no tiene <Instance>, probablemente es una multi-instancia STAT
|
instance_name = f'"{block_name}"'
|
||||||
instance_name = f'"{block_name}"' # Usar el nombre del bloque como nombre de instancia STAT (convención común)
|
instance_scope = "Static"
|
||||||
instance_scope = "Static" # Marcar como estático
|
|
||||||
print(
|
print(
|
||||||
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
||||||
)
|
)
|
||||||
# else: # Error si es Global y no tiene Instance? Depende de la semántica deseada.
|
|
||||||
# print(f"Error: FB Call '{block_name}' UID {uid} no es STAT y no tiene <flg:Instance>.")
|
|
||||||
# return {"uid": uid, "type": "Call_error", "error": "FB Call sin datos de instancia"}
|
|
||||||
|
|
||||||
# El 'type' aquí es genérico 'Call', la distinción FC/FB se hace con block_type
|
|
||||||
call_data = {
|
call_data = {
|
||||||
"uid": uid,
|
"uid": uid,
|
||||||
"type": "Call",
|
"type": "Call",
|
||||||
"block_name": block_name,
|
"block_name": block_name,
|
||||||
"block_type": block_type, # FC o FB
|
"block_type": block_type,
|
||||||
}
|
}
|
||||||
if instance_name:
|
if instance_name:
|
||||||
call_data["instance_db"] = instance_name # Nombre formateado SCL
|
call_data["instance_db"] = instance_name
|
||||||
if instance_scope:
|
if instance_scope:
|
||||||
call_data["instance_scope"] = instance_scope # Static, GlobalDB, etc.
|
call_data["instance_scope"] = instance_scope
|
||||||
|
|
||||||
return call_data
|
return call_data
|
||||||
|
|
||||||
|
|
||||||
def parse_interface_members(member_elements):
|
def parse_interface_members(member_elements):
|
||||||
"""
|
"""Parsea recursivamente miembros de interfaz/estructura."""
|
||||||
Parsea recursivamente una lista de elementos <Member> de una interfaz o estructura.
|
|
||||||
Maneja miembros simples, structs anidados y arrays con valores iniciales.
|
|
||||||
Usa el namespace 'iface'.
|
|
||||||
"""
|
|
||||||
members_data = []
|
members_data = []
|
||||||
if not member_elements:
|
if not member_elements:
|
||||||
return members_data
|
return members_data
|
||||||
|
|
||||||
for member in member_elements:
|
for member in member_elements:
|
||||||
member_name = member.get("Name")
|
member_name = member.get("Name")
|
||||||
member_dtype_raw = member.get(
|
member_dtype_raw = member.get("Datatype")
|
||||||
"Datatype"
|
member_version = member.get("Version")
|
||||||
) # Puede tener comillas o ser Array[...] of "..."
|
|
||||||
member_version = member.get("Version") # v1.0 etc.
|
|
||||||
member_remanence = member.get("Remanence", "NonRetain")
|
member_remanence = member.get("Remanence", "NonRetain")
|
||||||
member_accessibility = member.get("Accessibility", "Public")
|
member_accessibility = member.get("Accessibility", "Public")
|
||||||
|
|
||||||
if not member_name or not member_dtype_raw:
|
if not member_name or not member_dtype_raw:
|
||||||
print(
|
print("Advertencia: Miembro sin nombre o tipo de dato. Saltando.")
|
||||||
"Advertencia: Miembro sin nombre o tipo de dato encontrado. Saltando."
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Combinar tipo y versión si existe versión separada
|
|
||||||
member_dtype = (
|
member_dtype = (
|
||||||
f"{member_dtype_raw}:v{member_version}"
|
f"{member_dtype_raw}:v{member_version}"
|
||||||
if member_version
|
if member_version
|
||||||
else member_dtype_raw
|
else member_dtype_raw
|
||||||
)
|
)
|
||||||
|
|
||||||
member_info = {
|
member_info = {
|
||||||
"name": member_name,
|
"name": member_name,
|
||||||
"datatype": member_dtype, # Guardar el tipo original (puede tener comillas, versión)
|
"datatype": member_dtype,
|
||||||
"remanence": member_remanence,
|
"remanence": member_remanence,
|
||||||
"accessibility": member_accessibility,
|
"accessibility": member_accessibility,
|
||||||
"start_value": None,
|
"start_value": None,
|
||||||
"comment": None,
|
"comment": None,
|
||||||
"children": [], # Para Structs
|
"children": [],
|
||||||
"array_elements": {}, # Para Arrays
|
"array_elements": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Comentario del miembro
|
|
||||||
comment_node = member.xpath("./iface:Comment", namespaces=ns)
|
comment_node = member.xpath("./iface:Comment", namespaces=ns)
|
||||||
if comment_node:
|
if comment_node:
|
||||||
# Comentario está dentro de Comment/MultiLanguageText
|
member_info["comment"] = get_multilingual_text(
|
||||||
member_info["comment"] = get_multilingual_text(comment_node[0])
|
comment_node[0]
|
||||||
|
) # Usa la función robusta
|
||||||
# Valor inicial
|
|
||||||
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
|
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
|
||||||
if start_value_node:
|
if start_value_node:
|
||||||
# Puede ser un nombre de constante o un valor literal
|
|
||||||
constant_name = start_value_node[0].get("ConstantName")
|
constant_name = start_value_node[0].get("ConstantName")
|
||||||
member_info["start_value"] = (
|
member_info["start_value"] = (
|
||||||
constant_name
|
constant_name
|
||||||
|
@ -425,26 +344,18 @@ def parse_interface_members(member_elements):
|
||||||
else (
|
else (
|
||||||
start_value_node[0].text
|
start_value_node[0].text
|
||||||
if start_value_node[0].text is not None
|
if start_value_node[0].text is not None
|
||||||
else ""
|
else None
|
||||||
)
|
)
|
||||||
)
|
) # Devolver None si está vacío
|
||||||
# No intentar convertir aquí, se hará en x3 según el tipo de dato
|
|
||||||
|
|
||||||
# --- Structs Anidados ---
|
|
||||||
# Los miembros de un struct están dentro de Sections/Section/Member
|
|
||||||
nested_sections = member.xpath(
|
nested_sections = member.xpath(
|
||||||
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
||||||
) # Sección sin nombre específico
|
)
|
||||||
if nested_sections:
|
if nested_sections:
|
||||||
# Llamada recursiva
|
|
||||||
member_info["children"] = parse_interface_members(nested_sections)
|
member_info["children"] = parse_interface_members(nested_sections)
|
||||||
|
|
||||||
# --- Arrays ---
|
|
||||||
# Buscar elementos <Subelement> para valores iniciales de array
|
|
||||||
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
|
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
|
||||||
subelements = member.xpath("./iface:Subelement", namespaces=ns)
|
subelements = member.xpath("./iface:Subelement", namespaces=ns)
|
||||||
for sub in subelements:
|
for sub in subelements:
|
||||||
path = sub.get("Path") # Path es el índice: '0', '1', '0,0', etc.
|
path = sub.get("Path")
|
||||||
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
|
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
|
||||||
if path and sub_start_value_node:
|
if path and sub_start_value_node:
|
||||||
constant_name = sub_start_value_node[0].get("ConstantName")
|
constant_name = sub_start_value_node[0].get("ConstantName")
|
||||||
|
@ -454,25 +365,23 @@ def parse_interface_members(member_elements):
|
||||||
else (
|
else (
|
||||||
sub_start_value_node[0].text
|
sub_start_value_node[0].text
|
||||||
if sub_start_value_node[0].text is not None
|
if sub_start_value_node[0].text is not None
|
||||||
else ""
|
else None
|
||||||
)
|
)
|
||||||
)
|
) # Devolver None si está vacío
|
||||||
member_info["array_elements"][path] = value
|
member_info["array_elements"][path] = value
|
||||||
# Parsear comentario del subelemento si es necesario
|
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
|
||||||
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
|
if path and sub_comment_node:
|
||||||
if path and sub_comment_node:
|
sub_comment_text = get_multilingual_text(
|
||||||
sub_comment_text = get_multilingual_text(sub_comment_node[0])
|
sub_comment_node[0]
|
||||||
# ¿Cómo guardar comentario de subelemento? Podría ser un dict en array_elements
|
) # Usa la función robusta
|
||||||
if isinstance(member_info["array_elements"].get(path), dict):
|
if isinstance(member_info["array_elements"].get(path), dict):
|
||||||
member_info["array_elements"][path][
|
member_info["array_elements"][path][
|
||||||
"comment"
|
"comment"
|
||||||
] = sub_comment_text
|
] = sub_comment_text
|
||||||
else: # Si solo estaba el valor, convertir a dict
|
else:
|
||||||
current_val = member_info["array_elements"].get(path)
|
member_info["array_elements"][path] = {
|
||||||
member_info["array_elements"][path] = {
|
"value": member_info["array_elements"].get(path),
|
||||||
"value": current_val,
|
"comment": sub_comment_text,
|
||||||
"comment": sub_comment_text,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
members_data.append(member_info)
|
members_data.append(member_info)
|
||||||
return members_data
|
return members_data
|
||||||
|
|
|
@ -128,6 +128,13 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Usar la ruta absoluta para los scripts hijos
|
# Usar la ruta absoluta para los scripts hijos
|
||||||
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
||||||
|
|
||||||
|
# Derivar nombres esperados para archivos intermedios (para depuración)
|
||||||
|
xml_base_name = os.path.splitext(os.path.basename(absolute_xml_filepath))[0]
|
||||||
|
xml_dir = os.path.dirname(absolute_xml_filepath)
|
||||||
|
parsing_dir = os.path.join(xml_dir, "parsing")
|
||||||
|
expected_json_file = os.path.join(parsing_dir, f"{xml_base_name}.json")
|
||||||
|
expected_processed_json = os.path.join(parsing_dir, f"{xml_base_name}_processed.json")
|
||||||
|
|
||||||
# Ejecutar los scripts en secuencia
|
# Ejecutar los scripts en secuencia
|
||||||
success = True
|
success = True
|
||||||
|
|
|
@ -7,8 +7,8 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
import importlib
|
import importlib
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from collections import defaultdict # Puede ser necesario si load_parsers la usa
|
from collections import defaultdict
|
||||||
import copy # Puede ser necesario si load_parsers la usa
|
import copy
|
||||||
|
|
||||||
# Importar funciones comunes y namespaces desde el nuevo módulo de utils
|
# Importar funciones comunes y namespaces desde el nuevo módulo de utils
|
||||||
try:
|
try:
|
||||||
|
@ -22,8 +22,118 @@ except ImportError as e:
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- NUEVAS FUNCIONES DE PARSEO para UDT y Tag Table ---
|
||||||
|
|
||||||
# --- Cargador Dinámico de Parsers ---
|
def parse_udt(udt_element):
|
||||||
|
"""Parsea un elemento <SW.Types.PlcStruct> (UDT)."""
|
||||||
|
print(" -> Detectado: PlcStruct (UDT)")
|
||||||
|
block_data = {
|
||||||
|
"block_name": "UnknownUDT",
|
||||||
|
"block_type": "PlcUDT", # Identificador para x3
|
||||||
|
"language": "UDT", # Lenguaje específico
|
||||||
|
"interface": {},
|
||||||
|
"networks": [], # Los UDTs no tienen redes
|
||||||
|
"block_comment": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extraer nombre y comentario del UDT (similar a como se hace con bloques)
|
||||||
|
attribute_list_node = udt_element.xpath("./AttributeList")
|
||||||
|
if attribute_list_node:
|
||||||
|
attr_list = attribute_list_node[0]
|
||||||
|
name_node = attr_list.xpath("./Name/text()")
|
||||||
|
block_data["block_name"] = name_node[0].strip() if name_node else "UnknownUDT"
|
||||||
|
# Comentario del UDT
|
||||||
|
comment_node_list = udt_element.xpath("./ObjectList/MultilingualText[@CompositionName='Comment']")
|
||||||
|
if comment_node_list:
|
||||||
|
block_data["block_comment"] = get_multilingual_text(comment_node_list[0])
|
||||||
|
else: # Fallback
|
||||||
|
comment_attr_node = attr_list.xpath("../ObjectList/MultilingualText[@CompositionName='Comment']") # Buscar desde el padre
|
||||||
|
if comment_attr_node :
|
||||||
|
block_data["block_comment"] = get_multilingual_text(comment_attr_node[0])
|
||||||
|
|
||||||
|
|
||||||
|
# Extraer interfaz (miembros)
|
||||||
|
# La interfaz de un UDT suele estar directamente en <Interface><Sections><Section Name="None">
|
||||||
|
interface_node_list = udt_element.xpath(
|
||||||
|
"./AttributeList/Interface/iface:Sections/iface:Section[@Name='None']", namespaces=ns
|
||||||
|
)
|
||||||
|
if interface_node_list:
|
||||||
|
section_node = interface_node_list[0]
|
||||||
|
members_in_section = section_node.xpath("./iface:Member", namespaces=ns)
|
||||||
|
if members_in_section:
|
||||||
|
# Usar la función existente para parsear miembros
|
||||||
|
block_data["interface"]["None"] = parse_interface_members(members_in_section)
|
||||||
|
else:
|
||||||
|
print(f"Advertencia: Sección 'None' encontrada en UDT '{block_data['block_name']}' pero sin miembros.")
|
||||||
|
else:
|
||||||
|
# Intentar buscar interfaz directamente si no está en AttributeList (menos común)
|
||||||
|
interface_node_direct = udt_element.xpath(
|
||||||
|
".//iface:Interface/iface:Sections/iface:Section[@Name='None']", namespaces=ns
|
||||||
|
)
|
||||||
|
if interface_node_direct:
|
||||||
|
section_node = interface_node_direct[0]
|
||||||
|
members_in_section = section_node.xpath("./iface:Member", namespaces=ns)
|
||||||
|
if members_in_section:
|
||||||
|
block_data["interface"]["None"] = parse_interface_members(members_in_section)
|
||||||
|
else:
|
||||||
|
print(f"Advertencia: Sección 'None' encontrada directamente en UDT '{block_data['block_name']}' pero sin miembros.")
|
||||||
|
else:
|
||||||
|
print(f"Advertencia: No se encontró la sección 'None' de la interfaz para UDT '{block_data['block_name']}'.")
|
||||||
|
|
||||||
|
|
||||||
|
if not block_data["interface"]:
|
||||||
|
print(f"Advertencia: No se pudo extraer la interfaz del UDT '{block_data['block_name']}'.")
|
||||||
|
|
||||||
|
return block_data
|
||||||
|
|
||||||
|
def parse_tag_table(tag_table_element):
|
||||||
|
"""Parsea un elemento <SW.Tags.PlcTagTable>."""
|
||||||
|
print(" -> Detectado: PlcTagTable")
|
||||||
|
table_data = {
|
||||||
|
"block_name": "UnknownTagTable",
|
||||||
|
"block_type": "PlcTagTable", # Identificador para x3
|
||||||
|
"language": "TagTable", # Lenguaje específico
|
||||||
|
"tags": [],
|
||||||
|
"networks": [], # Las Tag Tables no tienen redes
|
||||||
|
"block_comment": "", # Las tablas de tags no suelen tener comentario de bloque
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extraer nombre de la tabla
|
||||||
|
attribute_list_node = tag_table_element.xpath("./AttributeList")
|
||||||
|
if attribute_list_node:
|
||||||
|
name_node = attribute_list_node[0].xpath("./Name/text()")
|
||||||
|
table_data["block_name"] = name_node[0].strip() if name_node else "UnknownTagTable"
|
||||||
|
|
||||||
|
# Extraer tags
|
||||||
|
tag_elements = tag_table_element.xpath("./ObjectList/SW.Tags.PlcTag")
|
||||||
|
print(f" - Encontrados {len(tag_elements)} tags.")
|
||||||
|
for tag_elem in tag_elements:
|
||||||
|
tag_info = {
|
||||||
|
"name": "UnknownTag",
|
||||||
|
"datatype": "Unknown",
|
||||||
|
"address": None,
|
||||||
|
"comment": ""
|
||||||
|
}
|
||||||
|
tag_attr_list = tag_elem.xpath("./AttributeList")
|
||||||
|
if tag_attr_list:
|
||||||
|
attr_list = tag_attr_list[0]
|
||||||
|
name_node = attr_list.xpath("./Name/text()")
|
||||||
|
tag_info["name"] = name_node[0].strip() if name_node else "UnknownTag"
|
||||||
|
dtype_node = attr_list.xpath("./DataTypeName/text()")
|
||||||
|
tag_info["datatype"] = dtype_node[0].strip() if dtype_node else "Unknown"
|
||||||
|
addr_node = attr_list.xpath("./LogicalAddress/text()")
|
||||||
|
tag_info["address"] = addr_node[0].strip() if addr_node else None
|
||||||
|
|
||||||
|
# Extraer comentario del tag
|
||||||
|
comment_node_list = tag_elem.xpath("./ObjectList/MultilingualText[@CompositionName='Comment']")
|
||||||
|
if comment_node_list:
|
||||||
|
tag_info["comment"] = get_multilingual_text(comment_node_list[0])
|
||||||
|
|
||||||
|
table_data["tags"].append(tag_info)
|
||||||
|
|
||||||
|
return table_data
|
||||||
|
|
||||||
|
# --- Cargador Dinámico de Parsers (sin cambios) ---
|
||||||
def load_parsers(parsers_dir="parsers"):
|
def load_parsers(parsers_dir="parsers"):
|
||||||
"""
|
"""
|
||||||
Escanea el directorio de parsers, importa módulos y construye
|
Escanea el directorio de parsers, importa módulos y construye
|
||||||
|
@ -35,7 +145,7 @@ def load_parsers(parsers_dir="parsers"):
|
||||||
parsers_dir_path = os.path.join(script_dir, parsers_dir)
|
parsers_dir_path = os.path.join(script_dir, parsers_dir)
|
||||||
if not os.path.isdir(parsers_dir_path):
|
if not os.path.isdir(parsers_dir_path):
|
||||||
print(f"Error: Directorio de parsers no encontrado: '{parsers_dir_path}'")
|
print(f"Error: Directorio de parsers no encontrado: '{parsers_dir_path}'")
|
||||||
return parser_map # Devuelve mapa vacío
|
return parser_map # Devuelve mapa vacío
|
||||||
|
|
||||||
print(f"Cargando parsers desde: '{parsers_dir_path}'")
|
print(f"Cargando parsers desde: '{parsers_dir_path}'")
|
||||||
parsers_package = os.path.basename(parsers_dir)
|
parsers_package = os.path.basename(parsers_dir)
|
||||||
|
@ -48,10 +158,8 @@ def load_parsers(parsers_dir="parsers"):
|
||||||
and filename.endswith(".py")
|
and filename.endswith(".py")
|
||||||
and filename not in ["__init__.py", "parser_utils.py"]
|
and filename not in ["__init__.py", "parser_utils.py"]
|
||||||
):
|
):
|
||||||
module_name_rel = filename[:-3] # Nombre sin .py (e.g., parse_lad_fbd)
|
module_name_rel = filename[:-3] # Nombre sin .py (e.g., parse_lad_fbd)
|
||||||
full_module_name = (
|
full_module_name = f"{parsers_package}.{module_name_rel}" # e.g., parsers.parse_lad_fbd
|
||||||
f"{parsers_package}.{module_name_rel}" # e.g., parsers.parse_lad_fbd
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
# Importar el módulo dinámicamente
|
# Importar el módulo dinámicamente
|
||||||
module = importlib.import_module(full_module_name)
|
module = importlib.import_module(full_module_name)
|
||||||
|
@ -73,7 +181,7 @@ def load_parsers(parsers_dir="parsers"):
|
||||||
if isinstance(languages, list) and callable(parser_func):
|
if isinstance(languages, list) and callable(parser_func):
|
||||||
# Añadir la función al mapa para cada lenguaje que soporta
|
# Añadir la función al mapa para cada lenguaje que soporta
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
lang_upper = lang.upper() # Usar mayúsculas como clave
|
lang_upper = lang.upper() # Usar mayúsculas como clave
|
||||||
if lang_upper in parser_map:
|
if lang_upper in parser_map:
|
||||||
print(
|
print(
|
||||||
f" Advertencia: Parser para '{lang_upper}' en {full_module_name} sobrescribe definición anterior."
|
f" Advertencia: Parser para '{lang_upper}' en {full_module_name} sobrescribe definición anterior."
|
||||||
|
@ -105,360 +213,210 @@ def load_parsers(parsers_dir="parsers"):
|
||||||
print(f"Lenguajes soportados: {list(parser_map.keys())}")
|
print(f"Lenguajes soportados: {list(parser_map.keys())}")
|
||||||
return parser_map
|
return parser_map
|
||||||
|
|
||||||
|
# --- Función Principal de Conversión (MODIFICADA) ---
|
||||||
# --- Función Principal de Conversión (Refactorizada) ---
|
|
||||||
def convert_xml_to_json(xml_filepath, json_filepath, parser_map):
|
def convert_xml_to_json(xml_filepath, json_filepath, parser_map):
|
||||||
"""Convierte XML a JSON usando los parsers cargados dinámicamente."""
|
"""Convierte XML a JSON, detectando tipo de bloque (FC/FB/OB/DB/UDT/TagTable)."""
|
||||||
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
|
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
|
||||||
if not os.path.exists(xml_filepath):
|
if not os.path.exists(xml_filepath):
|
||||||
print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'")
|
print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'")
|
||||||
return False # Indicar fallo
|
return False # Indicar fallo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("Paso 1: Parseando archivo XML...")
|
print("Paso 1: Parseando archivo XML...")
|
||||||
# Usar un parser que quite texto en blanco para simplificar XPath
|
parser = etree.XMLParser(remove_blank_text=True, recover=True) # recover=True puede ayudar
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
|
||||||
tree = etree.parse(xml_filepath, parser)
|
tree = etree.parse(xml_filepath, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
print("Paso 1: Parseo XML completado.")
|
print("Paso 1: Parseo XML completado.")
|
||||||
|
|
||||||
# --- Buscar bloque principal (FC, FB, GlobalDB, OB) ---
|
result = None # Inicializar resultado
|
||||||
print("Paso 2: Buscando el bloque SW.Blocks.FC/FB/GlobalDB/OB...")
|
|
||||||
# Usar local-name() para ignorar namespaces en esta búsqueda inicial
|
# --- Detección del tipo de bloque/objeto principal ---
|
||||||
block_list = root.xpath(
|
print("Paso 2: Detectando tipo de objeto principal...")
|
||||||
"//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB' or local-name()='SW.Blocks.OB']"
|
|
||||||
)
|
# Buscar UDT
|
||||||
if (
|
udt_element = root.find(".//SW.Types.PlcStruct", namespaces=root.nsmap)
|
||||||
not block_list
|
if udt_element is not None:
|
||||||
): # Intentar con namespace si el anterior falla (menos probable)
|
result = parse_udt(udt_element)
|
||||||
ns_doc = {
|
|
||||||
"doc": "http://www.siemens.com/automation/Openness/SW/Document/v5"
|
# Buscar Tag Table si no es UDT
|
||||||
} # Asumiendo este namespace
|
if result is None:
|
||||||
|
tag_table_element = root.find(".//SW.Tags.PlcTagTable", namespaces=root.nsmap)
|
||||||
|
if tag_table_element is not None:
|
||||||
|
result = parse_tag_table(tag_table_element)
|
||||||
|
|
||||||
|
# Buscar bloque FC/FB/OB/GlobalDB si no es UDT ni Tag Table
|
||||||
|
if result is None:
|
||||||
|
print("Paso 2: No es UDT ni Tag Table. Buscando SW.Blocks.* ...")
|
||||||
|
# Usar local-name() para ignorar namespaces en esta búsqueda inicial
|
||||||
block_list = root.xpath(
|
block_list = root.xpath(
|
||||||
"//doc:SW.Blocks.FC | //doc:SW.Blocks.FB | //doc:SW.Blocks.GlobalDB | //doc:SW.Blocks.OB",
|
"//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB' or local-name()='SW.Blocks.OB']"
|
||||||
namespaces=ns_doc,
|
|
||||||
)
|
)
|
||||||
|
# (Resto de la lógica de detección de bloques FC/FB/OB/DB como estaba antes...)
|
||||||
|
block_type_found = None
|
||||||
|
the_block = None
|
||||||
|
|
||||||
block_type_found = None
|
if block_list:
|
||||||
the_block = None
|
the_block = block_list[0]
|
||||||
|
block_tag_name = etree.QName(the_block.tag).localname
|
||||||
if block_list:
|
if block_tag_name == "SW.Blocks.FC": block_type_found = "FC"
|
||||||
the_block = block_list[0]
|
elif block_tag_name == "SW.Blocks.FB": block_type_found = "FB"
|
||||||
block_tag_name = etree.QName(
|
elif block_tag_name == "SW.Blocks.GlobalDB": block_type_found = "GlobalDB"
|
||||||
the_block.tag
|
elif block_tag_name == "SW.Blocks.OB": block_type_found = "OB"
|
||||||
).localname # Obtener nombre local sin ns
|
print(f"Paso 2b: Bloque {block_tag_name} (Tipo: {block_type_found}) encontrado (ID={the_block.get('ID')}).")
|
||||||
if block_tag_name == "SW.Blocks.FC":
|
|
||||||
block_type_found = "FC"
|
|
||||||
elif block_tag_name == "SW.Blocks.FB":
|
|
||||||
block_type_found = "FB"
|
|
||||||
elif block_tag_name == "SW.Blocks.GlobalDB":
|
|
||||||
block_type_found = "GlobalDB"
|
|
||||||
elif block_tag_name == "SW.Blocks.OB":
|
|
||||||
block_type_found = "OB"
|
|
||||||
print(
|
|
||||||
f"Paso 2: Bloque {block_tag_name} (Tipo: {block_type_found}) encontrado (ID={the_block.get('ID')})."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Error Crítico: No se encontró el elemento raíz del bloque (<SW.Blocks.FC/FB/GlobalDB/OB>)."
|
|
||||||
)
|
|
||||||
# Podríamos intentar buscar cualquier SW.Blocks.* como fallback?
|
|
||||||
any_block = root.xpath("//*[starts-with(local-name(), 'SW.Blocks.')]")
|
|
||||||
if any_block:
|
|
||||||
print(
|
|
||||||
f"Advertencia: Se encontró un bloque genérico: {etree.QName(any_block[0].tag).localname}. Intentando continuar..."
|
|
||||||
)
|
|
||||||
the_block = any_block[0]
|
|
||||||
block_type_found = "Unknown" # Marcar como desconocido
|
|
||||||
else:
|
else:
|
||||||
return False # Fallo si no se encuentra ningún bloque
|
print("Error Crítico: No se encontró el elemento raíz del bloque (<SW.Blocks.FC/FB/GlobalDB/OB>) ni UDT ni Tag Table.")
|
||||||
|
return False # Fallo si no se encuentra ningún objeto principal
|
||||||
|
|
||||||
# --- Extraer atributos del bloque ---
|
# --- Si es FC/FB/OB/DB, continuar con el parseo original ---
|
||||||
print("Paso 3: Extrayendo atributos del bloque...")
|
if the_block is not None:
|
||||||
# AttributeList generalmente no tiene namespace propio
|
print("Paso 3: Extrayendo atributos del bloque...")
|
||||||
attribute_list_node = the_block.xpath("./AttributeList")
|
# (Extracción de atributos Name, Number, Language como antes...)
|
||||||
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
|
attribute_list_node = the_block.xpath("./AttributeList")
|
||||||
if attribute_list_node:
|
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
|
||||||
attr_list = attribute_list_node[0]
|
if attribute_list_node:
|
||||||
# Name, Number, ProgrammingLanguage están directamente bajo AttributeList
|
attr_list = attribute_list_node[0]
|
||||||
name_node = attr_list.xpath("./Name/text()")
|
name_node = attr_list.xpath("./Name/text()")
|
||||||
block_name_val = name_node[0].strip() if name_node else block_name_val
|
block_name_val = name_node[0].strip() if name_node else block_name_val
|
||||||
num_node = attr_list.xpath("./Number/text()")
|
num_node = attr_list.xpath("./Number/text()")
|
||||||
try:
|
try: block_number_val = int(num_node[0]) if num_node else None
|
||||||
block_number_val = int(num_node[0]) if num_node else None
|
except (ValueError, TypeError): block_number_val = None
|
||||||
except (ValueError, TypeError):
|
lang_node = attr_list.xpath("./ProgrammingLanguage/text()")
|
||||||
block_number_val = None # Mantener como None si no es entero
|
block_lang_val = (lang_node[0].strip() if lang_node else ("DB" if block_type_found == "GlobalDB" else "Unknown"))
|
||||||
lang_node = attr_list.xpath("./ProgrammingLanguage/text()")
|
print(f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje Bloque='{block_lang_val}'")
|
||||||
block_lang_val = (
|
|
||||||
lang_node[0].strip()
|
|
||||||
if lang_node
|
|
||||||
else ("DB" if block_type_found == "GlobalDB" else "Unknown")
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje Bloque='{block_lang_val}'"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}."
|
|
||||||
)
|
|
||||||
if block_type_found == "GlobalDB":
|
|
||||||
block_lang_val = "DB" # Asignar lenguaje DB si es GlobalDB
|
|
||||||
|
|
||||||
# --- Extraer comentario del bloque ---
|
|
||||||
# ObjectList -> MultilingualText[@CompositionName='Comment']
|
|
||||||
block_comment_val = ""
|
|
||||||
# ObjectList tampoco suele tener namespace propio
|
|
||||||
comment_node_list = the_block.xpath(
|
|
||||||
"./ObjectList/MultilingualText[@CompositionName='Comment']"
|
|
||||||
)
|
|
||||||
if comment_node_list:
|
|
||||||
# Usar la función de utils que maneja los namespaces internos de MultilingualText
|
|
||||||
block_comment_val = get_multilingual_text(comment_node_list[0])
|
|
||||||
print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'")
|
|
||||||
else:
|
|
||||||
# Intentar buscar comentario en AttributeList como fallback?
|
|
||||||
comment_attr_node = the_block.xpath("./AttributeList/Comment")
|
|
||||||
if comment_attr_node:
|
|
||||||
block_comment_val = get_multilingual_text(comment_attr_node[0])
|
|
||||||
print(
|
|
||||||
f"Paso 3b (Fallback): Comentario bloque encontrado en AttributeList: '{block_comment_val[:50]}...'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Crear diccionario resultado ---
|
|
||||||
result = {
|
|
||||||
"block_name": block_name_val,
|
|
||||||
"block_number": block_number_val,
|
|
||||||
"language": block_lang_val, # Lenguaje general del bloque
|
|
||||||
"block_type": block_type_found,
|
|
||||||
"block_comment": block_comment_val,
|
|
||||||
"interface": {},
|
|
||||||
"networks": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Extraer interfaz ---
|
|
||||||
print("Paso 4: Extrayendo la interfaz del bloque...")
|
|
||||||
# Interface está dentro de AttributeList (sin ns propio), pero sus hijos usan 'iface'
|
|
||||||
interface_node_list = (
|
|
||||||
attribute_list_node[0].xpath("./Interface") if attribute_list_node else []
|
|
||||||
)
|
|
||||||
|
|
||||||
if interface_node_list:
|
|
||||||
interface_node = interface_node_list[0]
|
|
||||||
print("Paso 4: Nodo Interface encontrado.")
|
|
||||||
# Sections/Section usan namespace iface
|
|
||||||
all_sections = interface_node.xpath(".//iface:Section", namespaces=ns)
|
|
||||||
if all_sections:
|
|
||||||
processed_sections = set()
|
|
||||||
for section in all_sections:
|
|
||||||
section_name = section.get(
|
|
||||||
"Name"
|
|
||||||
) # Input, Output, Static, Temp, etc.
|
|
||||||
if not section_name or section_name in processed_sections:
|
|
||||||
continue
|
|
||||||
# Los Member dentro de Section usan namespace iface
|
|
||||||
members_in_section = section.xpath("./iface:Member", namespaces=ns)
|
|
||||||
if members_in_section:
|
|
||||||
# Usar la función de utils para parsear miembros
|
|
||||||
result["interface"][section_name] = parse_interface_members(
|
|
||||||
members_in_section
|
|
||||||
)
|
|
||||||
processed_sections.add(section_name)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Advertencia: Nodo Interface no contiene secciones <iface:Section>."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not result["interface"]:
|
|
||||||
print(
|
|
||||||
"Advertencia: Interface encontrada pero sin secciones procesables."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Manejo especial para DB si no hay <Interface> explícita
|
|
||||||
if block_type_found == "GlobalDB":
|
|
||||||
# Buscar directamente la sección Static (que usa namespace iface)
|
|
||||||
static_members = the_block.xpath(
|
|
||||||
".//iface:Section[@Name='Static']/iface:Member", namespaces=ns
|
|
||||||
)
|
|
||||||
if static_members:
|
|
||||||
print(
|
|
||||||
"Paso 4: Encontrada sección Static para GlobalDB (sin nodo Interface)."
|
|
||||||
)
|
|
||||||
result["interface"]["Static"] = parse_interface_members(
|
|
||||||
static_members
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print("Advertencia: No se encontró sección 'Static' para GlobalDB.")
|
print(f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}.")
|
||||||
else:
|
if block_type_found == "GlobalDB": block_lang_val = "DB"
|
||||||
print(
|
|
||||||
f"Advertencia: No se encontró <Interface> para bloque {block_type_found}."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not result["interface"]:
|
# (Extracción de comentario como antes...)
|
||||||
print("Advertencia: No se pudo extraer información de la interfaz.")
|
block_comment_val = ""
|
||||||
|
comment_node_list = the_block.xpath("./ObjectList/MultilingualText[@CompositionName='Comment']")
|
||||||
|
if comment_node_list: block_comment_val = get_multilingual_text(comment_node_list[0])
|
||||||
|
else: # Fallback
|
||||||
|
comment_attr_node = the_block.xpath("./AttributeList/Comment") # Buscar desde AttributeList
|
||||||
|
if comment_attr_node : block_comment_val = get_multilingual_text(comment_attr_node[0])
|
||||||
|
|
||||||
# --- Procesar redes (CompileUnits) ---
|
print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'")
|
||||||
print("Paso 5: Buscando y PROCESANDO redes (CompileUnits)...")
|
|
||||||
networks_processed_count = 0
|
|
||||||
result["networks"] = []
|
|
||||||
# ObjectList y SW.Blocks.CompileUnit no suelen tener namespace propio
|
|
||||||
object_list_node = the_block.xpath("./ObjectList")
|
|
||||||
|
|
||||||
if object_list_node:
|
# Crear diccionario resultado
|
||||||
compile_units = object_list_node[0].xpath("./SW.Blocks.CompileUnit")
|
result = {
|
||||||
print(
|
"block_name": block_name_val,
|
||||||
f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit."
|
"block_number": block_number_val,
|
||||||
)
|
"language": block_lang_val,
|
||||||
|
"block_type": block_type_found,
|
||||||
|
"block_comment": block_comment_val,
|
||||||
|
"interface": {},
|
||||||
|
"networks": [], # Inicializar networks aquí
|
||||||
|
}
|
||||||
|
|
||||||
# --- BUCLE PRINCIPAL DE PARSEO DE REDES (MODIFICADO) ---
|
# (Extracción de interfaz como antes...)
|
||||||
for network_elem in compile_units:
|
print("Paso 4: Extrayendo la interfaz del bloque...")
|
||||||
networks_processed_count += 1
|
interface_node_list = attribute_list_node[0].xpath("./Interface") if attribute_list_node else []
|
||||||
network_id = network_elem.get("ID")
|
if interface_node_list:
|
||||||
if not network_id:
|
interface_node = interface_node_list[0]
|
||||||
print("Advertencia: CompileUnit sin ID, saltando.")
|
all_sections = interface_node.xpath(".//iface:Section", namespaces=ns)
|
||||||
continue
|
if all_sections:
|
||||||
|
processed_sections = set()
|
||||||
|
for section in all_sections:
|
||||||
|
section_name = section.get("Name")
|
||||||
|
if not section_name or section_name in processed_sections: continue
|
||||||
|
members_in_section = section.xpath("./iface:Member", namespaces=ns)
|
||||||
|
if members_in_section:
|
||||||
|
result["interface"][section_name] = parse_interface_members(members_in_section)
|
||||||
|
processed_sections.add(section_name)
|
||||||
|
else: print("Advertencia: Nodo Interface no contiene secciones <iface:Section>.")
|
||||||
|
if not result["interface"]: print("Advertencia: Interface encontrada pero sin secciones procesables.")
|
||||||
|
elif block_type_found == "GlobalDB":
|
||||||
|
static_members = the_block.xpath(".//iface:Section[@Name='Static']/iface:Member", namespaces=ns)
|
||||||
|
if static_members:
|
||||||
|
print("Paso 4: Encontrada sección Static para GlobalDB (sin nodo Interface).")
|
||||||
|
result["interface"]["Static"] = parse_interface_members(static_members)
|
||||||
|
else: print("Advertencia: No se encontró sección 'Static' para GlobalDB.")
|
||||||
|
else: print(f"Advertencia: No se encontró <Interface> para bloque {block_type_found}.")
|
||||||
|
if not result["interface"]: print("Advertencia: No se pudo extraer información de la interfaz.")
|
||||||
|
|
||||||
# Detectar lenguaje de la RED (puede diferir del lenguaje del bloque)
|
# (Procesamiento de redes como antes, SOLO si NO es GlobalDB)
|
||||||
# AttributeList/ProgrammingLanguage sin namespace
|
if block_type_found != "GlobalDB":
|
||||||
network_lang = "LAD" # Default si no se encuentra
|
print("Paso 5: Buscando y PROCESANDO redes (CompileUnits)...")
|
||||||
net_attr_list = network_elem.xpath("./AttributeList")
|
networks_processed_count = 0
|
||||||
if net_attr_list:
|
result["networks"] = []
|
||||||
lang_node = net_attr_list[0].xpath("./ProgrammingLanguage/text()")
|
object_list_node = the_block.xpath("./ObjectList")
|
||||||
if lang_node:
|
if object_list_node:
|
||||||
network_lang = lang_node[0].strip()
|
compile_units = object_list_node[0].xpath("./SW.Blocks.CompileUnit")
|
||||||
|
print(f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit.")
|
||||||
|
|
||||||
print(
|
# Bucle de parseo de redes (igual que antes)
|
||||||
f" - Procesando Red ID={network_id}, Lenguaje Red={network_lang}"
|
for network_elem in compile_units:
|
||||||
)
|
networks_processed_count += 1
|
||||||
|
network_id = network_elem.get("ID")
|
||||||
|
if not network_id: continue
|
||||||
|
network_lang = "LAD"
|
||||||
|
net_attr_list = network_elem.xpath("./AttributeList")
|
||||||
|
if net_attr_list:
|
||||||
|
lang_node = net_attr_list[0].xpath("./ProgrammingLanguage/text()")
|
||||||
|
if lang_node: network_lang = lang_node[0].strip()
|
||||||
|
print(f" - Procesando Red ID={network_id}, Lenguaje Red={network_lang}")
|
||||||
|
parser_func = parser_map.get(network_lang.upper())
|
||||||
|
parsed_network_data = None
|
||||||
|
if parser_func:
|
||||||
|
try:
|
||||||
|
parsed_network_data = parser_func(network_elem)
|
||||||
|
except Exception as e_parse:
|
||||||
|
print(f" ERROR durante el parseo de Red {network_id} ({network_lang}): {e_parse}")
|
||||||
|
traceback.print_exc()
|
||||||
|
parsed_network_data = {"id": network_id, "language": network_lang, "logic": [], "error": f"Parser failed: {e_parse}"}
|
||||||
|
else:
|
||||||
|
print(f" Advertencia: Lenguaje de red '{network_lang}' no soportado.")
|
||||||
|
parsed_network_data = {"id": network_id, "language": network_lang, "logic": [], "error": f"Unsupported language: {network_lang}"}
|
||||||
|
|
||||||
# --- Llamada al Parser Dinámico ---
|
if parsed_network_data:
|
||||||
parser_func = parser_map.get(
|
title_element = network_elem.xpath(".//iface:MultilingualText[@CompositionName='Title']",namespaces=ns)
|
||||||
network_lang.upper()
|
parsed_network_data["title"] = (get_multilingual_text(title_element[0]) if title_element else f"Network {network_id}")
|
||||||
) # Buscar parser por lenguaje
|
comment_elem_net = network_elem.xpath("./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns)
|
||||||
parsed_network_data = None
|
if not comment_elem_net: comment_elem_net = network_elem.xpath(".//MultilingualText[@CompositionName='Comment']", namespaces=ns) # Fallback
|
||||||
|
parsed_network_data["comment"] = (get_multilingual_text(comment_elem_net[0]) if comment_elem_net else "")
|
||||||
|
result["networks"].append(parsed_network_data)
|
||||||
|
|
||||||
if parser_func:
|
if networks_processed_count == 0: print(f"Advertencia: ObjectList para {block_type_found} sin SW.Blocks.CompileUnit.")
|
||||||
try:
|
else: print(f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}.")
|
||||||
# Llamar a la función de parseo específica del lenguaje
|
else: # Es GlobalDB
|
||||||
# Pasar el elemento XML de la red y los namespaces
|
print("Paso 5: Saltando procesamiento de redes para GlobalDB.")
|
||||||
parsed_network_data = parser_func(
|
|
||||||
network_elem
|
|
||||||
) # Pasar ns ya no es necesario si están en utils
|
|
||||||
except Exception as e_parse:
|
|
||||||
print(
|
|
||||||
f" ERROR durante el parseo de Red {network_id} ({network_lang}): {e_parse}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
|
||||||
# Crear diccionario de error si el parser falla
|
|
||||||
parsed_network_data = {
|
|
||||||
"id": network_id,
|
|
||||||
"language": network_lang,
|
|
||||||
"logic": [],
|
|
||||||
"error": f"Parser failed: {e_parse}",
|
|
||||||
}
|
|
||||||
else: # Lenguaje no soportado por ningún parser cargado
|
|
||||||
print(
|
|
||||||
f" Advertencia: Lenguaje de red '{network_lang}' no soportado por los parsers cargados."
|
|
||||||
)
|
|
||||||
parsed_network_data = {
|
|
||||||
"id": network_id,
|
|
||||||
"language": network_lang,
|
|
||||||
"logic": [],
|
|
||||||
"error": f"Unsupported language: {network_lang}",
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Añadir Título y Comentario a la Red Parseada ---
|
|
||||||
if parsed_network_data:
|
|
||||||
# Usar get_multilingual_text de utils
|
|
||||||
title_element = network_elem.xpath(
|
|
||||||
".//iface:MultilingualText[@CompositionName='Title']",
|
|
||||||
namespaces=ns,
|
|
||||||
)
|
|
||||||
parsed_network_data["title"] = (
|
|
||||||
get_multilingual_text(title_element[0])
|
|
||||||
if title_element
|
|
||||||
else f"Network {network_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Buscar comentario específico de la red
|
# --- Escritura del JSON (si se encontró un objeto) ---
|
||||||
comment_elem_net = network_elem.xpath(
|
if result:
|
||||||
"./ObjectList/MultilingualText[@CompositionName='Comment']",
|
print("Paso 6: Escribiendo el resultado en el archivo JSON...")
|
||||||
namespaces=ns,
|
# Validaciones finales
|
||||||
)
|
if result.get("block_type") not in ["PlcUDT", "PlcTagTable"] and not result["interface"]:
|
||||||
if not comment_elem_net: # Fallback
|
print("ADVERTENCIA FINAL: 'interface' está vacía en el JSON.")
|
||||||
comment_elem_net = network_elem.xpath(
|
if result.get("block_type") not in ["PlcUDT", "PlcTagTable", "GlobalDB"] and not result["networks"]:
|
||||||
".//MultilingualText[@CompositionName='Comment']",
|
print("ADVERTENCIA FINAL: 'networks' está vacía en el JSON.")
|
||||||
namespaces=ns,
|
|
||||||
)
|
|
||||||
|
|
||||||
parsed_network_data["comment"] = (
|
try:
|
||||||
get_multilingual_text(comment_elem_net[0])
|
with open(json_filepath, "w", encoding="utf-8") as f:
|
||||||
if comment_elem_net
|
json.dump(result, f, indent=4, ensure_ascii=False)
|
||||||
else ""
|
print("Paso 6: Escritura JSON completada.")
|
||||||
)
|
print(f"Conversión finalizada. JSON guardado en: '{os.path.relpath(json_filepath)}'")
|
||||||
|
return True # Indicar éxito
|
||||||
|
|
||||||
# Añadir la red procesada (o con error) al resultado
|
except IOError as e: print(f"Error Crítico: No se pudo escribir JSON en '{json_filepath}'. Error: {e}"); return False
|
||||||
result["networks"].append(parsed_network_data)
|
except TypeError as e: print(f"Error Crítico: Problema al serializar a JSON. Error: {e}"); return False
|
||||||
|
|
||||||
# --- Fin Bucle Redes ---
|
|
||||||
|
|
||||||
if networks_processed_count == 0 and block_type_found != "GlobalDB":
|
|
||||||
print(
|
|
||||||
f"Advertencia: ObjectList para {block_type_found} sin SW.Blocks.CompileUnit."
|
|
||||||
)
|
|
||||||
elif block_type_found == "GlobalDB":
|
|
||||||
print("Paso 5: Saltando búsqueda de CompileUnits para GlobalDB (esperado).")
|
|
||||||
else:
|
else:
|
||||||
print(
|
print("Error Crítico: No se pudo determinar el tipo de objeto principal en el XML.")
|
||||||
f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}."
|
return False
|
||||||
)
|
|
||||||
|
|
||||||
# --- Escribir JSON ---
|
|
||||||
print("Paso 6: Escribiendo el resultado en el archivo JSON...")
|
|
||||||
# Validaciones finales opcionales
|
|
||||||
if not result["interface"]:
|
|
||||||
print("ADVERTENCIA FINAL: 'interface' está vacía en el JSON.")
|
|
||||||
if not result["networks"] and block_type_found != "GlobalDB":
|
|
||||||
print("ADVERTENCIA FINAL: 'networks' está vacía en el JSON.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(json_filepath, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(result, f, indent=4, ensure_ascii=False)
|
|
||||||
print("Paso 6: Escritura JSON completada.")
|
|
||||||
print(
|
|
||||||
f"Conversión finalizada. JSON guardado en: '{os.path.relpath(json_filepath)}'"
|
|
||||||
)
|
|
||||||
return True # Indicar éxito
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
print(
|
|
||||||
f"Error Crítico: No se pudo escribir JSON en '{json_filepath}'. Error: {e}"
|
|
||||||
)
|
|
||||||
return False # Indicar fallo
|
|
||||||
except TypeError as e:
|
|
||||||
print(
|
|
||||||
f"Error Crítico: Problema al serializar a JSON (posiblemente datos no serializables). Error: {e}"
|
|
||||||
)
|
|
||||||
# Opcional: Imprimir una versión parcial o depurar 'result'
|
|
||||||
# print("--- Datos antes de JSON DUMP (parcial) ---")
|
|
||||||
# try: print(json.dumps({k: v for k, v in result.items() if k != 'networks'}, indent=2)) # Imprimir sin redes
|
|
||||||
# except: print("No se pudo imprimir datos parciales.")
|
|
||||||
return False # Indicar fallo
|
|
||||||
|
|
||||||
except etree.XMLSyntaxError as e:
|
except etree.XMLSyntaxError as e:
|
||||||
print(
|
print(f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}")
|
||||||
f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}"
|
return False # Indicar fallo
|
||||||
)
|
|
||||||
return False # Indicar fallo
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error Crítico: Error inesperado durante la conversión: {e}")
|
print(f"Error Crítico: Error inesperado durante la conversión: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False # Indicar fallo
|
return False # Indicar fallo
|
||||||
|
|
||||||
|
|
||||||
# --- Punto de Entrada Principal (__main__) ---
|
# --- Punto de Entrada Principal (__main__) ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Convert Simatic XML (LAD/FBD/SCL/STL/OB/DB) to simplified JSON using dynamic parsers."
|
description="Convert Simatic XML (FC/FB/OB/DB/UDT/TagTable) to simplified JSON using dynamic parsers." # Actualizado
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"xml_filepath",
|
"xml_filepath",
|
||||||
|
@ -468,38 +426,31 @@ if __name__ == "__main__":
|
||||||
xml_input_file = args.xml_filepath
|
xml_input_file = args.xml_filepath
|
||||||
|
|
||||||
if not os.path.exists(xml_input_file):
|
if not os.path.exists(xml_input_file):
|
||||||
print(
|
print(f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'", file=sys.stderr)
|
||||||
f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Cargar Parsers Dinámicamente ---
|
# --- Cargar Parsers Dinámicamente ---
|
||||||
loaded_parsers = load_parsers()
|
loaded_parsers = load_parsers() # Carga parsers LAD/FBD/STL/SCL
|
||||||
if not loaded_parsers:
|
if not loaded_parsers:
|
||||||
print("Error Crítico (x1): No se cargaron parsers. Abortando.", file=sys.stderr)
|
# Continuar incluso sin parsers de red, ya que podríamos estar parseando UDT/TagTable
|
||||||
sys.exit(1)
|
print("Advertencia (x1): No se cargaron parsers de red. Se continuará para UDT/TagTable/DB.")
|
||||||
|
#sys.exit(1) # Ya no salimos si no hay parsers de red
|
||||||
|
|
||||||
# Derivar nombre de salida JSON
|
# Derivar nombre de salida JSON
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
|
||||||
output_dir = os.path.dirname(xml_input_file)
|
base_dir = os.path.dirname(xml_input_file)
|
||||||
# Asegurarse que el directorio de salida exista (puede ser el mismo que el de entrada)
|
output_dir = os.path.join(base_dir, "parsing")
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
json_output_file = os.path.join(output_dir, f"{xml_filename_base}_simplified.json")
|
json_output_file = os.path.join(output_dir, f"{xml_filename_base}.json")
|
||||||
|
|
||||||
print(
|
print(f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'")
|
||||||
f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Llamar a la función de conversión principal
|
# Llamar a la función de conversión principal
|
||||||
success = convert_xml_to_json(xml_input_file, json_output_file, loaded_parsers)
|
success = convert_xml_to_json(xml_input_file, json_output_file, loaded_parsers)
|
||||||
|
|
||||||
# Salir con código de error apropiado
|
# Salir con código de error apropiado
|
||||||
if success:
|
if success:
|
||||||
sys.exit(0) # Éxito
|
sys.exit(0) # Éxito
|
||||||
else:
|
else:
|
||||||
print(
|
print(f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.", file=sys.stderr)
|
||||||
f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.",
|
sys.exit(1) # Fallo
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1) # Fallo
|
|
|
@ -7,25 +7,25 @@ import traceback
|
||||||
import re
|
import re
|
||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
import sympy # Import sympy
|
import sympy # Import sympy
|
||||||
|
|
||||||
# Import necessary components from processors directory
|
# Import necessary components from processors directory
|
||||||
from processors.processor_utils import (
|
from processors.processor_utils import (
|
||||||
format_variable_name, # Keep if used outside processors
|
format_variable_name, # Keep if used outside processors
|
||||||
sympy_expr_to_scl, # Needed for IF grouping and maybe others
|
sympy_expr_to_scl, # Needed for IF grouping and maybe others
|
||||||
# get_target_scl_name might be used here? Unlikely.
|
# get_target_scl_name might be used here? Unlikely.
|
||||||
)
|
)
|
||||||
from processors.symbol_manager import SymbolManager # Import the manager
|
from processors.symbol_manager import SymbolManager # Import the manager
|
||||||
|
|
||||||
# --- Constantes y Configuración ---
|
# --- Constantes y Configuración ---
|
||||||
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
|
SCL_SUFFIX = "_sympy_processed"
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||||
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
|
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script"
|
||||||
|
|
||||||
# Global data dictionary
|
# Global data dictionary
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
# --- (Incluye aquí las funciones process_group_ifs y load_processors SIN CAMBIOS) ---
|
# --- (process_group_ifs y load_processors SIN CAMBIOS) ---
|
||||||
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
"""
|
"""
|
||||||
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
||||||
|
@ -203,19 +203,18 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
|
|
||||||
return made_change
|
return made_change
|
||||||
|
|
||||||
|
|
||||||
def load_processors(processors_dir="processors"):
|
def load_processors(processors_dir="processors"):
|
||||||
"""
|
"""
|
||||||
Escanea el directorio, importa módulos, construye el mapa y una lista
|
Escanea el directorio, importa módulos, construye el mapa y una lista
|
||||||
ordenada por prioridad.
|
ordenada por prioridad.
|
||||||
"""
|
"""
|
||||||
processor_map = {}
|
processor_map = {}
|
||||||
processor_list_unsorted = [] # Lista para guardar (priority, type_name, func)
|
processor_list_unsorted = [] # Lista para guardar (priority, type_name, func)
|
||||||
default_priority = 10 # Prioridad si no se define en get_processor_info
|
default_priority = 10 # Prioridad si no se define en get_processor_info
|
||||||
|
|
||||||
if not os.path.isdir(processors_dir):
|
if not os.path.isdir(processors_dir):
|
||||||
print(f"Error: Directorio de procesadores no encontrado: '{processors_dir}'")
|
print(f"Error: Directorio de procesadores no encontrado: '{processors_dir}'")
|
||||||
return processor_map, [] # Devuelve mapa vacío y lista vacía
|
return processor_map, [] # Devuelve mapa vacío y lista vacía
|
||||||
|
|
||||||
print(f"Cargando procesadores desde: '{processors_dir}'")
|
print(f"Cargando procesadores desde: '{processors_dir}'")
|
||||||
processors_package = os.path.basename(processors_dir)
|
processors_package = os.path.basename(processors_dir)
|
||||||
|
@ -300,17 +299,18 @@ def load_processors(processors_dir="processors"):
|
||||||
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
||||||
return processor_map, processor_list_sorted
|
return processor_map, processor_list_sorted
|
||||||
|
|
||||||
# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) ---
|
|
||||||
def process_json_to_scl(json_filepath):
|
# --- Bucle Principal de Procesamiento (MODIFICADO) ---
|
||||||
|
def process_json_to_scl(json_filepath, output_json_filepath):
|
||||||
"""
|
"""
|
||||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
|
||||||
y guarda JSON procesado.
|
y guarda JSON procesado en la ruta especificada.
|
||||||
"""
|
"""
|
||||||
global data
|
global data
|
||||||
|
|
||||||
if not os.path.exists(json_filepath):
|
if not os.path.exists(json_filepath):
|
||||||
print(f"Error: JSON no encontrado: {json_filepath}")
|
print(f"Error: JSON no encontrado: {json_filepath}")
|
||||||
return
|
return False
|
||||||
print(f"Cargando JSON desde: {json_filepath}")
|
print(f"Cargando JSON desde: {json_filepath}")
|
||||||
try:
|
try:
|
||||||
with open(json_filepath, "r", encoding="utf-8") as f:
|
with open(json_filepath, "r", encoding="utf-8") as f:
|
||||||
|
@ -318,78 +318,62 @@ def process_json_to_scl(json_filepath):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al cargar JSON: {e}")
|
print(f"Error al cargar JSON: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return
|
return False
|
||||||
|
|
||||||
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) ---
|
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
|
||||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB
|
block_type = data.get("block_type", "Unknown")
|
||||||
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}")
|
print(f"Procesando bloque tipo: {block_type}")
|
||||||
|
|
||||||
# --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO ---
|
# --- MODIFICADO: SALTAR PROCESAMIENTO PARA DB, UDT, TAG TABLE ---
|
||||||
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable"]: # <-- Comprobar tipos a saltar
|
||||||
print(
|
print(f"INFO: El bloque es {block_type}. Saltando procesamiento lógico de x2.")
|
||||||
"INFO: El bloque es un Data Block (GlobalDB). Saltando procesamiento lógico de x2."
|
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_json_filepath}")
|
||||||
)
|
|
||||||
# Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo)
|
|
||||||
output_filename = json_filepath.replace(
|
|
||||||
"_simplified.json", "_simplified_processed.json"
|
|
||||||
)
|
|
||||||
print(f"Guardando JSON de DB (sin cambios lógicos) en: {output_filename}")
|
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_json_filepath, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
print("Guardado de DB completado.")
|
print(f"Guardado de {block_type} completado.")
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error Crítico al guardar JSON del DB: {e}")
|
print(f"Error Crítico al guardar JSON de {block_type}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return # <<< SALIR TEMPRANO PARA DBs
|
return False
|
||||||
|
|
||||||
# --- SI NO ES DB (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
|
# --- SI NO ES DB/UDT/TAG TABLE (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
|
||||||
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...") # <-- Mensaje actualizado
|
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...")
|
||||||
|
|
||||||
|
# (Carga de procesadores y mapas de acceso SIN CAMBIOS)
|
||||||
script_dir = os.path.dirname(__file__)
|
script_dir = os.path.dirname(__file__)
|
||||||
processors_dir_path = os.path.join(script_dir, "processors")
|
processors_dir_path = os.path.join(script_dir, "processors")
|
||||||
processor_map, sorted_processors = load_processors(processors_dir_path)
|
processor_map, sorted_processors = load_processors(processors_dir_path)
|
||||||
if not processor_map:
|
if not processor_map:
|
||||||
print("Error crítico: No se cargaron procesadores. Abortando.")
|
print("Error crítico: No se cargaron procesadores. Abortando.")
|
||||||
return
|
return False
|
||||||
|
|
||||||
network_access_maps = {}
|
network_access_maps = {}
|
||||||
# Crear mapas de acceso por red (copiado/adaptado de versión anterior)
|
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
net_id = network["id"]
|
net_id = network["id"]
|
||||||
current_access_map = {}
|
current_access_map = {}
|
||||||
for instr in network.get("logic", []):
|
for instr in network.get("logic", []):
|
||||||
for _, source in instr.get("inputs", {}).items():
|
for _, source in instr.get("inputs", {}).items():
|
||||||
sources_to_check = (
|
sources_to_check = (source if isinstance(source, list) else ([source] if isinstance(source, dict) else []))
|
||||||
source
|
|
||||||
if isinstance(source, list)
|
|
||||||
else ([source] if isinstance(source, dict) else [])
|
|
||||||
)
|
|
||||||
for src in sources_to_check:
|
for src in sources_to_check:
|
||||||
if (
|
if (isinstance(src, dict) and src.get("uid") and src.get("type") in ["variable", "constant"]):
|
||||||
isinstance(src, dict)
|
|
||||||
and src.get("uid")
|
|
||||||
and src.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[src["uid"]] = src
|
current_access_map[src["uid"]] = src
|
||||||
for _, dest_list in instr.get("outputs", {}).items():
|
for _, dest_list in instr.get("outputs", {}).items():
|
||||||
if isinstance(dest_list, list):
|
if isinstance(dest_list, list):
|
||||||
for dest in dest_list:
|
for dest in dest_list:
|
||||||
if (
|
if (isinstance(dest, dict) and dest.get("uid") and dest.get("type") in ["variable", "constant"]):
|
||||||
isinstance(dest, dict)
|
|
||||||
and dest.get("uid")
|
|
||||||
and dest.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[dest["uid"]] = dest
|
current_access_map[dest["uid"]] = dest
|
||||||
network_access_maps[net_id] = current_access_map
|
network_access_maps[net_id] = current_access_map
|
||||||
|
|
||||||
|
# (Inicialización de SymbolManager y bucle iterativo SIN CAMBIOS)
|
||||||
symbol_manager = SymbolManager()
|
symbol_manager = SymbolManager()
|
||||||
sympy_map = {}
|
sympy_map = {}
|
||||||
max_passes = 30
|
max_passes = 30
|
||||||
passes = 0
|
passes = 0
|
||||||
processing_complete = False
|
processing_complete = False
|
||||||
|
|
||||||
print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---") # <-- Mensaje actualizado
|
print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---")
|
||||||
while passes < max_passes and not processing_complete:
|
while passes < max_passes and not processing_complete:
|
||||||
passes += 1
|
passes += 1
|
||||||
made_change_in_base_pass = False
|
made_change_in_base_pass = False
|
||||||
|
@ -398,246 +382,149 @@ def process_json_to_scl(json_filepath):
|
||||||
num_sympy_processed_this_pass = 0
|
num_sympy_processed_this_pass = 0
|
||||||
num_grouped_this_pass = 0
|
num_grouped_this_pass = 0
|
||||||
|
|
||||||
# --- FASE 1: Procesadores Base (Ignorando STL) ---
|
# FASE 1: Procesadores Base (Ignorando STL)
|
||||||
print(f" Fase 1 (SymPy Base - Orden por Prioridad):")
|
print(f" Fase 1 (SymPy Base - Orden por Prioridad):")
|
||||||
num_sympy_processed_this_pass = 0 # Resetear contador para el pase
|
num_sympy_processed_this_pass = 0
|
||||||
for processor_info in sorted_processors:
|
for processor_info in sorted_processors:
|
||||||
current_type_name = processor_info["type_name"]
|
current_type_name = processor_info["type_name"]
|
||||||
func_to_call = processor_info["func"]
|
func_to_call = processor_info["func"]
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network["id"]
|
network_id = network["id"]
|
||||||
network_lang = network.get("language", "LAD") # Lenguaje de la red
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL": # Saltar redes STL
|
if network_lang == "STL": continue
|
||||||
continue
|
|
||||||
|
|
||||||
access_map = network_access_maps.get(network_id, {})
|
access_map = network_access_maps.get(network_id, {})
|
||||||
network_logic = network.get("logic", [])
|
network_logic = network.get("logic", [])
|
||||||
for instruction in network_logic:
|
for instruction in network_logic:
|
||||||
instr_uid = instruction.get("instruction_uid")
|
instr_uid = instruction.get("instruction_uid")
|
||||||
# Usar el tipo *actual* de la instrucción para el lookup
|
|
||||||
instr_type_current = instruction.get("type", "Unknown")
|
instr_type_current = instruction.get("type", "Unknown")
|
||||||
|
|
||||||
# Saltar si ya está procesado, es error, agrupado, o tipo crudo
|
if (instr_type_current.endswith(SCL_SUFFIX) or "_error" in instr_type_current or instruction.get("grouped", False) or
|
||||||
if (
|
instr_type_current in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]):
|
||||||
instr_type_current.endswith(SCL_SUFFIX)
|
|
||||||
or "_error" in instr_type_current
|
|
||||||
or instruction.get("grouped", False)
|
|
||||||
or instr_type_current
|
|
||||||
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# El lookup usa el tipo actual (que aún no tiene el sufijo)
|
|
||||||
lookup_key = instr_type_current.lower()
|
lookup_key = instr_type_current.lower()
|
||||||
effective_type_name = lookup_key
|
effective_type_name = lookup_key
|
||||||
|
|
||||||
# Mapeo especial para llamadas FC/FB
|
|
||||||
if instr_type_current == "Call":
|
if instr_type_current == "Call":
|
||||||
call_block_type = instruction.get("block_type", "").upper()
|
call_block_type = instruction.get("block_type", "").upper()
|
||||||
if call_block_type == "FC":
|
if call_block_type == "FC": effective_type_name = "call_fc"
|
||||||
effective_type_name = "call_fc"
|
elif call_block_type == "FB": effective_type_name = "call_fb"
|
||||||
elif call_block_type == "FB":
|
|
||||||
effective_type_name = "call_fb"
|
|
||||||
# Añadir otros tipos de llamada si es necesario
|
|
||||||
|
|
||||||
# Si el tipo efectivo coincide con el procesador actual
|
|
||||||
if effective_type_name == current_type_name:
|
if effective_type_name == current_type_name:
|
||||||
try:
|
try:
|
||||||
# Pasar 'data' a la función del procesador
|
changed = func_to_call(instruction, network_id, sympy_map, symbol_manager, data)
|
||||||
changed = func_to_call(
|
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
)
|
|
||||||
if changed:
|
if changed:
|
||||||
made_change_in_base_pass = True
|
made_change_in_base_pass = True
|
||||||
num_sympy_processed_this_pass += 1
|
num_sympy_processed_this_pass += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}")
|
||||||
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
instruction["scl"] = (
|
instruction["scl"] = f"// ERROR en SymPy procesador base: {e}"
|
||||||
f"// ERROR en SymPy procesador base: {e}"
|
|
||||||
)
|
|
||||||
# Añadir sufijo de error al tipo actual
|
|
||||||
instruction["type"] = instr_type_current + "_error"
|
instruction["type"] = instr_type_current + "_error"
|
||||||
made_change_in_base_pass = True # Se hizo un cambio (marcar como error)
|
made_change_in_base_pass = True
|
||||||
print(
|
print(f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy.")
|
||||||
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# FASE 2: Agrupación IF (Ignorando STL)
|
||||||
# --- FASE 2: Agrupación IF (Ignorando STL) ---
|
if made_change_in_base_pass or passes == 1:
|
||||||
if (
|
|
||||||
made_change_in_base_pass or passes == 1
|
|
||||||
): # Ejecutar siempre en el primer pase o si hubo cambios
|
|
||||||
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
||||||
num_grouped_this_pass = 0 # Resetear contador para el pase
|
num_grouped_this_pass = 0
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network["id"]
|
network_id = network["id"]
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL":
|
if network_lang == "STL": continue
|
||||||
continue # Saltar STL
|
|
||||||
network_logic = network.get("logic", [])
|
network_logic = network.get("logic", [])
|
||||||
# Iterar en orden por UID puede ser más estable para agrupación
|
|
||||||
uids_in_network = sorted([instr.get("instruction_uid", "Z") for instr in network_logic if instr.get("instruction_uid")])
|
uids_in_network = sorted([instr.get("instruction_uid", "Z") for instr in network_logic if instr.get("instruction_uid")])
|
||||||
for uid_to_process in uids_in_network:
|
for uid_to_process in uids_in_network:
|
||||||
instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None)
|
instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None)
|
||||||
if not instruction: continue
|
if not instruction: continue
|
||||||
|
if instruction.get("grouped") or "_error" in instruction.get("type", ""): continue
|
||||||
# Saltar si ya está agrupada, es error, etc.
|
|
||||||
if instruction.get("grouped") or "_error" in instruction.get("type", ""):
|
|
||||||
continue
|
|
||||||
# La agrupación sólo aplica a instrucciones que generan condiciones booleanas
|
|
||||||
# y que ya fueron procesadas (tienen el sufijo)
|
|
||||||
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
||||||
try:
|
try:
|
||||||
group_changed = process_group_ifs(
|
group_changed = process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data)
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
)
|
|
||||||
if group_changed:
|
if group_changed:
|
||||||
made_change_in_group_pass = True
|
made_change_in_group_pass = True
|
||||||
num_grouped_this_pass += 1
|
num_grouped_this_pass += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}")
|
||||||
f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print(
|
print(f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL).")
|
||||||
f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Comprobar si se completó el procesamiento ---
|
# Comprobar si se completó
|
||||||
if not made_change_in_base_pass and not made_change_in_group_pass:
|
if not made_change_in_base_pass and not made_change_in_group_pass:
|
||||||
print(
|
print(f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---")
|
||||||
f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---"
|
|
||||||
)
|
|
||||||
processing_complete = True
|
processing_complete = True
|
||||||
else:
|
else:
|
||||||
print(
|
print(f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando...")
|
||||||
f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..."
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Comprobar límite de pases ---
|
|
||||||
if passes == max_passes and not processing_complete:
|
if passes == max_passes and not processing_complete:
|
||||||
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
|
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
|
||||||
|
|
||||||
# --- FIN BUCLE ITERATIVO ---
|
# --- FIN BUCLE ITERATIVO ---
|
||||||
|
|
||||||
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
|
# (Verificación Final y Guardado JSON SIN CAMBIOS)
|
||||||
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
|
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---")
|
||||||
unprocessed_count = 0
|
unprocessed_count = 0
|
||||||
unprocessed_details = []
|
unprocessed_details = []
|
||||||
ignored_types = [
|
ignored_types = ["raw_scl_chunk", "unsupported_lang", "raw_stl_chunk", "unsupported_content", "parsing_error"]
|
||||||
"raw_scl_chunk",
|
|
||||||
"unsupported_lang",
|
|
||||||
"raw_stl_chunk",
|
|
||||||
"unsupported_content", # Añadido de x1
|
|
||||||
"parsing_error", # Añadido de x1
|
|
||||||
]
|
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network.get("id", "Unknown ID")
|
network_id = network.get("id", "Unknown ID")
|
||||||
network_title = network.get("title", f"Network {network_id}")
|
network_title = network.get("title", f"Network {network_id}")
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL":
|
if network_lang == "STL": continue
|
||||||
continue # No verificar redes STL
|
|
||||||
for instruction in network.get("logic", []):
|
for instruction in network.get("logic", []):
|
||||||
instr_uid = instruction.get("instruction_uid", "Unknown UID")
|
instr_uid = instruction.get("instruction_uid", "Unknown UID")
|
||||||
instr_type = instruction.get("type", "Unknown Type")
|
instr_type = instruction.get("type", "Unknown Type")
|
||||||
is_grouped = instruction.get("grouped", False)
|
is_grouped = instruction.get("grouped", False)
|
||||||
if (
|
if (not instr_type.endswith(SCL_SUFFIX) and "_error" not in instr_type and not is_grouped and instr_type.lower() not in ignored_types):
|
||||||
not instr_type.endswith(SCL_SUFFIX)
|
|
||||||
and "_error" not in instr_type
|
|
||||||
and not is_grouped
|
|
||||||
and instr_type.lower() not in ignored_types
|
|
||||||
):
|
|
||||||
unprocessed_count += 1
|
unprocessed_count += 1
|
||||||
unprocessed_details.append(
|
unprocessed_details.append(f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), Instrucción UID: {instr_uid}, Tipo: '{instr_type}'")
|
||||||
f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), "
|
|
||||||
f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'"
|
|
||||||
)
|
|
||||||
if unprocessed_count > 0:
|
if unprocessed_count > 0:
|
||||||
print(
|
print(f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:")
|
||||||
f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:"
|
for detail in unprocessed_details: print(detail)
|
||||||
)
|
else: print("INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas.")
|
||||||
for detail in unprocessed_details:
|
|
||||||
print(detail)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas."
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Guardar JSON Final ---
|
print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}")
|
||||||
output_filename = json_filepath.replace(
|
|
||||||
"_simplified.json", "_simplified_processed.json"
|
|
||||||
)
|
|
||||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
|
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_json_filepath, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
print("Guardado completado.")
|
print("Guardado completado.")
|
||||||
except Exception as e:
|
return True
|
||||||
print(f"Error Crítico al guardar JSON procesado: {e}")
|
except Exception as e:
|
||||||
|
print(f"Error Crítico al guardar JSON procesado: {e}");
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- Ejecución (MODIFICADO) ---
|
||||||
# --- Ejecución (sin cambios en esta parte) ---
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Imports necesarios solo para la ejecución como script principal
|
parser = argparse.ArgumentParser(description="Process simplified JSON to embed SCL logic. Expects original XML filepath as argument.")
|
||||||
import argparse
|
parser.add_argument("source_xml_filepath", help="Path to the original source XML file (passed from x0_main.py).")
|
||||||
import os
|
args = parser.parse_args()
|
||||||
import sys
|
source_xml_file = args.source_xml_filepath
|
||||||
|
|
||||||
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Process simplified JSON (_simplified.json) to embed SCL logic (SymPy version). Expects original XML filepath as argument."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"source_xml_filepath", # Argumento posicional obligatorio
|
|
||||||
help="Path to the original source XML file (passed from x0_main.py, used to derive JSON input name).",
|
|
||||||
)
|
|
||||||
args = parser.parse_args() # Parsea los argumentos de sys.argv
|
|
||||||
|
|
||||||
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
|
|
||||||
|
|
||||||
# Verificar si el archivo XML original existe (como referencia, útil para depuración)
|
|
||||||
if not os.path.exists(source_xml_file):
|
if not os.path.exists(source_xml_file):
|
||||||
print(
|
print(f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.")
|
||||||
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Derivar nombre del archivo JSON de entrada (_simplified.json)
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
# Asumir que el JSON simplificado está en el mismo directorio que el XML original
|
base_dir = os.path.dirname(source_xml_file)
|
||||||
input_dir = os.path.dirname(source_xml_file) # Directorio del XML original
|
parsing_dir = os.path.join(base_dir, "parsing")
|
||||||
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
|
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}.json")
|
||||||
|
output_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
|
||||||
|
|
||||||
|
os.makedirs(parsing_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'")
|
||||||
|
|
||||||
# Determinar el nombre esperado del archivo JSON procesado de salida
|
|
||||||
output_json_file = os.path.join(
|
|
||||||
input_dir, f"{xml_filename_base}_simplified_processed.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verificar si el archivo JSON de entrada (_simplified.json) EXISTE antes de procesar
|
|
||||||
if not os.path.exists(input_json_file):
|
if not os.path.exists(input_json_file):
|
||||||
print(
|
print(f"Error Fatal (x2): El archivo de entrada JSON no existe: '{input_json_file}'")
|
||||||
f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'"
|
print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.")
|
||||||
)
|
sys.exit(1)
|
||||||
print(
|
|
||||||
f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
|
|
||||||
)
|
|
||||||
sys.exit(1) # Salir si el archivo necesario no está
|
|
||||||
else:
|
else:
|
||||||
# Llamar a la función principal de procesamiento del script
|
|
||||||
try:
|
try:
|
||||||
process_json_to_scl(input_json_file)
|
success = process_json_to_scl(input_json_file, output_json_file)
|
||||||
|
if success:
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
|
||||||
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
|
|
||||||
)
|
|
||||||
import traceback # Asegurar que traceback está importado
|
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
sys.exit(1)
|
|
@ -5,391 +5,29 @@ import os
|
||||||
import re
|
import re
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import traceback # Importar traceback para errores
|
import traceback
|
||||||
|
|
||||||
# --- Importar Utilidades y Constantes (Asumiendo ubicación) ---
|
# --- Importar Generadores Específicos ---
|
||||||
try:
|
try:
|
||||||
# Intenta importar desde el paquete de procesadores si está estructurado así
|
from generators.generate_scl_db import generate_scl_for_db
|
||||||
from processors.processor_utils import format_variable_name
|
from generators.generate_scl_code_block import generate_scl_for_code_block
|
||||||
|
from generators.generate_md_udt import generate_udt_markdown
|
||||||
# Definir SCL_SUFFIX aquí o importarlo si está centralizado
|
from generators.generate_md_tag_table import generate_tag_table_markdown
|
||||||
SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py
|
# Importar format_variable_name (necesario para el nombre de archivo)
|
||||||
GROUPED_COMMENT = (
|
from generators.generator_utils import format_variable_name
|
||||||
"// Logic included in grouped IF" # Opcional, si se usa para filtrar
|
except ImportError as e:
|
||||||
)
|
print(f"Error crítico: No se pudieron importar los módulos de 'generators': {e}")
|
||||||
except ImportError:
|
print("Asegúrate de que el directorio 'generators' y sus archivos .py existen.")
|
||||||
print(
|
sys.exit(1)
|
||||||
"Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils."
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Implementación local BÁSICA como fallback (MENOS RECOMENDADA)
|
|
||||||
def format_variable_name(name):
|
|
||||||
if not name:
|
|
||||||
return "_INVALID_NAME_"
|
|
||||||
if name.startswith('"') and name.endswith('"'):
|
|
||||||
return name # Mantener comillas
|
|
||||||
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
|
|
||||||
|
|
||||||
SCL_SUFFIX = "_sympy_processed"
|
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
|
||||||
|
|
||||||
|
|
||||||
# para formatear valores iniciales
|
|
||||||
def format_scl_start_value(value, datatype):
|
|
||||||
"""Formatea un valor para la inicialización SCL según el tipo."""
|
|
||||||
# Add initial debug print
|
|
||||||
# print(f"DEBUG format_scl_start_value: value='{value}', datatype='{datatype}'")
|
|
||||||
if value is None:
|
|
||||||
return None # Retornar None si no hay valor
|
|
||||||
datatype_lower = datatype.lower() if datatype else ""
|
|
||||||
value_str = str(value)
|
|
||||||
|
|
||||||
# Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE)
|
|
||||||
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1:
|
|
||||||
value_str_unquoted = value_str[1:-1]
|
|
||||||
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1:
|
|
||||||
value_str_unquoted = value_str[1:-1]
|
|
||||||
else:
|
|
||||||
value_str_unquoted = value_str
|
|
||||||
|
|
||||||
# --- Integer-like types ---
|
|
||||||
if any(
|
|
||||||
t in datatype_lower
|
|
||||||
for t in [
|
|
||||||
"int",
|
|
||||||
"byte",
|
|
||||||
"word",
|
|
||||||
"dint",
|
|
||||||
"dword",
|
|
||||||
"lint",
|
|
||||||
"lword",
|
|
||||||
"sint",
|
|
||||||
"usint",
|
|
||||||
"uint",
|
|
||||||
"udint",
|
|
||||||
"ulint",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# Intentar convertir el valor (sin comillas) a entero
|
|
||||||
return str(int(value_str_unquoted))
|
|
||||||
except ValueError:
|
|
||||||
# Si no es un entero válido, podría ser una constante simbólica
|
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
|
||||||
return value_str_unquoted # Devolver como símbolo
|
|
||||||
|
|
||||||
# --- Fallback for non-integer, non-symbol ---
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback for int-like. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
|
||||||
) # More debug
|
|
||||||
# MODIFIED FALLBACK: Escape newlines and use repr() for safety before formatting
|
|
||||||
try:
|
|
||||||
# Escape backslashes and single quotes properly for SCL string literal
|
|
||||||
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace(
|
|
||||||
"'", "''"
|
|
||||||
)
|
|
||||||
# Remove potential newlines that break Python f-string; SCL strings usually don't span lines implicitly
|
|
||||||
escaped_for_scl = escaped_for_scl.replace("\n", "").replace("\r", "")
|
|
||||||
# Format as SCL string literal
|
|
||||||
formatted_scl_string = f"'{escaped_for_scl}'"
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback result='{formatted_scl_string}'"
|
|
||||||
)
|
|
||||||
return formatted_scl_string
|
|
||||||
except Exception as format_exc:
|
|
||||||
print(
|
|
||||||
f"ERROR format_scl_start_value: Exception during fallback formatting: {format_exc}"
|
|
||||||
)
|
|
||||||
return f"'ERROR_FORMATTING_{value_str_unquoted[:20]}'" # Return an error string
|
|
||||||
|
|
||||||
# --- Other types (Bool, Real, String, Char, Time, Date, etc.) ---
|
|
||||||
elif "bool" in datatype_lower:
|
|
||||||
# Comparar sin importar mayúsculas/minúsculas y sin comillas
|
|
||||||
return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
|
||||||
elif "string" in datatype_lower:
|
|
||||||
# Usar el valor sin comillas originales y escapar las internas
|
|
||||||
escaped_value = value_str_unquoted.replace("'", "''")
|
|
||||||
return f"'{escaped_value}'"
|
|
||||||
elif "char" in datatype_lower:
|
|
||||||
# Usar el valor sin comillas originales y escapar las internas
|
|
||||||
escaped_value = value_str_unquoted.replace("'", "''")
|
|
||||||
# SCL usa comillas simples para Char. Asegurar que sea un solo caracter si es posible?
|
|
||||||
# Por ahora, solo formatear. Longitud se verifica en TIA.
|
|
||||||
return f"'{escaped_value}'"
|
|
||||||
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
|
||||||
try:
|
|
||||||
# Intentar convertir a float
|
|
||||||
f_val = float(value_str_unquoted)
|
|
||||||
s_val = str(f_val)
|
|
||||||
# Asegurar que tenga punto decimal si es entero
|
|
||||||
if "." not in s_val and "e" not in s_val.lower():
|
|
||||||
s_val += ".0"
|
|
||||||
return s_val
|
|
||||||
except ValueError:
|
|
||||||
# Podría ser constante simbólica
|
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
|
||||||
return value_str_unquoted
|
|
||||||
print(
|
|
||||||
f"Advertencia: Valor '{value_str}' no reconocido como real o símbolo para tipo {datatype}. Devolviendo como string."
|
|
||||||
)
|
|
||||||
# Use the robust fallback formatting here too
|
|
||||||
escaped_for_scl = (
|
|
||||||
value_str_unquoted.replace("\\", "\\\\")
|
|
||||||
.replace("'", "''")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
)
|
|
||||||
return f"'{escaped_for_scl}'"
|
|
||||||
elif "time" in datatype_lower:
|
|
||||||
# Quitar prefijos y añadir el correcto según el tipo específico
|
|
||||||
prefix = ""
|
|
||||||
val_to_use = value_str_unquoted # Usar valor sin comillas
|
|
||||||
if val_to_use.upper().startswith("T#"):
|
|
||||||
prefix = "T#"
|
|
||||||
val_to_use = val_to_use[2:]
|
|
||||||
elif val_to_use.upper().startswith("LT#"):
|
|
||||||
prefix = "LT#"
|
|
||||||
val_to_use = val_to_use[3:]
|
|
||||||
elif val_to_use.upper().startswith("S5T#"):
|
|
||||||
prefix = "S5T#"
|
|
||||||
val_to_use = val_to_use[4:]
|
|
||||||
|
|
||||||
if "s5time" in datatype_lower:
|
|
||||||
return f"S5T#{val_to_use}"
|
|
||||||
elif "ltime" in datatype_lower:
|
|
||||||
return f"LT#{val_to_use}"
|
|
||||||
else:
|
|
||||||
return f"T#{val_to_use}" # Default a TIME
|
|
||||||
elif "date" in datatype_lower:
|
|
||||||
val_to_use = value_str_unquoted
|
|
||||||
# Handle DTL first as it's longer
|
|
||||||
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
|
|
||||||
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
elif "dt" in datatype_lower:
|
|
||||||
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
|
||||||
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
else: # Default a Date D#
|
|
||||||
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
|
|
||||||
# --- Fallback for completely unknown types or complex structures ---
|
|
||||||
else:
|
|
||||||
# Si es un nombre válido (posiblemente UDT, constante global, etc.), devolverlo tal cual
|
|
||||||
# Ajustar regex para permitir más caracteres si es necesario
|
|
||||||
if re.match(
|
|
||||||
r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str
|
|
||||||
): # Permitir % para accesos tipo %DB1.DBD0
|
|
||||||
# Quitar comillas externas si es un UDT o struct complejo
|
|
||||||
if (
|
|
||||||
value_str.startswith('"')
|
|
||||||
and value_str.endswith('"')
|
|
||||||
and len(value_str) > 1
|
|
||||||
):
|
|
||||||
return value_str[1:-1]
|
|
||||||
# Mantener comillas si es acceso a DB ("DB_Name".Var)
|
|
||||||
if '"' in value_str and "." in value_str and value_str.count('"') == 2:
|
|
||||||
return value_str
|
|
||||||
# Si no tiene comillas y es un nombre simple o acceso #temp o %I0.0 etc
|
|
||||||
if not value_str.startswith('"') and not value_str.startswith("'"):
|
|
||||||
# Formatear nombres simples, pero dejar accesos % y # tal cual
|
|
||||||
if value_str.startswith("#") or value_str.startswith("%"):
|
|
||||||
return value_str
|
|
||||||
else:
|
|
||||||
# return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante
|
|
||||||
return value_str # Return as is if it looks symbolic
|
|
||||||
# Devolver el valor original si tiene comillas internas o estructura compleja no manejada arriba
|
|
||||||
return value_str
|
|
||||||
else:
|
|
||||||
# Si no parece un nombre/símbolo/acceso, tratarlo como string (último recurso)
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback final. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
|
||||||
)
|
|
||||||
# Use the robust fallback formatting
|
|
||||||
escaped_for_scl = (
|
|
||||||
value_str_unquoted.replace("\\", "\\\\")
|
|
||||||
.replace("'", "''")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
)
|
|
||||||
return f"'{escaped_for_scl}'"
|
|
||||||
|
|
||||||
|
|
||||||
# ... (generate_scl_declarations and generate_scl function remain the same as the previous version) ...
|
|
||||||
# --- (Incluye aquí las funciones generate_scl_declarations y generate_scl SIN CAMBIOS respecto a la respuesta anterior) ---
|
|
||||||
|
|
||||||
|
|
||||||
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
|
|
||||||
def generate_scl_declarations(variables, indent_level=1):
|
|
||||||
"""Genera las líneas SCL para declarar variables, structs y arrays."""
|
|
||||||
scl_lines = []
|
|
||||||
indent = " " * indent_level
|
|
||||||
for var in variables:
|
|
||||||
var_name_scl = format_variable_name(var.get("name"))
|
|
||||||
var_dtype_raw = var.get("datatype", "VARIANT")
|
|
||||||
var_comment = var.get("comment")
|
|
||||||
start_value = var.get("start_value")
|
|
||||||
children = var.get("children") # Para structs
|
|
||||||
array_elements = var.get("array_elements") # Para arrays
|
|
||||||
|
|
||||||
# Limpiar comillas del tipo de dato si es UDT/String/etc.
|
|
||||||
var_dtype_cleaned = var_dtype_raw
|
|
||||||
if isinstance(var_dtype_raw, str):
|
|
||||||
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'):
|
|
||||||
var_dtype_cleaned = var_dtype_raw[1:-1]
|
|
||||||
# Manejar caso 'Array [...] of "MyUDT"'
|
|
||||||
array_match = re.match(
|
|
||||||
r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE
|
|
||||||
)
|
|
||||||
if array_match:
|
|
||||||
var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
|
|
||||||
|
|
||||||
# Determinar tipo base para inicialización (importante para arrays)
|
|
||||||
base_type_for_init = var_dtype_cleaned
|
|
||||||
array_prefix_for_decl = ""
|
|
||||||
if var_dtype_cleaned.lower().startswith("array["):
|
|
||||||
match = re.match(
|
|
||||||
r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
array_prefix_for_decl = match.group(1)
|
|
||||||
base_type_for_init = match.group(2).strip()
|
|
||||||
|
|
||||||
# Construir tipo de dato para la declaración SCL
|
|
||||||
declaration_dtype = var_dtype_raw # Usar el raw por defecto
|
|
||||||
# Si es UDT o tipo complejo que requiere comillas y no es array simple
|
|
||||||
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl:
|
|
||||||
# Poner comillas si no las tiene ya el tipo base
|
|
||||||
if not base_type_for_init.startswith('"'):
|
|
||||||
declaration_dtype = f'"{base_type_for_init}"'
|
|
||||||
else:
|
|
||||||
declaration_dtype = base_type_for_init # Ya tiene comillas
|
|
||||||
# Si es array de UDT/complejo, reconstruir con comillas en el tipo base
|
|
||||||
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned:
|
|
||||||
if not base_type_for_init.startswith('"'):
|
|
||||||
declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
|
|
||||||
else:
|
|
||||||
declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
|
|
||||||
|
|
||||||
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
|
||||||
init_value_scl = None
|
|
||||||
|
|
||||||
# ---- Arrays ----
|
|
||||||
if array_elements:
|
|
||||||
# Ordenar índices (asumiendo que son numéricos '0', '1', ...)
|
|
||||||
try:
|
|
||||||
# Extraer números de los índices string
|
|
||||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
|
||||||
sorted_indices = sorted(indices_numeric.keys())
|
|
||||||
# Mapear de nuevo a string para buscar valor
|
|
||||||
sorted_indices_str = [str(k) for k in sorted_indices]
|
|
||||||
except ValueError:
|
|
||||||
# Fallback a orden alfabético si los índices no son números
|
|
||||||
print(
|
|
||||||
f"Advertencia: Índices de array no numéricos para '{var_name_scl}'. Usando orden alfabético."
|
|
||||||
)
|
|
||||||
sorted_indices_str = sorted(array_elements.keys())
|
|
||||||
|
|
||||||
init_values = []
|
|
||||||
for idx_str in sorted_indices_str:
|
|
||||||
try:
|
|
||||||
formatted_val = format_scl_start_value(
|
|
||||||
array_elements[idx_str], base_type_for_init
|
|
||||||
)
|
|
||||||
init_values.append(formatted_val)
|
|
||||||
except Exception as e_fmt:
|
|
||||||
print(
|
|
||||||
f"ERROR: Falló formateo para índice {idx_str} de array '{var_name_scl}'. Valor: {array_elements[idx_str]}. Error: {e_fmt}"
|
|
||||||
)
|
|
||||||
init_values.append(f"/*ERR_FMT_{idx_str}*/") # Placeholder de error
|
|
||||||
|
|
||||||
# Filtrar Nones que pueden venir de format_scl_start_value si el valor era None
|
|
||||||
valid_inits = [v for v in init_values if v is not None]
|
|
||||||
if valid_inits:
|
|
||||||
# Si todos los valores son iguales y es un array grande, podríamos usar notación x(value)
|
|
||||||
# Simplificación: por ahora, listar todos
|
|
||||||
init_value_scl = f"[{', '.join(valid_inits)}]"
|
|
||||||
elif array_elements: # Si había elementos pero todos formatearon a None
|
|
||||||
print(
|
|
||||||
f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos."
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- Structs ----
|
|
||||||
elif children:
|
|
||||||
# El valor inicial de un struct se maneja recursivamente dentro
|
|
||||||
# Añadir comentario? Puede ser redundante.
|
|
||||||
scl_lines.append(
|
|
||||||
declaration_line
|
|
||||||
) # Añadir línea de declaración base STRUCT
|
|
||||||
scl_lines.append(f"{indent}STRUCT")
|
|
||||||
# Llamada recursiva para los miembros internos
|
|
||||||
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
|
||||||
scl_lines.append(f"{indent}END_STRUCT;")
|
|
||||||
if var_comment: # Comentario después de END_STRUCT
|
|
||||||
scl_lines.append(f"{indent}// {var_comment}")
|
|
||||||
scl_lines.append("") # Línea extra para legibilidad
|
|
||||||
continue # Saltar el resto de la lógica para este struct
|
|
||||||
|
|
||||||
# ---- Tipos Simples ----
|
|
||||||
else:
|
|
||||||
if start_value is not None:
|
|
||||||
try:
|
|
||||||
init_value_scl = format_scl_start_value(
|
|
||||||
start_value, base_type_for_init
|
|
||||||
) # Usar tipo base
|
|
||||||
except Exception as e_fmt_simple:
|
|
||||||
print(
|
|
||||||
f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"
|
|
||||||
)
|
|
||||||
init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder
|
|
||||||
|
|
||||||
# Añadir inicialización si existe y no es None
|
|
||||||
if init_value_scl is not None:
|
|
||||||
declaration_line += f" := {init_value_scl}"
|
|
||||||
|
|
||||||
declaration_line += ";"
|
|
||||||
|
|
||||||
# Añadir comentario si existe
|
|
||||||
if var_comment:
|
|
||||||
declaration_line += f" // {var_comment}"
|
|
||||||
|
|
||||||
scl_lines.append(declaration_line)
|
|
||||||
|
|
||||||
return scl_lines
|
|
||||||
|
|
||||||
|
|
||||||
# --- Función Principal de Generación SCL ---
|
|
||||||
def generate_scl(processed_json_filepath, output_scl_filepath):
|
|
||||||
"""Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
|
|
||||||
|
|
||||||
|
# --- Función Principal de Generación (Despachador) ---
|
||||||
|
def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||||
|
"""
|
||||||
|
Genera un archivo SCL o Markdown a partir del JSON procesado,
|
||||||
|
llamando a la función generadora apropiada y escribiendo el archivo.
|
||||||
|
"""
|
||||||
if not os.path.exists(processed_json_filepath):
|
if not os.path.exists(processed_json_filepath):
|
||||||
print(
|
print(f"Error: JSON no encontrado: '{processed_json_filepath}'")
|
||||||
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
|
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
|
||||||
|
@ -397,411 +35,76 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
with open(processed_json_filepath, "r", encoding="utf-8") as f:
|
with open(processed_json_filepath, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al cargar o parsear JSON: {e}")
|
print(f"Error al cargar/parsear JSON: {e}"); traceback.print_exc(); return
|
||||||
traceback.print_exc()
|
|
||||||
|
block_name = data.get("block_name", "UnknownBlock")
|
||||||
|
block_type = data.get("block_type", "Unknown")
|
||||||
|
scl_block_name = format_variable_name(block_name) # Nombre seguro para archivo
|
||||||
|
output_content = []
|
||||||
|
output_extension = ".scl" # Default
|
||||||
|
|
||||||
|
print(f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})")
|
||||||
|
|
||||||
|
# --- Selección del Generador y Extensión ---
|
||||||
|
generation_function = None
|
||||||
|
if block_type == "PlcUDT":
|
||||||
|
print(" -> Modo de generación: UDT Markdown")
|
||||||
|
generation_function = generate_udt_markdown
|
||||||
|
output_extension = ".md"
|
||||||
|
elif block_type == "PlcTagTable":
|
||||||
|
print(" -> Modo de generación: Tag Table Markdown")
|
||||||
|
generation_function = generate_tag_table_markdown
|
||||||
|
output_extension = ".md"
|
||||||
|
elif block_type == "GlobalDB":
|
||||||
|
print(" -> Modo de generación: DATA_BLOCK SCL")
|
||||||
|
generation_function = generate_scl_for_db
|
||||||
|
output_extension = ".scl"
|
||||||
|
elif block_type in ["FC", "FB", "OB"]:
|
||||||
|
print(f" -> Modo de generación: {block_type} SCL")
|
||||||
|
generation_function = generate_scl_for_code_block
|
||||||
|
output_extension = ".scl"
|
||||||
|
else: # Tipo desconocido
|
||||||
|
print(f"Error: Tipo de bloque desconocido '{block_type}'. No se generará archivo.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Extracción de Información del Bloque (Común) ---
|
# --- Llamar a la función generadora ---
|
||||||
block_name = data.get("block_name", "UnknownBlock")
|
if generation_function:
|
||||||
block_number = data.get("block_number")
|
try:
|
||||||
# block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
|
output_content = generation_function(data)
|
||||||
block_type = data.get(
|
except Exception as gen_e:
|
||||||
"block_type", "Unknown"
|
print(f"Error durante la generación de contenido para {block_type} '{scl_block_name}': {gen_e}")
|
||||||
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
|
traceback.print_exc()
|
||||||
block_comment = data.get("block_comment", "")
|
return # No intentar escribir si la generación falla
|
||||||
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
|
|
||||||
print(
|
|
||||||
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
|
|
||||||
)
|
|
||||||
scl_output = []
|
|
||||||
|
|
||||||
# --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
|
# --- Escritura del Archivo de Salida ---
|
||||||
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
output_filename_base = f"{scl_block_name}{output_extension}"
|
||||||
print("Modo de generación: DATA_BLOCK")
|
output_filepath = os.path.join(output_directory, output_filename_base)
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
|
||||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
|
||||||
if block_number:
|
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
|
||||||
if block_comment:
|
|
||||||
# Dividir comentarios largos en múltiples líneas
|
|
||||||
comment_lines = block_comment.splitlines()
|
|
||||||
scl_output.append(f"// Block Comment:")
|
|
||||||
for line in comment_lines:
|
|
||||||
scl_output.append(f"// {line}")
|
|
||||||
scl_output.append("")
|
|
||||||
scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
|
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
|
||||||
scl_output.append("VERSION : 0.1")
|
|
||||||
scl_output.append("")
|
|
||||||
interface_data = data.get("interface", {})
|
|
||||||
# En DBs, la sección relevante suele ser 'Static'
|
|
||||||
static_vars = interface_data.get("Static", [])
|
|
||||||
if static_vars:
|
|
||||||
scl_output.append("VAR")
|
|
||||||
# Usar la función recursiva para generar declaraciones
|
|
||||||
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
|
||||||
scl_output.append("END_VAR")
|
|
||||||
scl_output.append("")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
|
||||||
)
|
|
||||||
# Añadir bloque VAR vacío si no hay variables
|
|
||||||
scl_output.append("VAR")
|
|
||||||
scl_output.append("END_VAR")
|
|
||||||
scl_output.append("")
|
|
||||||
scl_output.append("BEGIN")
|
|
||||||
scl_output.append(
|
|
||||||
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
|
|
||||||
)
|
|
||||||
scl_output.append("END_DATA_BLOCK")
|
|
||||||
|
|
||||||
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
|
print(f" -> Escribiendo archivo de salida en: {output_filepath}")
|
||||||
else:
|
|
||||||
# Determinar palabra clave SCL
|
|
||||||
scl_block_keyword = "FUNCTION_BLOCK" # Default
|
|
||||||
if block_type == "FC":
|
|
||||||
scl_block_keyword = "FUNCTION"
|
|
||||||
elif block_type == "OB":
|
|
||||||
scl_block_keyword = "ORGANIZATION_BLOCK"
|
|
||||||
elif block_type == "FB":
|
|
||||||
scl_block_keyword = "FUNCTION_BLOCK"
|
|
||||||
else: # Fallback
|
|
||||||
print(
|
|
||||||
f"Advertencia: Tipo de bloque desconocido '{block_type}', usando FUNCTION_BLOCK."
|
|
||||||
)
|
|
||||||
scl_block_keyword = "FUNCTION_BLOCK" # O quizás lanzar error?
|
|
||||||
|
|
||||||
print(f"Modo de generación: {scl_block_keyword}")
|
|
||||||
|
|
||||||
# Cabecera del Bloque
|
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
|
||||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
|
||||||
if block_number:
|
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
|
||||||
# Indicar lenguaje original de las redes si es relevante
|
|
||||||
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:
|
|
||||||
comment_lines = block_comment.splitlines()
|
|
||||||
scl_output.append(f"// Block Comment:")
|
|
||||||
for line in comment_lines:
|
|
||||||
scl_output.append(f"// {line}")
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Manejar tipo de retorno para FUNCTION (FC)
|
|
||||||
return_type = "Void" # Default
|
|
||||||
interface_data = data.get("interface", {})
|
|
||||||
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
|
|
||||||
# Asumir un solo valor de retorno
|
|
||||||
return_member = interface_data["Return"][0]
|
|
||||||
return_type_raw = return_member.get("datatype", "Void")
|
|
||||||
# Limpiar comillas si es UDT/String
|
|
||||||
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
|
|
||||||
)
|
|
||||||
# Añadir comillas si es UDT y no las tenía
|
|
||||||
if (
|
|
||||||
return_type != return_type_raw
|
|
||||||
and not return_type_raw.lower().startswith("array")
|
|
||||||
):
|
|
||||||
return_type = f'"{return_type}"'
|
|
||||||
else: # Mantener raw si es tipo básico o ya tenía comillas
|
|
||||||
return_type = return_type_raw
|
|
||||||
|
|
||||||
# Línea de declaración del bloque
|
|
||||||
if scl_block_keyword == "FUNCTION":
|
|
||||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
|
|
||||||
else: # FB y OB
|
|
||||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
|
|
||||||
|
|
||||||
# Atributos y versión
|
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
|
||||||
scl_output.append("VERSION : 0.1")
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant)
|
|
||||||
# Orden estándar SCL
|
|
||||||
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
|
||||||
declared_temps = set() # Para rastrear temps ya declaradas
|
|
||||||
has_declarations = False
|
|
||||||
|
|
||||||
for section_name in section_order:
|
|
||||||
vars_in_section = interface_data.get(section_name, [])
|
|
||||||
if vars_in_section:
|
|
||||||
has_declarations = True
|
|
||||||
# Mapeo de nombres de sección JSON a palabras clave SCL VAR_
|
|
||||||
scl_section_keyword = f"VAR_{section_name.upper()}"
|
|
||||||
if section_name == "Static":
|
|
||||||
scl_section_keyword = "VAR_STAT" # Para FBs
|
|
||||||
if section_name == "Temp":
|
|
||||||
scl_section_keyword = "VAR_TEMP"
|
|
||||||
if section_name == "Constant":
|
|
||||||
scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_
|
|
||||||
|
|
||||||
scl_output.append(scl_section_keyword)
|
|
||||||
# Usar la función recursiva para generar declaraciones
|
|
||||||
scl_output.extend(
|
|
||||||
generate_scl_declarations(vars_in_section, indent_level=1)
|
|
||||||
)
|
|
||||||
# Añadir END_VAR (o END_CONSTANT)
|
|
||||||
scl_output.append(
|
|
||||||
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
|
||||||
)
|
|
||||||
scl_output.append("") # Línea en blanco
|
|
||||||
|
|
||||||
# Guardar nombres de Temp declarados explícitamente
|
|
||||||
if section_name == "Temp":
|
|
||||||
declared_temps.update(
|
|
||||||
format_variable_name(v.get("name"))
|
|
||||||
for v in vars_in_section
|
|
||||||
if v.get("name")
|
|
||||||
)
|
|
||||||
# Declaraciones VAR_TEMP adicionales (auto-detectadas)
|
|
||||||
# Buscar variables que empiecen con #_temp_ en el SCL generado
|
|
||||||
temp_vars_detected = set()
|
|
||||||
# Patrón para encontrar #variable o "#variable"
|
|
||||||
temp_pattern = re.compile(
|
|
||||||
r'"?(#\w+)"?'
|
|
||||||
) # Busca # seguido de caracteres alfanuméricos
|
|
||||||
|
|
||||||
for network in data.get("networks", []):
|
|
||||||
for instruction in network.get("logic", []):
|
|
||||||
# Revisar el SCL final y el SCL de actualización de memoria si existe
|
|
||||||
scl_code = instruction.get("scl", "")
|
|
||||||
edge_update_code = instruction.get(
|
|
||||||
"_edge_mem_update_scl", ""
|
|
||||||
) # Para flancos
|
|
||||||
code_to_scan = (
|
|
||||||
(scl_code if scl_code else "")
|
|
||||||
+ "\n"
|
|
||||||
+ (edge_update_code if edge_update_code else "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if code_to_scan:
|
|
||||||
# Usar findall para encontrar todas las ocurrencias
|
|
||||||
found_temps = temp_pattern.findall(code_to_scan)
|
|
||||||
for temp_name in found_temps:
|
|
||||||
# findall devuelve el grupo capturado (#...)
|
|
||||||
if temp_name:
|
|
||||||
temp_vars_detected.add(temp_name)
|
|
||||||
|
|
||||||
# Filtrar las que ya estaban declaradas
|
|
||||||
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
|
||||||
|
|
||||||
if additional_temps:
|
|
||||||
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
|
||||||
# Si no se declaró la sección Temp antes, añadirla ahora
|
|
||||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
|
||||||
scl_output.append("VAR_TEMP")
|
|
||||||
|
|
||||||
for temp_name in additional_temps:
|
|
||||||
# Formatear por si acaso, aunque el patrón ya debería dar #nombre
|
|
||||||
scl_name = format_variable_name(temp_name)
|
|
||||||
# Inferir tipo (Bool es lo más común para temporales internos)
|
|
||||||
# Se podría mejorar si el nombre da pistas (ej. _temp_r para Real)
|
|
||||||
inferred_type = "Bool" # Asumir Bool por defecto
|
|
||||||
scl_output.append(
|
|
||||||
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Si abrimos la sección aquí, cerrarla
|
|
||||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
|
||||||
scl_output.append("END_VAR")
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# --- Cuerpo del Bloque (BEGIN...END) ---
|
|
||||||
scl_output.append("BEGIN")
|
|
||||||
scl_output.append("")
|
|
||||||
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
|
|
||||||
for i, network in enumerate(data.get("networks", [])):
|
|
||||||
network_title = network.get(
|
|
||||||
"title", f'Network {network.get("id", i+1)}'
|
|
||||||
) # Usar i+1 si falta ID
|
|
||||||
network_comment = network.get("comment", "")
|
|
||||||
network_lang = network.get("language", "LAD") # Lenguaje original de la red
|
|
||||||
scl_output.append(
|
|
||||||
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
|
||||||
)
|
|
||||||
if network_comment:
|
|
||||||
# Indentar comentarios de red
|
|
||||||
for line in network_comment.splitlines():
|
|
||||||
scl_output.append(f" // {line}")
|
|
||||||
scl_output.append("") # Línea en blanco antes del código de red
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# --- Manejo Especial Redes STL ---
|
|
||||||
if network_lang == "STL":
|
|
||||||
# Asumir que la lógica STL está en el primer elemento como RAW_STL_CHUNK
|
|
||||||
if 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"
|
|
||||||
)
|
|
||||||
# Incrustar STL como comentario multi-línea o delimitado
|
|
||||||
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
|
|
||||||
# Comentar cada línea STL
|
|
||||||
for stl_line in raw_stl_code.splitlines():
|
|
||||||
scl_output.append(f" // {stl_line}")
|
|
||||||
scl_output.append(f" // --- END STL Network {i+1} ---")
|
|
||||||
scl_output.append("") # Línea en blanco después
|
|
||||||
else:
|
|
||||||
scl_output.append(
|
|
||||||
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
|
||||||
)
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# --- Manejo Redes SCL/LAD/FBD procesadas ---
|
|
||||||
else:
|
|
||||||
# Iterar por las instrucciones procesadas
|
|
||||||
for instruction in logic_in_network:
|
|
||||||
instruction_type = instruction.get("type", "")
|
|
||||||
scl_code = instruction.get("scl", "")
|
|
||||||
is_grouped = instruction.get("grouped", False)
|
|
||||||
|
|
||||||
# Saltar instrucciones agrupadas (su lógica está en el IF)
|
|
||||||
if is_grouped:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder
|
|
||||||
if (
|
|
||||||
instruction_type.endswith(SCL_SUFFIX)
|
|
||||||
or instruction_type
|
|
||||||
in [
|
|
||||||
"RAW_SCL_CHUNK",
|
|
||||||
"UNSUPPORTED_LANG",
|
|
||||||
"UNSUPPORTED_CONTENT",
|
|
||||||
"PARSING_ERROR",
|
|
||||||
]
|
|
||||||
or "_error" in instruction_type # Incluir errores comentados
|
|
||||||
) and scl_code:
|
|
||||||
|
|
||||||
# Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF)
|
|
||||||
is_only_comment = all(
|
|
||||||
line.strip().startswith("//")
|
|
||||||
for line in scl_code.splitlines()
|
|
||||||
if line.strip()
|
|
||||||
)
|
|
||||||
is_if_block = scl_code.strip().startswith("IF")
|
|
||||||
|
|
||||||
# Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error)
|
|
||||||
if (
|
|
||||||
not is_only_comment
|
|
||||||
or is_if_block
|
|
||||||
or "_error" in instruction_type
|
|
||||||
or instruction_type
|
|
||||||
in [
|
|
||||||
"UNSUPPORTED_LANG",
|
|
||||||
"UNSUPPORTED_CONTENT",
|
|
||||||
"PARSING_ERROR",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
network_has_code = True
|
|
||||||
for line in scl_code.splitlines():
|
|
||||||
scl_output.append(f" {line}") # Indentar código
|
|
||||||
# Añadir línea en blanco después de cada bloque SCL para legibilidad
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Si la red no produjo código SCL imprimible (ej. solo lógica interna)
|
|
||||||
if (
|
|
||||||
not network_has_code and network_lang != "STL"
|
|
||||||
): # No añadir para STL ya comentado
|
|
||||||
scl_output.append(
|
|
||||||
f" // Network {i+1} did not produce printable SCL code."
|
|
||||||
)
|
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Fin del bloque FC/FB/OB
|
|
||||||
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
|
|
||||||
|
|
||||||
# --- Escritura del Archivo SCL (Común) ---
|
|
||||||
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
|
|
||||||
try:
|
try:
|
||||||
with open(output_scl_filepath, "w", encoding="utf-8") as f:
|
os.makedirs(output_directory, exist_ok=True)
|
||||||
for line in scl_output:
|
with open(output_filepath, "w", encoding="utf-8") as f:
|
||||||
|
for line in output_content:
|
||||||
f.write(line + "\n")
|
f.write(line + "\n")
|
||||||
print("Generación de SCL completada.")
|
print(f"Generación de {output_extension.upper()} completada.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al escribir el archivo SCL: {e}")
|
print(f"Error al escribir el archivo {output_extension.upper()}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
# --- Ejecución ---
|
# --- Ejecución ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Imports necesarios solo para la ejecución como script principal
|
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file.")
|
||||||
import argparse
|
parser.add_argument("source_xml_filepath", help="Path to the original source XML file.")
|
||||||
import os
|
args = parser.parse_args(); source_xml_file = args.source_xml_filepath
|
||||||
import sys
|
if not os.path.exists(source_xml_file): print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'.")
|
||||||
import traceback # Asegurarse que traceback está importado
|
|
||||||
|
|
||||||
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate final SCL file from processed JSON (_simplified_processed.json). Expects original XML filepath as argument."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"source_xml_filepath", # Argumento posicional obligatorio
|
|
||||||
help="Path to the original source XML file (passed from x0_main.py, used to derive input/output names).",
|
|
||||||
)
|
|
||||||
args = parser.parse_args() # Parsea los argumentos de sys.argv
|
|
||||||
|
|
||||||
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
|
|
||||||
|
|
||||||
# Verificar si el archivo XML original existe (como referencia)
|
|
||||||
if not os.path.exists(source_xml_file):
|
|
||||||
print(
|
|
||||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
# Asumir que los archivos están en el mismo directorio que el XML original
|
base_dir = os.path.dirname(source_xml_file)
|
||||||
base_dir = os.path.dirname(source_xml_file) # Directorio del XML original
|
parsing_dir = os.path.join(base_dir, "parsing")
|
||||||
|
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
|
||||||
input_json_file = os.path.join(
|
output_dir = base_dir
|
||||||
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
print(f"(x3) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}' en directorio: '{os.path.relpath(output_dir)}'")
|
||||||
)
|
|
||||||
# Cambiar extensión de salida a .scl
|
|
||||||
output_scl_file = os.path.join(
|
|
||||||
base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verificar si el archivo JSON procesado de entrada EXISTE
|
|
||||||
if not os.path.exists(input_json_file):
|
if not os.path.exists(input_json_file):
|
||||||
print(
|
print(f"Error Fatal (x3): JSON procesado no encontrado: '{input_json_file}'"); sys.exit(1)
|
||||||
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
|
|
||||||
)
|
|
||||||
sys.exit(1) # Salir si el archivo necesario no está
|
|
||||||
else:
|
else:
|
||||||
# Llamar a la función principal de generación SCL del script
|
try: generate_scl_or_markdown(input_json_file, output_dir); sys.exit(0)
|
||||||
try:
|
except Exception as e: print(f"Error Crítico (x3): {e}"); traceback.print_exc(); sys.exit(1)
|
||||||
generate_scl(input_json_file, output_scl_file)
|
|
||||||
sys.exit(0) # Salir con éxito explícitamente
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
|
|
||||||
)
|
|
||||||
# traceback ya debería estar importado
|
|
||||||
traceback.print_exc()
|
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
|
|
@ -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,147 @@
|
||||||
|
# generators/generate_scl_code_block.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||||
|
|
||||||
|
# Definir SCL_SUFFIX aquí porque se usa en _generate_scl_body
|
||||||
|
SCL_SUFFIX = "_sympy_processed"
|
||||||
|
|
||||||
|
def _generate_scl_header(data, scl_block_name):
|
||||||
|
"""Genera el encabezado SCL para FC/FB/OB."""
|
||||||
|
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" # Default for FB
|
||||||
|
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:")
|
||||||
|
for line in block_comment.splitlines():
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
|
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 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: # FB, OB
|
||||||
|
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
|
||||||
|
|
||||||
|
def _generate_scl_interface(interface_data):
|
||||||
|
"""Genera las secciones VAR_* de la interfaz SCL para FC/FB/OB."""
|
||||||
|
scl_output = []
|
||||||
|
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
||||||
|
declared_temps = set()
|
||||||
|
|
||||||
|
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()}"
|
||||||
|
if section_name == "Static": scl_section_keyword = "VAR_STAT" # Para FBs
|
||||||
|
if section_name == "Temp": scl_section_keyword = "VAR_TEMP"
|
||||||
|
if section_name == "Constant": scl_section_keyword = "CONSTANT"
|
||||||
|
|
||||||
|
scl_output.append(scl_section_keyword)
|
||||||
|
scl_output.extend(generate_scl_declarations(vars_in_section, indent_level=1))
|
||||||
|
scl_output.append("END_VAR" if section_name != "Constant" else "END_CONSTANT")
|
||||||
|
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
|
||||||
|
|
||||||
|
def _generate_scl_temp_vars(data, declared_temps):
|
||||||
|
"""Detecta y genera declaraciones VAR_TEMP adicionales."""
|
||||||
|
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)
|
||||||
|
for temp_name in found_temps:
|
||||||
|
if temp_name: temp_vars_detected.add(temp_name)
|
||||||
|
|
||||||
|
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||||
|
if additional_temps:
|
||||||
|
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
||||||
|
if not declared_temps:
|
||||||
|
scl_output.append("VAR_TEMP")
|
||||||
|
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 declared_temps:
|
||||||
|
scl_output.append("END_VAR")
|
||||||
|
scl_output.append("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def _generate_scl_body(networks):
|
||||||
|
"""Genera el cuerpo SCL (BEGIN...END) con la lógica de las redes."""
|
||||||
|
scl_output = ["BEGIN", ""]
|
||||||
|
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_line}") for stl_line in raw_stl_code.splitlines()]; 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: # SCL/LAD/FBD
|
||||||
|
for instruction in logic_in_network:
|
||||||
|
instruction_type = instruction.get("type", ""); scl_code = instruction.get("scl", ""); is_grouped = instruction.get("grouped", False)
|
||||||
|
if is_grouped: continue
|
||||||
|
if (instruction_type.endswith(SCL_SUFFIX) or instruction_type in ["RAW_SCL_CHUNK","UNSUPPORTED_LANG","UNSUPPORTED_CONTENT","PARSING_ERROR"] or "_error" in instruction_type) and scl_code:
|
||||||
|
is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines() if line.strip())
|
||||||
|
is_if_block = scl_code.strip().startswith("IF")
|
||||||
|
if (not is_only_comment or is_if_block or "_error" in instruction_type or instruction_type in ["UNSUPPORTED_LANG","UNSUPPORTED_CONTENT","PARSING_ERROR"]):
|
||||||
|
network_has_code = True; [scl_output.append(f" {line}") for line in scl_code.splitlines()]; 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("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def generate_scl_for_code_block(data):
|
||||||
|
"""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_lines, declared_temps = _generate_scl_interface(interface_data)
|
||||||
|
scl_output.extend(interface_lines)
|
||||||
|
scl_output.extend(_generate_scl_temp_vars(data, declared_temps))
|
||||||
|
scl_output.extend(_generate_scl_body(data.get("networks", [])))
|
||||||
|
scl_output.append(f"END_{scl_block_keyword}")
|
||||||
|
|
||||||
|
return scl_output
|
|
@ -0,0 +1,60 @@
|
||||||
|
# generators/generate_scl_db.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .generator_utils import format_variable_name, generate_scl_declarations
|
||||||
|
|
||||||
|
def _generate_scl_header(data, scl_block_name):
|
||||||
|
"""Genera el encabezado SCL para DB."""
|
||||||
|
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:")
|
||||||
|
for line in block_comment.splitlines():
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
|
scl_output.append("")
|
||||||
|
scl_output.append(f'DATA_BLOCK "{scl_block_name}"') # Keyword específica
|
||||||
|
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||||
|
scl_output.append("VERSION : 0.1")
|
||||||
|
scl_output.append("")
|
||||||
|
return scl_output
|
||||||
|
|
||||||
|
def _generate_scl_interface(interface_data):
|
||||||
|
"""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")
|
||||||
|
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
||||||
|
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
|
||||||
|
|
||||||
|
def generate_scl_for_db(data):
|
||||||
|
"""Genera el contenido SCL completo para un DATA_BLOCK."""
|
||||||
|
scl_output = []
|
||||||
|
scl_block_name = format_variable_name(data.get("block_name", "UnknownDB"))
|
||||||
|
|
||||||
|
# Generar cabecera
|
||||||
|
scl_output.extend(_generate_scl_header(data, scl_block_name))
|
||||||
|
|
||||||
|
# Generar interfaz
|
||||||
|
interface_data = data.get("interface", {})
|
||||||
|
scl_output.extend(_generate_scl_interface(interface_data))
|
||||||
|
|
||||||
|
# Generar cuerpo (vacío para DB)
|
||||||
|
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,150 @@
|
||||||
|
# generators/generator_utils.py
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
|
||||||
|
# --- Importar format_variable_name desde processors ---
|
||||||
|
# Es mejor mantenerlo centralizado si se usa en varios pasos.
|
||||||
|
try:
|
||||||
|
from processors.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 (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!).")
|
||||||
|
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 ---
|
||||||
|
|
||||||
|
# para formatear valores iniciales
|
||||||
|
def format_scl_start_value(value, datatype):
|
||||||
|
"""Formatea un valor para la inicialización SCL/Markdown según el tipo."""
|
||||||
|
if value is None: return None
|
||||||
|
datatype_lower = datatype.lower() if datatype else ""
|
||||||
|
value_str = str(value); value_str_unquoted = value_str
|
||||||
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: value_str_unquoted = value_str[1:-1]
|
||||||
|
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1: value_str_unquoted = value_str[1:-1]
|
||||||
|
|
||||||
|
# Integer-like
|
||||||
|
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:
|
||||||
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): return value_str_unquoted
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", "")
|
||||||
|
return f"'{escaped_for_scl}'" # Fallback as string
|
||||||
|
# Bool
|
||||||
|
elif "bool" in datatype_lower: return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
||||||
|
# String/Char
|
||||||
|
elif "string" in datatype_lower: escaped_value = value_str_unquoted.replace("'", "''"); return f"'{escaped_value}'"
|
||||||
|
elif "char" in datatype_lower: escaped_value = value_str_unquoted.replace("'", "''"); return f"'{escaped_value}'"
|
||||||
|
# Real
|
||||||
|
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
||||||
|
try:
|
||||||
|
f_val = float(value_str_unquoted); s_val = str(f_val)
|
||||||
|
if "." not in s_val and "e" not in s_val.lower(): s_val += ".0"
|
||||||
|
return s_val
|
||||||
|
except ValueError:
|
||||||
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted): return value_str_unquoted
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'" # Fallback
|
||||||
|
# Time
|
||||||
|
elif "time" in datatype_lower:
|
||||||
|
prefix, val_to_use = "", value_str_unquoted
|
||||||
|
if val_to_use.upper().startswith("T#"): prefix, val_to_use = "T#", val_to_use[2:]
|
||||||
|
elif val_to_use.upper().startswith("LT#"): prefix, val_to_use = "LT#", val_to_use[3:]
|
||||||
|
elif val_to_use.upper().startswith("S5T#"): prefix, val_to_use = "S5T#", val_to_use[4:]
|
||||||
|
if "s5time" in datatype_lower: return f"S5T#{val_to_use}"
|
||||||
|
elif "ltime" in datatype_lower: return f"LT#{val_to_use}"
|
||||||
|
else: return f"T#{val_to_use}"
|
||||||
|
# Date/Time Of Day
|
||||||
|
elif "date" in datatype_lower: # Must check DTL/DT/TOD first
|
||||||
|
val_to_use = value_str_unquoted
|
||||||
|
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
|
||||||
|
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"; val_to_use = val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
elif "dt" in datatype_lower:
|
||||||
|
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"; val_to_use = val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
||||||
|
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"; val_to_use = val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
else: # Default to Date D#
|
||||||
|
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"; val_to_use = val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use; return f"{prefix}{val_to_use}"
|
||||||
|
# Fallback
|
||||||
|
else:
|
||||||
|
if re.match(r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str): # Check if it looks like a symbol/path
|
||||||
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: return value_str[1:-1] # UDT literal?
|
||||||
|
if '"' in value_str and "." in value_str and value_str.count('"') == 2: return value_str # DB access?
|
||||||
|
if not value_str.startswith('"') and not value_str.startswith("'"):
|
||||||
|
if value_str.startswith("#") or value_str.startswith("%"): return value_str # Temp or Absolute
|
||||||
|
else: return value_str # Symbolic constant?
|
||||||
|
return value_str # Other complex string?
|
||||||
|
else: # Final fallback: treat as string literal
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'"
|
||||||
|
|
||||||
|
def generate_scl_declarations(variables, indent_level=1):
|
||||||
|
"""Genera las líneas SCL para declarar variables, structs y arrays."""
|
||||||
|
scl_lines = []
|
||||||
|
indent = " " * indent_level
|
||||||
|
for var in variables:
|
||||||
|
var_name_scl = format_variable_name(var.get("name"))
|
||||||
|
var_dtype_raw = var.get("datatype", "VARIANT")
|
||||||
|
var_comment = var.get("comment")
|
||||||
|
start_value = var.get("start_value")
|
||||||
|
children = var.get("children")
|
||||||
|
array_elements = var.get("array_elements")
|
||||||
|
|
||||||
|
# Limpiar y determinar tipo base
|
||||||
|
var_dtype_cleaned = var_dtype_raw
|
||||||
|
if isinstance(var_dtype_raw, str):
|
||||||
|
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'): var_dtype_cleaned = var_dtype_raw[1:-1]
|
||||||
|
array_match = re.match(r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE)
|
||||||
|
if array_match: var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}"
|
||||||
|
base_type_for_init = var_dtype_cleaned
|
||||||
|
array_prefix_for_decl = ""
|
||||||
|
if isinstance(var_dtype_cleaned, str) and var_dtype_cleaned.lower().startswith("array["): # Check if string before lower()
|
||||||
|
match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE)
|
||||||
|
if match: array_prefix_for_decl, base_type_for_init = match.group(1), match.group(2).strip()
|
||||||
|
|
||||||
|
# Construir tipo para declaración
|
||||||
|
declaration_dtype = var_dtype_raw
|
||||||
|
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl: # Simple UDT/Complex
|
||||||
|
if isinstance(base_type_for_init, str) and not base_type_for_init.startswith('"'): declaration_dtype = f'"{base_type_for_init}"'
|
||||||
|
else: declaration_dtype = base_type_for_init
|
||||||
|
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned: # Array of UDT/Complex
|
||||||
|
if isinstance(base_type_for_init, str) and not base_type_for_init.startswith('"'): declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
|
||||||
|
else: declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
|
||||||
|
|
||||||
|
|
||||||
|
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
||||||
|
init_value_scl = None
|
||||||
|
|
||||||
|
# Manejar Arrays / Structs / Simples
|
||||||
|
if array_elements:
|
||||||
|
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: print(f"Advertencia: Índices array no numéricos para '{var_name_scl}'."); sorted_indices_str = sorted(array_elements.keys())
|
||||||
|
init_values = []
|
||||||
|
for idx_str in sorted_indices_str:
|
||||||
|
try: formatted_val = format_scl_start_value(array_elements[idx_str], base_type_for_init); init_values.append(formatted_val)
|
||||||
|
except Exception as e_fmt: print(f"ERROR formato array idx {idx_str} de '{var_name_scl}': {e_fmt}"); init_values.append(f"/*ERR_FMT_{idx_str}*/")
|
||||||
|
valid_inits = [v for v in init_values if v is not None]
|
||||||
|
if valid_inits: init_value_scl = f"[{', '.join(valid_inits)}]"
|
||||||
|
elif array_elements: print(f"Advertencia: Valores iniciales array '{var_name_scl}' son None/inválidos.")
|
||||||
|
elif children:
|
||||||
|
scl_lines.append(declaration_line); scl_lines.append(f"{indent}STRUCT")
|
||||||
|
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
||||||
|
scl_lines.append(f"{indent}END_STRUCT;")
|
||||||
|
if var_comment: scl_lines.append(f"{indent}// {var_comment}")
|
||||||
|
scl_lines.append(""); continue
|
||||||
|
else: # Simple
|
||||||
|
if start_value is not None:
|
||||||
|
try: init_value_scl = format_scl_start_value(start_value, base_type_for_init)
|
||||||
|
except Exception as e_fmt_simple: print(f"ERROR formato simple '{var_name_scl}': {e_fmt_simple}"); init_value_scl = f"/*ERR_FMT_SIMPLE*/"
|
||||||
|
|
||||||
|
# Añadir inicialización y comentario
|
||||||
|
if init_value_scl is not None: declaration_line += f" := {init_value_scl}"
|
||||||
|
declaration_line += ";"
|
||||||
|
if var_comment: declaration_line += f" // {var_comment}"
|
||||||
|
scl_lines.append(declaration_line)
|
||||||
|
return scl_lines
|
|
@ -128,6 +128,13 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Usar la ruta absoluta para los scripts hijos
|
# Usar la ruta absoluta para los scripts hijos
|
||||||
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
||||||
|
|
||||||
|
# Derivar nombres esperados para archivos intermedios (para depuración)
|
||||||
|
xml_base_name = os.path.splitext(os.path.basename(absolute_xml_filepath))[0]
|
||||||
|
xml_dir = os.path.dirname(absolute_xml_filepath)
|
||||||
|
parsing_dir = os.path.join(xml_dir, "parsing")
|
||||||
|
expected_json_file = os.path.join(parsing_dir, f"{xml_base_name}.json")
|
||||||
|
expected_processed_json = os.path.join(parsing_dir, f"{xml_base_name}_processed.json")
|
||||||
|
|
||||||
# Ejecutar los scripts en secuencia
|
# Ejecutar los scripts en secuencia
|
||||||
success = True
|
success = True
|
||||||
|
|
|
@ -438,9 +438,10 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# Derivar nombre de salida JSON
|
# Derivar nombre de salida JSON
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
|
||||||
output_dir = os.path.dirname(xml_input_file)
|
base_dir = os.path.dirname(xml_input_file)
|
||||||
|
output_dir = os.path.join(base_dir, "parsing")
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
json_output_file = os.path.join(output_dir, f"{xml_filename_base}_simplified.json")
|
json_output_file = os.path.join(output_dir, f"{xml_filename_base}.json")
|
||||||
|
|
||||||
print(f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'")
|
print(f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'")
|
||||||
|
|
||||||
|
|
|
@ -301,16 +301,16 @@ def load_processors(processors_dir="processors"):
|
||||||
|
|
||||||
|
|
||||||
# --- Bucle Principal de Procesamiento (MODIFICADO) ---
|
# --- Bucle Principal de Procesamiento (MODIFICADO) ---
|
||||||
def process_json_to_scl(json_filepath):
|
def process_json_to_scl(json_filepath, output_json_filepath):
|
||||||
"""
|
"""
|
||||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
|
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
|
||||||
y guarda JSON procesado.
|
y guarda JSON procesado en la ruta especificada.
|
||||||
"""
|
"""
|
||||||
global data
|
global data
|
||||||
|
|
||||||
if not os.path.exists(json_filepath):
|
if not os.path.exists(json_filepath):
|
||||||
print(f"Error: JSON no encontrado: {json_filepath}")
|
print(f"Error: JSON no encontrado: {json_filepath}")
|
||||||
return
|
return False
|
||||||
print(f"Cargando JSON desde: {json_filepath}")
|
print(f"Cargando JSON desde: {json_filepath}")
|
||||||
try:
|
try:
|
||||||
with open(json_filepath, "r", encoding="utf-8") as f:
|
with open(json_filepath, "r", encoding="utf-8") as f:
|
||||||
|
@ -318,7 +318,7 @@ def process_json_to_scl(json_filepath):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al cargar JSON: {e}")
|
print(f"Error al cargar JSON: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return
|
return False
|
||||||
|
|
||||||
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
|
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
|
||||||
block_type = data.get("block_type", "Unknown")
|
block_type = data.get("block_type", "Unknown")
|
||||||
|
@ -327,18 +327,16 @@ def process_json_to_scl(json_filepath):
|
||||||
# --- MODIFICADO: SALTAR PROCESAMIENTO PARA DB, UDT, TAG TABLE ---
|
# --- MODIFICADO: SALTAR PROCESAMIENTO PARA DB, UDT, TAG TABLE ---
|
||||||
if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable"]: # <-- Comprobar tipos a saltar
|
if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable"]: # <-- Comprobar tipos a saltar
|
||||||
print(f"INFO: El bloque es {block_type}. Saltando procesamiento lógico de x2.")
|
print(f"INFO: El bloque es {block_type}. Saltando procesamiento lógico de x2.")
|
||||||
output_filename = json_filepath.replace(
|
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_json_filepath}")
|
||||||
"_simplified.json", "_simplified_processed.json"
|
|
||||||
)
|
|
||||||
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_filename}")
|
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_json_filepath, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
print(f"Guardado de {block_type} completado.")
|
print(f"Guardado de {block_type} completado.")
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error Crítico al guardar JSON de {block_type}: {e}")
|
print(f"Error Crítico al guardar JSON de {block_type}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return # <<< SALIR TEMPRANO PARA DB/UDT/TAG TABLE
|
return False
|
||||||
|
|
||||||
# --- SI NO ES DB/UDT/TAG TABLE (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
|
# --- SI NO ES DB/UDT/TAG TABLE (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
|
||||||
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...")
|
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...")
|
||||||
|
@ -349,7 +347,7 @@ def process_json_to_scl(json_filepath):
|
||||||
processor_map, sorted_processors = load_processors(processors_dir_path)
|
processor_map, sorted_processors = load_processors(processors_dir_path)
|
||||||
if not processor_map:
|
if not processor_map:
|
||||||
print("Error crítico: No se cargaron procesadores. Abortando.")
|
print("Error crítico: No se cargaron procesadores. Abortando.")
|
||||||
return
|
return False
|
||||||
|
|
||||||
network_access_maps = {}
|
network_access_maps = {}
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
|
@ -484,18 +482,21 @@ def process_json_to_scl(json_filepath):
|
||||||
for detail in unprocessed_details: print(detail)
|
for detail in unprocessed_details: print(detail)
|
||||||
else: print("INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas.")
|
else: print("INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas.")
|
||||||
|
|
||||||
output_filename = json_filepath.replace("_simplified.json", "_simplified_processed.json")
|
print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}")
|
||||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}")
|
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False)
|
with open(output_json_filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
print("Guardado completado.")
|
print("Guardado completado.")
|
||||||
except Exception as e: print(f"Error Crítico al guardar JSON procesado: {e}"); traceback.print_exc()
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error Crítico al guardar JSON procesado: {e}");
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- Ejecución (MODIFICADO) ---
|
||||||
# --- Ejecución (SIN CAMBIOS) ---
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Process simplified JSON (_simplified.json) to embed SCL logic (SymPy version). Expects original XML filepath as argument.")
|
parser = argparse.ArgumentParser(description="Process simplified JSON to embed SCL logic. Expects original XML filepath as argument.")
|
||||||
parser.add_argument("source_xml_filepath", help="Path to the original source XML file (passed from x0_main.py, used to derive JSON input name).")
|
parser.add_argument("source_xml_filepath", help="Path to the original source XML file (passed from x0_main.py).")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
source_xml_file = args.source_xml_filepath
|
source_xml_file = args.source_xml_filepath
|
||||||
|
|
||||||
|
@ -503,21 +504,27 @@ if __name__ == "__main__":
|
||||||
print(f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.")
|
print(f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.")
|
||||||
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
input_dir = os.path.dirname(source_xml_file)
|
base_dir = os.path.dirname(source_xml_file)
|
||||||
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
|
parsing_dir = os.path.join(base_dir, "parsing")
|
||||||
output_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified_processed.json")
|
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}.json")
|
||||||
|
output_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
|
||||||
|
|
||||||
|
os.makedirs(parsing_dir, exist_ok=True)
|
||||||
|
|
||||||
print(f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'")
|
print(f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'")
|
||||||
|
|
||||||
if not os.path.exists(input_json_file):
|
if not os.path.exists(input_json_file):
|
||||||
print(f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'")
|
print(f"Error Fatal (x2): El archivo de entrada JSON no existe: '{input_json_file}'")
|
||||||
print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.")
|
print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
process_json_to_scl(input_json_file)
|
success = process_json_to_scl(input_json_file, output_json_file)
|
||||||
|
if success:
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
|
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
|
@ -5,483 +5,29 @@ import os
|
||||||
import re
|
import re
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import traceback # Importar traceback para errores
|
import traceback
|
||||||
|
|
||||||
# --- Importar Utilidades y Constantes (Asumiendo ubicación) ---
|
# --- Importar Generadores Específicos ---
|
||||||
try:
|
try:
|
||||||
# Intenta importar desde el paquete de procesadores si está estructurado así
|
from generators.generate_scl_db import generate_scl_for_db
|
||||||
from processors.processor_utils import format_variable_name
|
from generators.generate_scl_code_block import generate_scl_for_code_block
|
||||||
|
from generators.generate_md_udt import generate_udt_markdown
|
||||||
|
from generators.generate_md_tag_table import generate_tag_table_markdown
|
||||||
|
# Importar format_variable_name (necesario para el nombre de archivo)
|
||||||
|
from generators.generator_utils import format_variable_name
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error crítico: No se pudieron importar los módulos de 'generators': {e}")
|
||||||
|
print("Asegúrate de que el directorio 'generators' y sus archivos .py existen.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Definir SCL_SUFFIX aquí o importarlo si está centralizado
|
# --- Función Principal de Generación (Despachador) ---
|
||||||
SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py
|
|
||||||
GROUPED_COMMENT = (
|
|
||||||
"// Logic included in grouped IF" # Opcional, si se usa para filtrar
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
print(
|
|
||||||
"Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils."
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Implementación local BÁSICA como fallback (MENOS RECOMENDADA)
|
|
||||||
def format_variable_name(name):
|
|
||||||
if not name:
|
|
||||||
return "_INVALID_NAME_"
|
|
||||||
if name.startswith('"') and name.endswith('"'):
|
|
||||||
return name # Mantener comillas
|
|
||||||
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
|
|
||||||
|
|
||||||
SCL_SUFFIX = "_sympy_processed"
|
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
|
||||||
|
|
||||||
|
|
||||||
# para formatear valores iniciales
|
|
||||||
def format_scl_start_value(value, datatype):
|
|
||||||
"""Formatea un valor para la inicialización SCL según el tipo."""
|
|
||||||
# Add initial debug print
|
|
||||||
# print(f"DEBUG format_scl_start_value: value='{value}', datatype='{datatype}'")
|
|
||||||
if value is None:
|
|
||||||
return None # Retornar None si no hay valor
|
|
||||||
datatype_lower = datatype.lower() if datatype else ""
|
|
||||||
value_str = str(value)
|
|
||||||
|
|
||||||
# Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE)
|
|
||||||
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1:
|
|
||||||
value_str_unquoted = value_str[1:-1]
|
|
||||||
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1:
|
|
||||||
value_str_unquoted = value_str[1:-1]
|
|
||||||
else:
|
|
||||||
value_str_unquoted = value_str
|
|
||||||
|
|
||||||
# --- Integer-like types ---
|
|
||||||
if any(
|
|
||||||
t in datatype_lower
|
|
||||||
for t in [
|
|
||||||
"int",
|
|
||||||
"byte",
|
|
||||||
"word",
|
|
||||||
"dint",
|
|
||||||
"dword",
|
|
||||||
"lint",
|
|
||||||
"lword",
|
|
||||||
"sint",
|
|
||||||
"usint",
|
|
||||||
"uint",
|
|
||||||
"udint",
|
|
||||||
"ulint",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# Intentar convertir el valor (sin comillas) a entero
|
|
||||||
return str(int(value_str_unquoted))
|
|
||||||
except ValueError:
|
|
||||||
# Si no es un entero válido, podría ser una constante simbólica
|
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
|
||||||
return value_str_unquoted # Devolver como símbolo
|
|
||||||
|
|
||||||
# --- Fallback for non-integer, non-symbol ---
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback for int-like. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
|
||||||
) # More debug
|
|
||||||
# MODIFIED FALLBACK: Escape newlines and use repr() for safety before formatting
|
|
||||||
try:
|
|
||||||
# Escape backslashes and single quotes properly for SCL string literal
|
|
||||||
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace(
|
|
||||||
"'", "''"
|
|
||||||
)
|
|
||||||
# Remove potential newlines that break Python f-string; SCL strings usually don't span lines implicitly
|
|
||||||
escaped_for_scl = escaped_for_scl.replace("\n", "").replace("\r", "")
|
|
||||||
# Format as SCL string literal
|
|
||||||
formatted_scl_string = f"'{escaped_for_scl}'"
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback result='{formatted_scl_string}'"
|
|
||||||
)
|
|
||||||
return formatted_scl_string
|
|
||||||
except Exception as format_exc:
|
|
||||||
print(
|
|
||||||
f"ERROR format_scl_start_value: Exception during fallback formatting: {format_exc}"
|
|
||||||
)
|
|
||||||
return f"'ERROR_FORMATTING_{value_str_unquoted[:20]}'" # Return an error string
|
|
||||||
|
|
||||||
# --- Other types (Bool, Real, String, Char, Time, Date, etc.) ---
|
|
||||||
elif "bool" in datatype_lower:
|
|
||||||
# Comparar sin importar mayúsculas/minúsculas y sin comillas
|
|
||||||
return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
|
||||||
elif "string" in datatype_lower:
|
|
||||||
# Usar el valor sin comillas originales y escapar las internas
|
|
||||||
escaped_value = value_str_unquoted.replace("'", "''")
|
|
||||||
return f"'{escaped_value}'"
|
|
||||||
elif "char" in datatype_lower:
|
|
||||||
# Usar el valor sin comillas originales y escapar las internas
|
|
||||||
escaped_value = value_str_unquoted.replace("'", "''")
|
|
||||||
# SCL usa comillas simples para Char. Asegurar que sea un solo caracter si es posible?
|
|
||||||
# Por ahora, solo formatear. Longitud se verifica en TIA.
|
|
||||||
return f"'{escaped_value}'"
|
|
||||||
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
|
||||||
try:
|
|
||||||
# Intentar convertir a float
|
|
||||||
f_val = float(value_str_unquoted)
|
|
||||||
s_val = str(f_val)
|
|
||||||
# Asegurar que tenga punto decimal si es entero
|
|
||||||
if "." not in s_val and "e" not in s_val.lower():
|
|
||||||
s_val += ".0"
|
|
||||||
return s_val
|
|
||||||
except ValueError:
|
|
||||||
# Podría ser constante simbólica
|
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
|
||||||
return value_str_unquoted
|
|
||||||
print(
|
|
||||||
f"Advertencia: Valor '{value_str}' no reconocido como real o símbolo para tipo {datatype}. Devolviendo como string."
|
|
||||||
)
|
|
||||||
# Use the robust fallback formatting here too
|
|
||||||
escaped_for_scl = (
|
|
||||||
value_str_unquoted.replace("\\", "\\\\")
|
|
||||||
.replace("'", "''")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
)
|
|
||||||
return f"'{escaped_for_scl}'"
|
|
||||||
elif "time" in datatype_lower:
|
|
||||||
# Quitar prefijos y añadir el correcto según el tipo específico
|
|
||||||
prefix = ""
|
|
||||||
val_to_use = value_str_unquoted # Usar valor sin comillas
|
|
||||||
if val_to_use.upper().startswith("T#"):
|
|
||||||
prefix = "T#"
|
|
||||||
val_to_use = val_to_use[2:]
|
|
||||||
elif val_to_use.upper().startswith("LT#"):
|
|
||||||
prefix = "LT#"
|
|
||||||
val_to_use = val_to_use[3:]
|
|
||||||
elif val_to_use.upper().startswith("S5T#"):
|
|
||||||
prefix = "S5T#"
|
|
||||||
val_to_use = val_to_use[4:]
|
|
||||||
|
|
||||||
if "s5time" in datatype_lower:
|
|
||||||
return f"S5T#{val_to_use}"
|
|
||||||
elif "ltime" in datatype_lower:
|
|
||||||
return f"LT#{val_to_use}"
|
|
||||||
else:
|
|
||||||
return f"T#{val_to_use}" # Default a TIME
|
|
||||||
elif "date" in datatype_lower:
|
|
||||||
val_to_use = value_str_unquoted
|
|
||||||
# Handle DTL first as it's longer
|
|
||||||
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
|
|
||||||
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
elif "dt" in datatype_lower:
|
|
||||||
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
|
||||||
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
else: # Default a Date D#
|
|
||||||
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"
|
|
||||||
val_to_use = (
|
|
||||||
val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use
|
|
||||||
)
|
|
||||||
return f"{prefix}{val_to_use}"
|
|
||||||
|
|
||||||
# --- Fallback for completely unknown types or complex structures ---
|
|
||||||
else:
|
|
||||||
# Si es un nombre válido (posiblemente UDT, constante global, etc.), devolverlo tal cual
|
|
||||||
# Ajustar regex para permitir más caracteres si es necesario
|
|
||||||
if re.match(
|
|
||||||
r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str
|
|
||||||
): # Permitir % para accesos tipo %DB1.DBD0
|
|
||||||
# Quitar comillas externas si es un UDT o struct complejo
|
|
||||||
if (
|
|
||||||
value_str.startswith('"')
|
|
||||||
and value_str.endswith('"')
|
|
||||||
and len(value_str) > 1
|
|
||||||
):
|
|
||||||
return value_str[1:-1]
|
|
||||||
# Mantener comillas si es acceso a DB ("DB_Name".Var)
|
|
||||||
if '"' in value_str and "." in value_str and value_str.count('"') == 2:
|
|
||||||
return value_str
|
|
||||||
# Si no tiene comillas y es un nombre simple o acceso #temp o %I0.0 etc
|
|
||||||
if not value_str.startswith('"') and not value_str.startswith("'"):
|
|
||||||
# Formatear nombres simples, pero dejar accesos % y # tal cual
|
|
||||||
if value_str.startswith("#") or value_str.startswith("%"):
|
|
||||||
return value_str
|
|
||||||
else:
|
|
||||||
# return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante
|
|
||||||
return value_str # Return as is if it looks symbolic
|
|
||||||
# Devolver el valor original si tiene comillas internas o estructura compleja no manejada arriba
|
|
||||||
return value_str
|
|
||||||
else:
|
|
||||||
# Si no parece un nombre/símbolo/acceso, tratarlo como string (último recurso)
|
|
||||||
print(
|
|
||||||
f"DEBUG format_scl_start_value: Fallback final. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
|
||||||
)
|
|
||||||
# Use the robust fallback formatting
|
|
||||||
escaped_for_scl = (
|
|
||||||
value_str_unquoted.replace("\\", "\\\\")
|
|
||||||
.replace("'", "''")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
)
|
|
||||||
return f"'{escaped_for_scl}'"
|
|
||||||
|
|
||||||
|
|
||||||
# ... (generate_scl_declarations and generate_scl function remain the same as the previous version) ...
|
|
||||||
# --- (Incluye aquí las funciones generate_scl_declarations y generate_scl SIN CAMBIOS respecto a la respuesta anterior) ---
|
|
||||||
|
|
||||||
|
|
||||||
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
|
|
||||||
def generate_scl_declarations(variables, indent_level=1):
|
|
||||||
"""Genera las líneas SCL para declarar variables, structs y arrays."""
|
|
||||||
scl_lines = []
|
|
||||||
indent = " " * indent_level
|
|
||||||
for var in variables:
|
|
||||||
var_name_scl = format_variable_name(var.get("name"))
|
|
||||||
var_dtype_raw = var.get("datatype", "VARIANT")
|
|
||||||
var_comment = var.get("comment")
|
|
||||||
start_value = var.get("start_value")
|
|
||||||
children = var.get("children") # Para structs
|
|
||||||
array_elements = var.get("array_elements") # Para arrays
|
|
||||||
|
|
||||||
# Limpiar comillas del tipo de dato si es UDT/String/etc.
|
|
||||||
var_dtype_cleaned = var_dtype_raw
|
|
||||||
if isinstance(var_dtype_raw, str):
|
|
||||||
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'):
|
|
||||||
var_dtype_cleaned = var_dtype_raw[1:-1]
|
|
||||||
# Manejar caso 'Array [...] of "MyUDT"'
|
|
||||||
array_match = re.match(
|
|
||||||
r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE
|
|
||||||
)
|
|
||||||
if array_match:
|
|
||||||
var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
|
|
||||||
|
|
||||||
# Determinar tipo base para inicialización (importante para arrays)
|
|
||||||
base_type_for_init = var_dtype_cleaned
|
|
||||||
array_prefix_for_decl = ""
|
|
||||||
if var_dtype_cleaned.lower().startswith("array["):
|
|
||||||
match = re.match(
|
|
||||||
r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
array_prefix_for_decl = match.group(1)
|
|
||||||
base_type_for_init = match.group(2).strip()
|
|
||||||
|
|
||||||
# Construir tipo de dato para la declaración SCL
|
|
||||||
declaration_dtype = var_dtype_raw # Usar el raw por defecto
|
|
||||||
# Si es UDT o tipo complejo que requiere comillas y no es array simple
|
|
||||||
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl:
|
|
||||||
# Poner comillas si no las tiene ya el tipo base
|
|
||||||
if not base_type_for_init.startswith('"'):
|
|
||||||
declaration_dtype = f'"{base_type_for_init}"'
|
|
||||||
else:
|
|
||||||
declaration_dtype = base_type_for_init # Ya tiene comillas
|
|
||||||
# Si es array de UDT/complejo, reconstruir con comillas en el tipo base
|
|
||||||
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned:
|
|
||||||
if not base_type_for_init.startswith('"'):
|
|
||||||
declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
|
|
||||||
else:
|
|
||||||
declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
|
|
||||||
|
|
||||||
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
|
||||||
init_value_scl = None
|
|
||||||
|
|
||||||
# ---- Arrays ----
|
|
||||||
if array_elements:
|
|
||||||
# Ordenar índices (asumiendo que son numéricos '0', '1', ...)
|
|
||||||
try:
|
|
||||||
# Extraer números de los índices string
|
|
||||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
|
||||||
sorted_indices = sorted(indices_numeric.keys())
|
|
||||||
# Mapear de nuevo a string para buscar valor
|
|
||||||
sorted_indices_str = [str(k) for k in sorted_indices]
|
|
||||||
except ValueError:
|
|
||||||
# Fallback a orden alfabético si los índices no son números
|
|
||||||
print(
|
|
||||||
f"Advertencia: Índices de array no numéricos para '{var_name_scl}'. Usando orden alfabético."
|
|
||||||
)
|
|
||||||
sorted_indices_str = sorted(array_elements.keys())
|
|
||||||
|
|
||||||
init_values = []
|
|
||||||
for idx_str in sorted_indices_str:
|
|
||||||
try:
|
|
||||||
formatted_val = format_scl_start_value(
|
|
||||||
array_elements[idx_str], base_type_for_init
|
|
||||||
)
|
|
||||||
init_values.append(formatted_val)
|
|
||||||
except Exception as e_fmt:
|
|
||||||
print(
|
|
||||||
f"ERROR: Falló formateo para índice {idx_str} de array '{var_name_scl}'. Valor: {array_elements[idx_str]}. Error: {e_fmt}"
|
|
||||||
)
|
|
||||||
init_values.append(f"/*ERR_FMT_{idx_str}*/") # Placeholder de error
|
|
||||||
|
|
||||||
# Filtrar Nones que pueden venir de format_scl_start_value si el valor era None
|
|
||||||
valid_inits = [v for v in init_values if v is not None]
|
|
||||||
if valid_inits:
|
|
||||||
# Si todos los valores son iguales y es un array grande, podríamos usar notación x(value)
|
|
||||||
# Simplificación: por ahora, listar todos
|
|
||||||
init_value_scl = f"[{', '.join(valid_inits)}]"
|
|
||||||
elif array_elements: # Si había elementos pero todos formatearon a None
|
|
||||||
print(
|
|
||||||
f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos."
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- Structs ----
|
|
||||||
elif children:
|
|
||||||
# El valor inicial de un struct se maneja recursivamente dentro
|
|
||||||
# Añadir comentario? Puede ser redundante.
|
|
||||||
scl_lines.append(
|
|
||||||
declaration_line
|
|
||||||
) # Añadir línea de declaración base STRUCT
|
|
||||||
scl_lines.append(f"{indent}STRUCT")
|
|
||||||
# Llamada recursiva para los miembros internos
|
|
||||||
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
|
||||||
scl_lines.append(f"{indent}END_STRUCT;")
|
|
||||||
if var_comment: # Comentario después de END_STRUCT
|
|
||||||
scl_lines.append(f"{indent}// {var_comment}")
|
|
||||||
scl_lines.append("") # Línea extra para legibilidad
|
|
||||||
continue # Saltar el resto de la lógica para este struct
|
|
||||||
|
|
||||||
# ---- Tipos Simples ----
|
|
||||||
else:
|
|
||||||
if start_value is not None:
|
|
||||||
try:
|
|
||||||
init_value_scl = format_scl_start_value(
|
|
||||||
start_value, base_type_for_init
|
|
||||||
) # Usar tipo base
|
|
||||||
except Exception as e_fmt_simple:
|
|
||||||
print(
|
|
||||||
f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"
|
|
||||||
)
|
|
||||||
init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder
|
|
||||||
|
|
||||||
# Añadir inicialización si existe y no es None
|
|
||||||
if init_value_scl is not None:
|
|
||||||
declaration_line += f" := {init_value_scl}"
|
|
||||||
|
|
||||||
declaration_line += ";"
|
|
||||||
|
|
||||||
# Añadir comentario si existe
|
|
||||||
if var_comment:
|
|
||||||
declaration_line += f" // {var_comment}"
|
|
||||||
|
|
||||||
scl_lines.append(declaration_line)
|
|
||||||
|
|
||||||
return scl_lines
|
|
||||||
|
|
||||||
# --- NUEVAS FUNCIONES para generar Markdown ---
|
|
||||||
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:**")
|
|
||||||
for line in udt_comment.splitlines():
|
|
||||||
md_lines.append(f"> {line}")
|
|
||||||
md_lines.append("")
|
|
||||||
|
|
||||||
# Extraer miembros (asumiendo que están en interface['None'])
|
|
||||||
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("|---|---|---|---|")
|
|
||||||
# Usar una función auxiliar recursiva para manejar structs anidados
|
|
||||||
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
|
|
||||||
|
|
||||||
# --- generate_markdown_member_rows (MODIFICADA) ---
|
|
||||||
def generate_markdown_member_rows(members, level=0):
|
|
||||||
"""Función auxiliar recursiva para generar filas Markdown para miembros de UDT."""
|
|
||||||
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 ""
|
|
||||||
# CORRECCIÓN: Manejar el caso en que comment sea None
|
|
||||||
comment_raw = member.get("comment")
|
|
||||||
comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else "" # Usar "" si es None
|
|
||||||
|
|
||||||
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_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 " " # Evitar None en la tabla
|
|
||||||
comment = (
|
|
||||||
tag.get("comment", "").replace("|", "\|").replace("\n", " ")
|
|
||||||
) # Escapar pipes
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# --- Función Principal de Generación (MODIFICADA) ---
|
|
||||||
def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||||
"""
|
"""
|
||||||
Genera un archivo SCL o Markdown a partir del JSON procesado,
|
Genera un archivo SCL o Markdown a partir del JSON procesado,
|
||||||
eligiendo el formato y la extensión según el tipo de bloque.
|
llamando a la función generadora apropiada y escribiendo el archivo.
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(processed_json_filepath):
|
if not os.path.exists(processed_json_filepath):
|
||||||
print(
|
print(f"Error: JSON no encontrado: '{processed_json_filepath}'")
|
||||||
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
|
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
|
||||||
|
@ -489,285 +35,53 @@ def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||||
with open(processed_json_filepath, "r", encoding="utf-8") as f:
|
with open(processed_json_filepath, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al cargar o parsear JSON: {e}")
|
print(f"Error al cargar/parsear JSON: {e}"); traceback.print_exc(); return
|
||||||
traceback.print_exc()
|
|
||||||
return
|
|
||||||
|
|
||||||
# --- Extracción de Información y Determinación de Tipo ---
|
|
||||||
block_name = data.get("block_name", "UnknownBlock")
|
block_name = data.get("block_name", "UnknownBlock")
|
||||||
block_number = data.get("block_number")
|
block_type = data.get("block_type", "Unknown")
|
||||||
block_type = data.get(
|
scl_block_name = format_variable_name(block_name) # Nombre seguro para archivo
|
||||||
"block_type", "Unknown"
|
|
||||||
) # FC, FB, OB, GlobalDB, PlcUDT, PlcTagTable
|
|
||||||
block_comment = data.get("block_comment", "")
|
|
||||||
scl_block_name = format_variable_name(block_name)
|
|
||||||
output_content = []
|
output_content = []
|
||||||
output_extension = ".scl" # Default
|
output_extension = ".scl" # Default
|
||||||
|
|
||||||
print(
|
print(f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})")
|
||||||
f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Selección del Generador y Extensión ---
|
# --- Selección del Generador y Extensión ---
|
||||||
|
generation_function = None
|
||||||
if block_type == "PlcUDT":
|
if block_type == "PlcUDT":
|
||||||
print(" -> Modo de generación: UDT Markdown")
|
print(" -> Modo de generación: UDT Markdown")
|
||||||
output_content = generate_udt_markdown(data)
|
generation_function = generate_udt_markdown
|
||||||
output_extension = ".md"
|
output_extension = ".md"
|
||||||
elif block_type == "PlcTagTable":
|
elif block_type == "PlcTagTable":
|
||||||
print(" -> Modo de generación: Tag Table Markdown")
|
print(" -> Modo de generación: Tag Table Markdown")
|
||||||
output_content = generate_tag_table_markdown(data)
|
generation_function = generate_tag_table_markdown
|
||||||
output_extension = ".md"
|
output_extension = ".md"
|
||||||
elif block_type == "GlobalDB":
|
elif block_type == "GlobalDB":
|
||||||
print(" -> Modo de generación: DATA_BLOCK SCL")
|
print(" -> Modo de generación: DATA_BLOCK SCL")
|
||||||
|
generation_function = generate_scl_for_db
|
||||||
output_extension = ".scl"
|
output_extension = ".scl"
|
||||||
# (Lógica de generación SCL para DB como estaba antes)
|
|
||||||
output_content.append(f"// Block Type: {block_type}")
|
|
||||||
if block_name != scl_block_name:
|
|
||||||
output_content.append(f"// Block Name (Original): {block_name}")
|
|
||||||
if block_number:
|
|
||||||
output_content.append(f"// Block Number: {block_number}")
|
|
||||||
if block_comment:
|
|
||||||
output_content.append(f"// Block Comment:")
|
|
||||||
for line in block_comment.splitlines():
|
|
||||||
output_content.append(f"// {line}")
|
|
||||||
output_content.append("")
|
|
||||||
output_content.append(f'DATA_BLOCK "{scl_block_name}"')
|
|
||||||
output_content.append("{ S7_Optimized_Access := 'TRUE' }")
|
|
||||||
output_content.append("VERSION : 0.1")
|
|
||||||
output_content.append("")
|
|
||||||
interface_data = data.get("interface", {})
|
|
||||||
static_vars = interface_data.get("Static", [])
|
|
||||||
if static_vars:
|
|
||||||
output_content.append("VAR")
|
|
||||||
output_content.extend(
|
|
||||||
generate_scl_declarations(static_vars, indent_level=1)
|
|
||||||
)
|
|
||||||
output_content.append("END_VAR")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
|
||||||
)
|
|
||||||
output_content.append("VAR\nEND_VAR") # Añadir vacío
|
|
||||||
output_content.append("")
|
|
||||||
output_content.append("BEGIN")
|
|
||||||
output_content.append(" // Data Blocks have no executable code")
|
|
||||||
output_content.append("END_DATA_BLOCK")
|
|
||||||
|
|
||||||
elif block_type in ["FC", "FB", "OB"]:
|
elif block_type in ["FC", "FB", "OB"]:
|
||||||
print(f" -> Modo de generación: {block_type} SCL")
|
print(f" -> Modo de generación: {block_type} SCL")
|
||||||
|
generation_function = generate_scl_for_code_block
|
||||||
output_extension = ".scl"
|
output_extension = ".scl"
|
||||||
# (Lógica de generación SCL para FC/FB/OB como estaba antes)
|
else: # Tipo desconocido
|
||||||
scl_block_keyword = "FUNCTION_BLOCK"
|
print(f"Error: Tipo de bloque desconocido '{block_type}'. No se generará archivo.")
|
||||||
if block_type == "FC":
|
|
||||||
scl_block_keyword = "FUNCTION"
|
|
||||||
elif block_type == "OB":
|
|
||||||
scl_block_keyword = "ORGANIZATION_BLOCK"
|
|
||||||
|
|
||||||
output_content.append(f"// Block Type: {block_type}")
|
|
||||||
if block_name != scl_block_name:
|
|
||||||
output_content.append(f"// Block Name (Original): {block_name}")
|
|
||||||
if block_number:
|
|
||||||
output_content.append(f"// Block Number: {block_number}")
|
|
||||||
original_net_langs = set(
|
|
||||||
n.get("language", "Unknown") for n in data.get("networks", [])
|
|
||||||
)
|
|
||||||
output_content.append(
|
|
||||||
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
|
|
||||||
)
|
|
||||||
if block_comment:
|
|
||||||
output_content.append(f"// Block Comment:")
|
|
||||||
for line in block_comment.splitlines():
|
|
||||||
output_content.append(f"// {line}")
|
|
||||||
output_content.append("")
|
|
||||||
|
|
||||||
return_type = "Void"
|
|
||||||
interface_data = data.get("interface", {})
|
|
||||||
if scl_block_keyword == "FUNCTION" and 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 return_type_raw.lower().startswith("array")
|
|
||||||
):
|
|
||||||
return_type = f'"{return_type}"'
|
|
||||||
else:
|
|
||||||
return_type = return_type_raw
|
|
||||||
if scl_block_keyword == "FUNCTION":
|
|
||||||
output_content.append(
|
|
||||||
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
output_content.append(f'{scl_block_keyword} "{scl_block_name}"')
|
|
||||||
|
|
||||||
output_content.append("{ S7_Optimized_Access := 'TRUE' }")
|
|
||||||
output_content.append("VERSION : 0.1")
|
|
||||||
output_content.append("")
|
|
||||||
|
|
||||||
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
|
||||||
declared_temps = set()
|
|
||||||
has_declarations = False
|
|
||||||
for section_name in section_order:
|
|
||||||
vars_in_section = interface_data.get(section_name, [])
|
|
||||||
if vars_in_section:
|
|
||||||
has_declarations = True
|
|
||||||
scl_section_keyword = f"VAR_{section_name.upper()}"
|
|
||||||
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"
|
|
||||||
output_content.append(scl_section_keyword)
|
|
||||||
output_content.extend(
|
|
||||||
generate_scl_declarations(vars_in_section, indent_level=1)
|
|
||||||
)
|
|
||||||
output_content.append(
|
|
||||||
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
|
||||||
)
|
|
||||||
output_content.append("")
|
|
||||||
if section_name == "Temp":
|
|
||||||
declared_temps.update(
|
|
||||||
format_variable_name(v.get("name"))
|
|
||||||
for v in vars_in_section
|
|
||||||
if v.get("name")
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
for temp_name in found_temps:
|
|
||||||
if temp_name:
|
|
||||||
temp_vars_detected.add(temp_name)
|
|
||||||
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
|
||||||
if additional_temps:
|
|
||||||
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
|
||||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
|
||||||
output_content.append("VAR_TEMP")
|
|
||||||
for temp_name in additional_temps:
|
|
||||||
scl_name = format_variable_name(temp_name)
|
|
||||||
inferred_type = "Bool"
|
|
||||||
output_content.append(
|
|
||||||
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
|
||||||
)
|
|
||||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
|
||||||
output_content.append("END_VAR")
|
|
||||||
output_content.append("")
|
|
||||||
|
|
||||||
output_content.append("BEGIN")
|
|
||||||
output_content.append("")
|
|
||||||
for i, network in enumerate(data.get("networks", [])):
|
|
||||||
network_title = network.get("title", f'Network {network.get("id", i+1)}')
|
|
||||||
network_comment = network.get("comment", "")
|
|
||||||
network_lang = network.get("language", "LAD")
|
|
||||||
output_content.append(
|
|
||||||
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
|
||||||
)
|
|
||||||
if network_comment:
|
|
||||||
for line in network_comment.splitlines():
|
|
||||||
output_content.append(f" // {line}")
|
|
||||||
output_content.append("")
|
|
||||||
network_has_code = False
|
|
||||||
logic_in_network = network.get("logic", [])
|
|
||||||
if not logic_in_network:
|
|
||||||
output_content.append(f" // Network {i+1} has no logic elements.")
|
|
||||||
output_content.append("")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if network_lang == "STL":
|
|
||||||
if 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"
|
|
||||||
)
|
|
||||||
output_content.append(f" // --- BEGIN STL Network {i+1} ---")
|
|
||||||
for stl_line in raw_stl_code.splitlines():
|
|
||||||
output_content.append(f" // {stl_line}")
|
|
||||||
output_content.append(f" // --- END STL Network {i+1} ---")
|
|
||||||
output_content.append("")
|
|
||||||
else:
|
|
||||||
output_content.append(
|
|
||||||
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
|
||||||
)
|
|
||||||
output_content.append("")
|
|
||||||
else: # SCL/LAD/FBD
|
|
||||||
for instruction in logic_in_network:
|
|
||||||
instruction_type = instruction.get("type", "")
|
|
||||||
scl_code = instruction.get("scl", "")
|
|
||||||
is_grouped = instruction.get("grouped", False)
|
|
||||||
if is_grouped:
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
instruction_type.endswith(SCL_SUFFIX)
|
|
||||||
or instruction_type
|
|
||||||
in [
|
|
||||||
"RAW_SCL_CHUNK",
|
|
||||||
"UNSUPPORTED_LANG",
|
|
||||||
"UNSUPPORTED_CONTENT",
|
|
||||||
"PARSING_ERROR",
|
|
||||||
]
|
|
||||||
or "_error" in instruction_type
|
|
||||||
) and scl_code:
|
|
||||||
is_only_comment = all(
|
|
||||||
line.strip().startswith("//")
|
|
||||||
for line in scl_code.splitlines()
|
|
||||||
if line.strip()
|
|
||||||
)
|
|
||||||
is_if_block = scl_code.strip().startswith("IF")
|
|
||||||
if (
|
|
||||||
not is_only_comment
|
|
||||||
or is_if_block
|
|
||||||
or "_error" in instruction_type
|
|
||||||
or instruction_type
|
|
||||||
in [
|
|
||||||
"UNSUPPORTED_LANG",
|
|
||||||
"UNSUPPORTED_CONTENT",
|
|
||||||
"PARSING_ERROR",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
network_has_code = True
|
|
||||||
for line in scl_code.splitlines():
|
|
||||||
output_content.append(f" {line}")
|
|
||||||
output_content.append("")
|
|
||||||
if not network_has_code and network_lang != "STL":
|
|
||||||
output_content.append(
|
|
||||||
f" // Network {i+1} did not produce printable SCL/MD code."
|
|
||||||
)
|
|
||||||
output_content.append("")
|
|
||||||
output_content.append(f"END_{scl_block_keyword}")
|
|
||||||
|
|
||||||
else: # Tipo desconocido
|
|
||||||
print(
|
|
||||||
f"Error: Tipo de bloque desconocido '{block_type}' encontrado en JSON. No se generará archivo."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Escritura del Archivo de Salida (.scl o .md) ---
|
# --- Llamar a la función generadora ---
|
||||||
# Construir nombre de archivo de salida
|
if generation_function:
|
||||||
output_filename_base = (
|
try:
|
||||||
f"{scl_block_name}{output_extension}" # Usar nombre SCL seguro
|
output_content = generation_function(data)
|
||||||
)
|
except Exception as gen_e:
|
||||||
|
print(f"Error durante la generación de contenido para {block_type} '{scl_block_name}': {gen_e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return # No intentar escribir si la generación falla
|
||||||
|
|
||||||
|
# --- Escritura del Archivo de Salida ---
|
||||||
|
output_filename_base = f"{scl_block_name}{output_extension}"
|
||||||
output_filepath = os.path.join(output_directory, output_filename_base)
|
output_filepath = os.path.join(output_directory, output_filename_base)
|
||||||
|
|
||||||
print(f" -> Escribiendo archivo de salida en: {output_filepath}")
|
print(f" -> Escribiendo archivo de salida en: {output_filepath}")
|
||||||
try:
|
try:
|
||||||
# Crear directorio si no existe
|
|
||||||
os.makedirs(output_directory, exist_ok=True)
|
os.makedirs(output_directory, exist_ok=True)
|
||||||
with open(output_filepath, "w", encoding="utf-8") as f:
|
with open(output_filepath, "w", encoding="utf-8") as f:
|
||||||
for line in output_content:
|
for line in output_content:
|
||||||
|
@ -777,54 +91,20 @@ def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||||
print(f"Error al escribir el archivo {output_extension.upper()}: {e}")
|
print(f"Error al escribir el archivo {output_extension.upper()}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
# --- Ejecución ---
|
# --- Ejecución ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file.")
|
||||||
description="Generate final SCL or Markdown file from processed JSON (_simplified_processed.json)." # Actualizado
|
parser.add_argument("source_xml_filepath", help="Path to the original source XML file.")
|
||||||
)
|
args = parser.parse_args(); source_xml_file = args.source_xml_filepath
|
||||||
parser.add_argument(
|
if not os.path.exists(source_xml_file): print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'.")
|
||||||
"source_xml_filepath",
|
|
||||||
help="Path to the original source XML file (passed from x0_main.py, used to derive input/output names).",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
source_xml_file = args.source_xml_filepath
|
|
||||||
|
|
||||||
if not os.path.exists(source_xml_file):
|
|
||||||
print(
|
|
||||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
|
||||||
)
|
|
||||||
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
base_dir = os.path.dirname(source_xml_file)
|
base_dir = os.path.dirname(source_xml_file)
|
||||||
|
parsing_dir = os.path.join(base_dir, "parsing")
|
||||||
input_json_file = os.path.join(
|
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
|
||||||
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
output_dir = base_dir
|
||||||
)
|
print(f"(x3) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}' en directorio: '{os.path.relpath(output_dir)}'")
|
||||||
|
|
||||||
# MODIFICADO: El directorio de salida ahora es el mismo que el de entrada
|
|
||||||
output_dir = base_dir # Escribir .scl/.md en el mismo directorio
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"(x3) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}' en directorio: '{os.path.relpath(output_dir)}'"
|
|
||||||
) # Log actualizado
|
|
||||||
|
|
||||||
if not os.path.exists(input_json_file):
|
if not os.path.exists(input_json_file):
|
||||||
print(
|
print(f"Error Fatal (x3): JSON procesado no encontrado: '{input_json_file}'"); sys.exit(1)
|
||||||
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try: generate_scl_or_markdown(input_json_file, output_dir); sys.exit(0)
|
||||||
# Pasar el directorio de salida a la función principal
|
except Exception as e: print(f"Error Crítico (x3): {e}"); traceback.print_exc(); sys.exit(1)
|
||||||
generate_scl_or_markdown(input_json_file, output_dir)
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Error Crítico (x3) durante la generación de SCL/MD desde '{input_json_file}': {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
|
||||||
sys.exit(1)
|
|
Loading…
Reference in New Issue