Uso del subdirectorio parsing para una implementacion mas limpia

This commit is contained in:
Miguel 2025-04-20 19:39:46 +02:00
parent 546705f8ca
commit 60fea74ebf
21 changed files with 1495 additions and 2281 deletions

View File

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -15,45 +15,44 @@ ns = {
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:
return ""
return "" # Devolver cadena vacía si el elemento es None
try:
# Intenta buscar el idioma por defecto
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)
# CORRECCIÓN: Devolver "" si .text es None
if text_items_default and text_items_default[0].text is not None:
return text_items_default[0].text.strip()
# Intenta buscar el idioma de fallback
# Intentar buscar el idioma de fallback
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)
# CORRECCIÓN: Devolver "" si .text es None
if text_items_fallback and text_items_fallback[0].text is not None:
return text_items_fallback[0].text.strip()
# Si no encuentra ninguno, toma el primer texto que encuentre
xpath_expr_any = ".//iface:MultilingualTextItem/iface:AttributeList/iface:Text"
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:
return text_items_any[0].text.strip()
# Fallback si MultilingualText está vacío o tiene una estructura inesperada
return ""
# Fallback final si no se encontró ningún MultilingualTextItem con texto
return "" # Asegurar retorno de string vacío
except Exception as e:
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
# 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):
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
# Adaptado para usar namespace flg
if symbol_element is None:
return None
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)
# Formatear correctamente con comillas dobles si es necesario (ej. DBs)
return (
".".join(
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):
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
# Adaptado para usar namespace flg
if access_element is None:
return None
uid = access_element.get("UId")
scope = access_element.get("Scope")
info = {"uid": uid, "scope": scope, "type": "unknown"}
# Buscar Symbol o Constant usando el namespace flg
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
constant = access_element.xpath("./flg:Constant", namespaces=ns)
if symbol:
info["type"] = "variable"
# Llamar a get_symbol_name que ahora espera flg:Symbol
info["name"] = get_symbol_name(symbol[0])
if info["name"] is None:
info["type"] = "error_parsing_symbol"
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
# Intentar extraer texto directamente como fallback muy básico
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
info["name"] = (
f'"_ERR_PARSING_{raw_text[:20]}"'
if raw_text
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
)
# return info # Podríamos devolver el error aquí
elif constant:
info["type"] = "constant"
# Buscar ConstantType y ConstantValue usando el namespace flg
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
# Extraer texto
info["datatype"] = (
const_type_elem[0].text.strip()
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
else None
)
if value_str is None:
info["type"] = "error_parsing_constant"
info["value"] = None
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:
val_lower = value_str.lower()
if val_lower in ["true", "false"]:
@ -127,15 +113,14 @@ def parse_access(access_element):
elif value_str.isdigit() or (
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:
try:
float(value_str)
info["datatype"] = "Real" # O LReal? Real es más seguro
info["datatype"] = "Real"
except ValueError:
pass # Podría ser string con punto
pass
elif "#" in value_str:
# Inferir tipo desde prefijo (T#, DT#, '...', etc.)
parts = value_str.split("#", 1)
prefix = parts[0].upper()
if prefix == "T":
@ -152,21 +137,14 @@ def parse_access(access_element):
info["datatype"] = "DTL"
elif prefix == "TOD":
info["datatype"] = "Time_Of_Day"
# Añadir más prefijos si es necesario (WSTRING#, STRING#, etc.)
elif value_str.startswith("'") and value_str.endswith("'"):
info["datatype"] = "String" # O Char? String es más probable
info["datatype"] = "String"
else:
info["datatype"] = (
"TypedConstant" # Genérico si no se reconoce prefijo
)
info["datatype"] = "TypedConstant"
elif value_str.startswith("'") and value_str.endswith("'"):
info["datatype"] = "String" # O Char?
info["value"] = value_str # Guardar valor original
# Intentar conversión numérica/booleana (igual que antes)
info["datatype"] = "String"
info["value"] = value_str
dtype_lower = info["datatype"].lower()
# Quitar prefijo y comillas para la conversión
val_str_processed = value_str
if isinstance(value_str, str):
if "#" in value_str:
@ -198,29 +176,19 @@ def parse_access(access_element):
)
elif dtype_lower in ["real", "lreal"]:
info["value"] = float(val_str_processed)
# Mantener string para otros tipos (Time, Date, String, Char, TypedConstant)
except (ValueError, TypeError) as e:
# 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
except (ValueError, TypeError):
info["value"] = value_str
else:
info["type"] = "unknown_structure"
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:
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
info["type"] = "error_no_name"
# return info
return info
def parse_part(part_element):
"""Parsea un nodo <flg:Part> de LAD/FBD."""
# Asume que Part está en namespace flg
if part_element is None:
return None
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')}"
)
return None
template_values = {}
negated_pins = {}
try:
# TemplateValue parece NO tener namespace flg
for tv in part_element.xpath("./TemplateValue"):
tv_name = tv.get("Name")
tv_type = tv.get("Type")
@ -241,20 +208,16 @@ def parse_part(part_element):
template_values[tv_name] = tv_type
except Exception as e:
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
negated_pins = {}
try:
# Negated parece NO tener namespace flg
for negated_elem in part_element.xpath("./Negated"):
negated_pin_name = negated_elem.get("Name")
if negated_pin_name:
negated_pins[negated_pin_name] = True
except Exception as e:
print(f"Advertencia: Error extrayendo Negated Pins Part UID={uid}: {e}")
return {
"uid": uid,
"type": name, # El 'type' de la instrucción (e.g., 'Add', 'Contact')
"type": name,
"template_values": template_values,
"negated_pins": negated_pins,
}
@ -262,7 +225,6 @@ def parse_part(part_element):
def parse_call(call_element):
"""Parsea un nodo <flg:Call> de LAD/FBD."""
# Asume que Call está en namespace flg
if call_element is None:
return None
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')}"
)
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)
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")
if not call_info_elem_no_ns:
print(
f"Error: Call UID {uid} sin elemento CallInfo (probado sin NS tambien)."
)
return {
"uid": uid,
"type": "Call_error",
"error": "Missing CallInfo",
} # Devolver error
print(f"Error: Call UID {uid} sin elemento CallInfo.")
return {"uid": uid, "type": "Call_error", "error": "Missing CallInfo"}
else:
# Si se encontró sin NS, usar ese (menos probable pero posible)
print(f"Advertencia: Call UID {uid} encontró CallInfo SIN namespace.")
call_info = call_info_elem_no_ns[0]
else:
call_info = call_info_elem[0] # Usar el encontrado con namespace
call_info = call_info_elem[0]
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:
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
return {
@ -303,23 +253,17 @@ def parse_call(call_element):
"type": "Call_error",
"error": "Missing Name or BlockType in CallInfo",
}
instance_name = None
instance_scope = None
# Buscar Instance y Component (que también deberían estar en namespace flg)
# Solo relevante si es FB
instance_name, instance_scope = None, None
if block_type == "FB":
instance_elem_list = call_info.xpath("./flg:Instance", namespaces=ns)
if instance_elem_list:
instance_elem = instance_elem_list[0]
instance_scope = instance_elem.get("Scope") # GlobalDB, LocalVariable, etc.
# Buscar Component dentro de Instance
instance_scope = instance_elem.get("Scope")
component_elem_list = instance_elem.xpath("./flg:Component", namespaces=ns)
if component_elem_list:
component_elem = component_elem_list[0]
db_name_raw = component_elem.get("Name")
if db_name_raw:
# Asegurar comillas dobles para nombres de DB
instance_name = (
f'"{db_name_raw}"'
if not db_name_raw.startswith('"')
@ -337,87 +281,62 @@ def parse_call(call_element):
print(
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") # Scope del <Call> mismo
call_scope = call_element.get("Scope")
if call_scope == "LocalVariable":
# Si la llamada es local y no tiene <Instance>, probablemente es una multi-instancia STAT
instance_name = f'"{block_name}"' # Usar el nombre del bloque como nombre de instancia STAT (convención común)
instance_scope = "Static" # Marcar como estático
instance_name = f'"{block_name}"'
instance_scope = "Static"
print(
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 = {
"uid": uid,
"type": "Call",
"block_name": block_name,
"block_type": block_type, # FC o FB
"block_type": block_type,
}
if instance_name:
call_data["instance_db"] = instance_name # Nombre formateado SCL
call_data["instance_db"] = instance_name
if instance_scope:
call_data["instance_scope"] = instance_scope # Static, GlobalDB, etc.
call_data["instance_scope"] = instance_scope
return call_data
def parse_interface_members(member_elements):
"""
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'.
"""
"""Parsea recursivamente miembros de interfaz/estructura."""
members_data = []
if not member_elements:
return members_data
for member in member_elements:
member_name = member.get("Name")
member_dtype_raw = member.get(
"Datatype"
) # Puede tener comillas o ser Array[...] of "..."
member_version = member.get("Version") # v1.0 etc.
member_dtype_raw = member.get("Datatype")
member_version = member.get("Version")
member_remanence = member.get("Remanence", "NonRetain")
member_accessibility = member.get("Accessibility", "Public")
if not member_name or not member_dtype_raw:
print(
"Advertencia: Miembro sin nombre o tipo de dato encontrado. Saltando."
)
print("Advertencia: Miembro sin nombre o tipo de dato. Saltando.")
continue
# Combinar tipo y versión si existe versión separada
member_dtype = (
f"{member_dtype_raw}:v{member_version}"
if member_version
else member_dtype_raw
)
member_info = {
"name": member_name,
"datatype": member_dtype, # Guardar el tipo original (puede tener comillas, versión)
"datatype": member_dtype,
"remanence": member_remanence,
"accessibility": member_accessibility,
"start_value": None,
"comment": None,
"children": [], # Para Structs
"array_elements": {}, # Para Arrays
"children": [],
"array_elements": {},
}
# Comentario del miembro
comment_node = member.xpath("./iface:Comment", namespaces=ns)
if comment_node:
# Comentario está dentro de Comment/MultiLanguageText
member_info["comment"] = get_multilingual_text(comment_node[0])
# Valor inicial
member_info["comment"] = get_multilingual_text(
comment_node[0]
) # Usa la función robusta
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
if start_value_node:
# Puede ser un nombre de constante o un valor literal
constant_name = start_value_node[0].get("ConstantName")
member_info["start_value"] = (
constant_name
@ -425,26 +344,18 @@ def parse_interface_members(member_elements):
else (
start_value_node[0].text
if start_value_node[0].text is not None
else ""
else None
)
)
# 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
) # Devolver None si está vacío
nested_sections = member.xpath(
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
) # Sección sin nombre específico
)
if nested_sections:
# Llamada recursiva
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["):
subelements = member.xpath("./iface:Subelement", namespaces=ns)
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)
if path and sub_start_value_node:
constant_name = sub_start_value_node[0].get("ConstantName")
@ -454,25 +365,23 @@ def parse_interface_members(member_elements):
else (
sub_start_value_node[0].text
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
# Parsear comentario del subelemento si es necesario
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
if path and sub_comment_node:
sub_comment_text = get_multilingual_text(sub_comment_node[0])
# ¿Cómo guardar comentario de subelemento? Podría ser un dict en array_elements
if isinstance(member_info["array_elements"].get(path), dict):
member_info["array_elements"][path][
"comment"
] = sub_comment_text
else: # Si solo estaba el valor, convertir a dict
current_val = member_info["array_elements"].get(path)
member_info["array_elements"][path] = {
"value": current_val,
"comment": sub_comment_text,
}
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
if path and sub_comment_node:
sub_comment_text = get_multilingual_text(
sub_comment_node[0]
) # Usa la función robusta
if isinstance(member_info["array_elements"].get(path), dict):
member_info["array_elements"][path][
"comment"
] = sub_comment_text
else:
member_info["array_elements"][path] = {
"value": member_info["array_elements"].get(path),
"comment": sub_comment_text,
}
members_data.append(member_info)
return members_data

View File

@ -128,6 +128,13 @@ if __name__ == "__main__":
# Usar la ruta absoluta para los scripts hijos
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
success = True

View File

@ -7,8 +7,8 @@ import sys
import traceback
import importlib
from lxml import etree
from collections import defaultdict # Puede ser necesario si load_parsers la usa
import copy # Puede ser necesario si load_parsers la usa
from collections import defaultdict
import copy
# Importar funciones comunes y namespaces desde el nuevo módulo de utils
try:
@ -22,8 +22,118 @@ except ImportError as e:
)
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"):
"""
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)
if not os.path.isdir(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}'")
parsers_package = os.path.basename(parsers_dir)
@ -48,10 +158,8 @@ def load_parsers(parsers_dir="parsers"):
and filename.endswith(".py")
and filename not in ["__init__.py", "parser_utils.py"]
):
module_name_rel = filename[:-3] # Nombre sin .py (e.g., parse_lad_fbd)
full_module_name = (
f"{parsers_package}.{module_name_rel}" # e.g., parsers.parse_lad_fbd
)
module_name_rel = filename[:-3] # Nombre sin .py (e.g., parse_lad_fbd)
full_module_name = f"{parsers_package}.{module_name_rel}" # e.g., parsers.parse_lad_fbd
try:
# Importar el módulo dinámicamente
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):
# Añadir la función al mapa para cada lenguaje que soporta
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:
print(
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())}")
return parser_map
# --- Función Principal de Conversión (Refactorizada) ---
# --- Función Principal de Conversión (MODIFICADA) ---
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}'...")
if not os.path.exists(xml_filepath):
print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'")
return False # Indicar fallo
return False # Indicar fallo
try:
print("Paso 1: Parseando archivo XML...")
# Usar un parser que quite texto en blanco para simplificar XPath
parser = etree.XMLParser(remove_blank_text=True)
parser = etree.XMLParser(remove_blank_text=True, recover=True) # recover=True puede ayudar
tree = etree.parse(xml_filepath, parser)
root = tree.getroot()
print("Paso 1: Parseo XML completado.")
# --- Buscar bloque principal (FC, FB, GlobalDB, OB) ---
print("Paso 2: Buscando el bloque SW.Blocks.FC/FB/GlobalDB/OB...")
# Usar local-name() para ignorar namespaces en esta búsqueda inicial
block_list = root.xpath(
"//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB' or local-name()='SW.Blocks.OB']"
)
if (
not block_list
): # Intentar con namespace si el anterior falla (menos probable)
ns_doc = {
"doc": "http://www.siemens.com/automation/Openness/SW/Document/v5"
} # Asumiendo este namespace
result = None # Inicializar resultado
# --- Detección del tipo de bloque/objeto principal ---
print("Paso 2: Detectando tipo de objeto principal...")
# Buscar UDT
udt_element = root.find(".//SW.Types.PlcStruct", namespaces=root.nsmap)
if udt_element is not None:
result = parse_udt(udt_element)
# Buscar Tag Table si no es UDT
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(
"//doc:SW.Blocks.FC | //doc:SW.Blocks.FB | //doc:SW.Blocks.GlobalDB | //doc:SW.Blocks.OB",
namespaces=ns_doc,
"//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB' or local-name()='SW.Blocks.OB']"
)
# (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
the_block = None
if block_list:
the_block = block_list[0]
block_tag_name = etree.QName(
the_block.tag
).localname # Obtener nombre local sin ns
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
if block_list:
the_block = block_list[0]
block_tag_name = etree.QName(the_block.tag).localname
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 2b: Bloque {block_tag_name} (Tipo: {block_type_found}) encontrado (ID={the_block.get('ID')}).")
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 ---
print("Paso 3: Extrayendo atributos del bloque...")
# AttributeList generalmente no tiene namespace propio
attribute_list_node = the_block.xpath("./AttributeList")
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
if attribute_list_node:
attr_list = attribute_list_node[0]
# Name, Number, ProgrammingLanguage están directamente bajo AttributeList
name_node = attr_list.xpath("./Name/text()")
block_name_val = name_node[0].strip() if name_node else block_name_val
num_node = attr_list.xpath("./Number/text()")
try:
block_number_val = int(num_node[0]) if num_node else None
except (ValueError, TypeError):
block_number_val = None # Mantener como None si no es entero
lang_node = attr_list.xpath("./ProgrammingLanguage/text()")
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
)
# --- Si es FC/FB/OB/DB, continuar con el parseo original ---
if the_block is not None:
print("Paso 3: Extrayendo atributos del bloque...")
# (Extracción de atributos Name, Number, Language como antes...)
attribute_list_node = the_block.xpath("./AttributeList")
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
if attribute_list_node:
attr_list = attribute_list_node[0]
name_node = attr_list.xpath("./Name/text()")
block_name_val = name_node[0].strip() if name_node else block_name_val
num_node = attr_list.xpath("./Number/text()")
try: block_number_val = int(num_node[0]) if num_node else None
except (ValueError, TypeError): block_number_val = None
lang_node = attr_list.xpath("./ProgrammingLanguage/text()")
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("Advertencia: No se encontró sección 'Static' para GlobalDB.")
else:
print(
f"Advertencia: No se encontró <Interface> para bloque {block_type_found}."
)
print(f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}.")
if block_type_found == "GlobalDB": block_lang_val = "DB"
if not result["interface"]:
print("Advertencia: No se pudo extraer información de la interfaz.")
# (Extracción de comentario como antes...)
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("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")
print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'")
if object_list_node:
compile_units = object_list_node[0].xpath("./SW.Blocks.CompileUnit")
print(
f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit."
)
# Crear diccionario resultado
result = {
"block_name": block_name_val,
"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) ---
for network_elem in compile_units:
networks_processed_count += 1
network_id = network_elem.get("ID")
if not network_id:
print("Advertencia: CompileUnit sin ID, saltando.")
continue
# (Extracción de interfaz como antes...)
print("Paso 4: Extrayendo la interfaz del bloque...")
interface_node_list = attribute_list_node[0].xpath("./Interface") if attribute_list_node else []
if interface_node_list:
interface_node = interface_node_list[0]
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")
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)
# AttributeList/ProgrammingLanguage sin namespace
network_lang = "LAD" # Default si no se encuentra
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()
# (Procesamiento de redes como antes, SOLO si NO es GlobalDB)
if block_type_found != "GlobalDB":
print("Paso 5: Buscando y PROCESANDO redes (CompileUnits)...")
networks_processed_count = 0
result["networks"] = []
object_list_node = the_block.xpath("./ObjectList")
if object_list_node:
compile_units = object_list_node[0].xpath("./SW.Blocks.CompileUnit")
print(f"Paso 5: Se encontraron {len(compile_units)} elementos SW.Blocks.CompileUnit.")
print(
f" - Procesando Red ID={network_id}, Lenguaje Red={network_lang}"
)
# Bucle de parseo de redes (igual que antes)
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 ---
parser_func = parser_map.get(
network_lang.upper()
) # Buscar parser por lenguaje
parsed_network_data = None
if parsed_network_data:
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}")
comment_elem_net = network_elem.xpath("./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns)
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:
try:
# Llamar a la función de parseo específica del lenguaje
# Pasar el elemento XML de la red y los namespaces
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}",
}
if networks_processed_count == 0: print(f"Advertencia: ObjectList para {block_type_found} sin SW.Blocks.CompileUnit.")
else: print(f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}.")
else: # Es GlobalDB
print("Paso 5: Saltando procesamiento de redes para GlobalDB.")
# --- 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
comment_elem_net = network_elem.xpath(
"./ObjectList/MultilingualText[@CompositionName='Comment']",
namespaces=ns,
)
if not comment_elem_net: # Fallback
comment_elem_net = network_elem.xpath(
".//MultilingualText[@CompositionName='Comment']",
namespaces=ns,
)
# --- Escritura del JSON (si se encontró un objeto) ---
if result:
print("Paso 6: Escribiendo el resultado en el archivo JSON...")
# Validaciones finales
if result.get("block_type") not in ["PlcUDT", "PlcTagTable"] and not result["interface"]:
print("ADVERTENCIA FINAL: 'interface' está vacía en el JSON.")
if result.get("block_type") not in ["PlcUDT", "PlcTagTable", "GlobalDB"] and not result["networks"]:
print("ADVERTENCIA FINAL: 'networks' está vacía en el JSON.")
parsed_network_data["comment"] = (
get_multilingual_text(comment_elem_net[0])
if comment_elem_net
else ""
)
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
# Añadir la red procesada (o con error) al resultado
result["networks"].append(parsed_network_data)
# --- 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).")
except IOError as e: print(f"Error Crítico: No se pudo escribir JSON en '{json_filepath}'. Error: {e}"); return False
except TypeError as e: print(f"Error Crítico: Problema al serializar a JSON. Error: {e}"); return False
else:
print(
f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}."
)
print("Error Crítico: No se pudo determinar el tipo de objeto principal en el XML.")
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:
print(
f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}"
)
return False # Indicar fallo
print(f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}")
return False # Indicar fallo
except Exception as e:
print(f"Error Crítico: Error inesperado durante la conversión: {e}")
traceback.print_exc()
return False # Indicar fallo
return False # Indicar fallo
# --- Punto de Entrada Principal (__main__) ---
if __name__ == "__main__":
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(
"xml_filepath",
@ -468,38 +426,31 @@ if __name__ == "__main__":
xml_input_file = args.xml_filepath
if not os.path.exists(xml_input_file):
print(
f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'",
file=sys.stderr,
)
print(f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'", file=sys.stderr)
sys.exit(1)
# --- Cargar Parsers Dinámicamente ---
loaded_parsers = load_parsers()
loaded_parsers = load_parsers() # Carga parsers LAD/FBD/STL/SCL
if not loaded_parsers:
print("Error Crítico (x1): No se cargaron parsers. Abortando.", file=sys.stderr)
sys.exit(1)
# Continuar incluso sin parsers de red, ya que podríamos estar parseando UDT/TagTable
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
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
output_dir = os.path.dirname(xml_input_file)
# Asegurarse que el directorio de salida exista (puede ser el mismo que el de entrada)
base_dir = os.path.dirname(xml_input_file)
output_dir = os.path.join(base_dir, "parsing")
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)}'")
# Llamar a la función de conversión principal
success = convert_xml_to_json(xml_input_file, json_output_file, loaded_parsers)
# Salir con código de error apropiado
if success:
sys.exit(0) # Éxito
sys.exit(0) # Éxito
else:
print(
f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.",
file=sys.stderr,
)
sys.exit(1) # Fallo
print(f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.", file=sys.stderr)
sys.exit(1) # Fallo

View File

@ -7,25 +7,25 @@ import traceback
import re
import importlib
import sys
import sympy # Import sympy
import sympy # Import sympy
# Import necessary components from processors directory
from processors.processor_utils import (
format_variable_name, # Keep if used outside processors
sympy_expr_to_scl, # Needed for IF grouping and maybe others
format_variable_name, # Keep if used outside processors
sympy_expr_to_scl, # Needed for IF grouping and maybe others
# 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 ---
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
SCL_SUFFIX = "_sympy_processed"
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
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):
"""
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
def load_processors(processors_dir="processors"):
"""
Escanea el directorio, importa módulos, construye el mapa y una lista
ordenada por prioridad.
"""
processor_map = {}
processor_list_unsorted = [] # Lista para guardar (priority, type_name, func)
default_priority = 10 # Prioridad si no se define en get_processor_info
processor_list_unsorted = [] # Lista para guardar (priority, type_name, func)
default_priority = 10 # Prioridad si no se define en get_processor_info
if not os.path.isdir(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}'")
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
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),
y guarda JSON procesado.
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
y guarda JSON procesado en la ruta especificada.
"""
global data
if not os.path.exists(json_filepath):
print(f"Error: JSON no encontrado: {json_filepath}")
return
return False
print(f"Cargando JSON desde: {json_filepath}")
try:
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:
print(f"Error al cargar JSON: {e}")
traceback.print_exc()
return
return False
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) ---
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}")
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
block_type = data.get("block_type", "Unknown")
print(f"Procesando bloque tipo: {block_type}")
# --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO ---
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
print(
"INFO: El bloque es un Data Block (GlobalDB). Saltando procesamiento lógico de x2."
)
# 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}")
# --- MODIFICADO: SALTAR PROCESAMIENTO PARA DB, UDT, TAG TABLE ---
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"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_json_filepath}")
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)
print("Guardado de DB completado.")
print(f"Guardado de {block_type} completado.")
return True
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()
return # <<< SALIR TEMPRANO PARA DBs
return False
# --- SI NO ES DB (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...") # <-- Mensaje actualizado
# --- 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...")
# (Carga de procesadores y mapas de acceso SIN CAMBIOS)
script_dir = os.path.dirname(__file__)
processors_dir_path = os.path.join(script_dir, "processors")
processor_map, sorted_processors = load_processors(processors_dir_path)
if not processor_map:
print("Error crítico: No se cargaron procesadores. Abortando.")
return
return False
network_access_maps = {}
# Crear mapas de acceso por red (copiado/adaptado de versión anterior)
for network in data.get("networks", []):
net_id = network["id"]
current_access_map = {}
for instr in network.get("logic", []):
for _, source in instr.get("inputs", {}).items():
sources_to_check = (
source
if isinstance(source, list)
else ([source] if isinstance(source, dict) else [])
)
sources_to_check = (source if isinstance(source, list) else ([source] if isinstance(source, dict) else []))
for src in sources_to_check:
if (
isinstance(src, dict)
and src.get("uid")
and src.get("type") in ["variable", "constant"]
):
if (isinstance(src, dict) and src.get("uid") and src.get("type") in ["variable", "constant"]):
current_access_map[src["uid"]] = src
for _, dest_list in instr.get("outputs", {}).items():
if isinstance(dest_list, list):
for dest in dest_list:
if (
isinstance(dest, dict)
and dest.get("uid")
and dest.get("type") in ["variable", "constant"]
):
if (isinstance(dest, dict) and dest.get("uid") and dest.get("type") in ["variable", "constant"]):
current_access_map[dest["uid"]] = dest
network_access_maps[net_id] = current_access_map
# (Inicialización de SymbolManager y bucle iterativo SIN CAMBIOS)
symbol_manager = SymbolManager()
sympy_map = {}
max_passes = 30
passes = 0
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:
passes += 1
made_change_in_base_pass = False
@ -398,246 +382,149 @@ def process_json_to_scl(json_filepath):
num_sympy_processed_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):")
num_sympy_processed_this_pass = 0 # Resetear contador para el pase
num_sympy_processed_this_pass = 0
for processor_info in sorted_processors:
current_type_name = processor_info["type_name"]
func_to_call = processor_info["func"]
for network in data.get("networks", []):
network_id = network["id"]
network_lang = network.get("language", "LAD") # Lenguaje de la red
if network_lang == "STL": # Saltar redes STL
continue
network_lang = network.get("language", "LAD")
if network_lang == "STL": continue
access_map = network_access_maps.get(network_id, {})
network_logic = network.get("logic", [])
for instruction in network_logic:
instr_uid = instruction.get("instruction_uid")
# Usar el tipo *actual* de la instrucción para el lookup
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 instr_type_current
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]
):
if (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
# El lookup usa el tipo actual (que aún no tiene el sufijo)
lookup_key = instr_type_current.lower()
effective_type_name = lookup_key
# Mapeo especial para llamadas FC/FB
if instr_type_current == "Call":
call_block_type = instruction.get("block_type", "").upper()
if call_block_type == "FC":
effective_type_name = "call_fc"
elif call_block_type == "FB":
effective_type_name = "call_fb"
# Añadir otros tipos de llamada si es necesario
if call_block_type == "FC": effective_type_name = "call_fc"
elif call_block_type == "FB": effective_type_name = "call_fb"
# Si el tipo efectivo coincide con el procesador actual
if effective_type_name == current_type_name:
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:
made_change_in_base_pass = True
num_sympy_processed_this_pass += 1
except Exception as e:
print(
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
)
print(f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}")
traceback.print_exc()
instruction["scl"] = (
f"// ERROR en SymPy procesador base: {e}"
)
# Añadir sufijo de error al tipo actual
instruction["scl"] = f"// ERROR en SymPy procesador base: {e}"
instruction["type"] = instr_type_current + "_error"
made_change_in_base_pass = True # Se hizo un cambio (marcar como error)
print(
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
)
made_change_in_base_pass = True
print(f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy.")
# --- FASE 2: Agrupación IF (Ignorando STL) ---
if (
made_change_in_base_pass or passes == 1
): # Ejecutar siempre en el primer pase o si hubo cambios
# FASE 2: Agrupación IF (Ignorando STL)
if made_change_in_base_pass or passes == 1:
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", []):
network_id = network["id"]
network_lang = network.get("language", "LAD")
if network_lang == "STL":
continue # Saltar STL
if network_lang == "STL": continue
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")])
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)
if not instruction: 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("grouped") or "_error" in instruction.get("type", ""): continue
if instruction.get("type", "").endswith(SCL_SUFFIX):
try:
group_changed = process_group_ifs(
instruction, network_id, sympy_map, symbol_manager, data
)
group_changed = process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data)
if group_changed:
made_change_in_group_pass = True
num_grouped_this_pass += 1
except Exception as e:
print(
f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}"
)
print(f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}")
traceback.print_exc()
print(
f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)."
)
print(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:
print(
f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---"
)
print(f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---")
processing_complete = True
else:
print(
f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..."
)
# --- Comprobar límite de pases ---
print(f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando...")
if passes == max_passes and not processing_complete:
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
# --- FIN BUCLE ITERATIVO ---
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
# (Verificación Final y Guardado JSON SIN CAMBIOS)
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---")
unprocessed_count = 0
unprocessed_details = []
ignored_types = [
"raw_scl_chunk",
"unsupported_lang",
"raw_stl_chunk",
"unsupported_content", # Añadido de x1
"parsing_error", # Añadido de x1
]
ignored_types = ["raw_scl_chunk", "unsupported_lang", "raw_stl_chunk", "unsupported_content", "parsing_error"]
for network in data.get("networks", []):
network_id = network.get("id", "Unknown ID")
network_title = network.get("title", f"Network {network_id}")
network_lang = network.get("language", "LAD")
if network_lang == "STL":
continue # No verificar redes STL
if network_lang == "STL": continue
for instruction in network.get("logic", []):
instr_uid = instruction.get("instruction_uid", "Unknown UID")
instr_type = instruction.get("type", "Unknown Type")
is_grouped = instruction.get("grouped", False)
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
):
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):
unprocessed_count += 1
unprocessed_details.append(
f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), "
f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'"
)
unprocessed_details.append(f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), Instrucción UID: {instr_uid}, Tipo: '{instr_type}'")
if unprocessed_count > 0:
print(
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."
)
print(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.")
# --- Guardar JSON Final ---
output_filename = json_filepath.replace(
"_simplified.json", "_simplified_processed.json"
)
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}")
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)
print("Guardado completado.")
except Exception as e:
print(f"Error Crítico al guardar JSON procesado: {e}")
return True
except Exception as e:
print(f"Error Crítico al guardar JSON procesado: {e}");
traceback.print_exc()
return False
# --- Ejecución (sin cambios en esta parte) ---
# --- Ejecución (MODIFICADO) ---
if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal
import argparse
import os
import sys
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).")
args = parser.parse_args()
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):
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.")
# Derivar nombre del archivo JSON de entrada (_simplified.json)
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
input_dir = os.path.dirname(source_xml_file) # Directorio del XML original
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
base_dir = os.path.dirname(source_xml_file)
parsing_dir = os.path.join(base_dir, "parsing")
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):
print(
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) # Salir si el archivo necesario no está
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)}'.")
sys.exit(1)
else:
# Llamar a la función principal de procesamiento del script
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:
print(
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
)
import traceback # Asegurar que traceback está importado
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla
sys.exit(1)

View File

@ -5,391 +5,29 @@ import os
import re
import argparse
import sys
import traceback # Importar traceback para errores
import traceback
# --- Importar Utilidades y Constantes (Asumiendo ubicación) ---
# --- Importar Generadores Específicos ---
try:
# Intenta importar desde el paquete de procesadores si está estructurado así
from processors.processor_utils import format_variable_name
# Definir SCL_SUFFIX aquí o importarlo si está centralizado
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
# --- 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
from generators.generate_scl_db import generate_scl_for_db
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)
# --- 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):
print(
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
)
print(f"Error: JSON no encontrado: '{processed_json_filepath}'")
return
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:
data = json.load(f)
except Exception as e:
print(f"Error al cargar o parsear JSON: {e}")
traceback.print_exc()
print(f"Error al cargar/parsear JSON: {e}"); traceback.print_exc(); return
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
# --- Extracción de Información del Bloque (Común) ---
block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number")
# block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
block_type = data.get(
"block_type", "Unknown"
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
block_comment = data.get("block_comment", "")
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 = []
# --- Llamar a la función generadora ---
if generation_function:
try:
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
# --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
print("Modo de generación: DATA_BLOCK")
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")
# --- Escritura del Archivo de Salida ---
output_filename_base = f"{scl_block_name}{output_extension}"
output_filepath = os.path.join(output_directory, output_filename_base)
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
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}")
print(f" -> Escribiendo archivo de salida en: {output_filepath}")
try:
with open(output_scl_filepath, "w", encoding="utf-8") as f:
for line in scl_output:
os.makedirs(output_directory, exist_ok=True)
with open(output_filepath, "w", encoding="utf-8") as f:
for line in output_content:
f.write(line + "\n")
print("Generación de SCL completada.")
print(f"Generación de {output_extension.upper()} completada.")
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()
# --- Ejecución ---
if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal
import argparse
import os
import sys
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)
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file.")
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
if not os.path.exists(source_xml_file): print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'.")
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) # Directorio del XML original
input_json_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.json"
)
# 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
base_dir = os.path.dirname(source_xml_file)
parsing_dir = os.path.join(base_dir, "parsing")
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_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)}'")
if not os.path.exists(input_json_file):
print(
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á
print(f"Error Fatal (x3): JSON procesado no encontrado: '{input_json_file}'"); sys.exit(1)
else:
# Llamar a la función principal de generación SCL del script
try:
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
try: generate_scl_or_markdown(input_json_file, output_dir); sys.exit(0)
except Exception as e: print(f"Error Crítico (x3): {e}"); traceback.print_exc(); sys.exit(1)

0
generators/__init__.py Normal file
View File

View File

@ -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

View File

@ -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 = "&nbsp;&nbsp;&nbsp;&nbsp;" * 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}&nbsp;&nbsp;*(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}&nbsp;&nbsp;`[{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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -128,6 +128,13 @@ if __name__ == "__main__":
# Usar la ruta absoluta para los scripts hijos
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
success = True

View File

@ -438,9 +438,10 @@ if __name__ == "__main__":
# Derivar nombre de salida JSON
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)
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)}'")

View File

@ -301,16 +301,16 @@ def load_processors(processors_dir="processors"):
# --- 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),
y guarda JSON procesado.
y guarda JSON procesado en la ruta especificada.
"""
global data
if not os.path.exists(json_filepath):
print(f"Error: JSON no encontrado: {json_filepath}")
return
return False
print(f"Cargando JSON desde: {json_filepath}")
try:
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:
print(f"Error al cargar JSON: {e}")
traceback.print_exc()
return
return False
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
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 ---
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.")
output_filename = json_filepath.replace(
"_simplified.json", "_simplified_processed.json"
)
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_filename}")
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_json_filepath}")
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)
print(f"Guardado de {block_type} completado.")
return True
except Exception as e:
print(f"Error Crítico al guardar JSON de {block_type}: {e}")
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 ---
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)
if not processor_map:
print("Error crítico: No se cargaron procesadores. Abortando.")
return
return False
network_access_maps = {}
for network in data.get("networks", []):
@ -484,18 +482,21 @@ def process_json_to_scl(json_filepath):
for detail in unprocessed_details: print(detail)
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_filename}")
print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}")
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.")
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 (SIN CAMBIOS) ---
# --- Ejecución (MODIFICADO) ---
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.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 = 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).")
args = parser.parse_args()
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.")
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
input_dir = os.path.dirname(source_xml_file)
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
output_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified_processed.json")
base_dir = os.path.dirname(source_xml_file)
parsing_dir = os.path.join(base_dir, "parsing")
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)}'")
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)}'.")
sys.exit(1)
else:
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:
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -5,483 +5,29 @@ import os
import re
import argparse
import sys
import traceback # Importar traceback para errores
import traceback
# --- Importar Utilidades y Constantes (Asumiendo ubicación) ---
# --- Importar Generadores Específicos ---
try:
# Intenta importar desde el paquete de procesadores si está estructurado así
from processors.processor_utils import format_variable_name
from generators.generate_scl_db import generate_scl_for_db
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
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 = "&nbsp;&nbsp;&nbsp;&nbsp;" * 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}&nbsp;&nbsp;*(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}&nbsp;&nbsp;`[{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) ---
# --- 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,
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):
print(
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
)
print(f"Error: JSON no encontrado: '{processed_json_filepath}'")
return
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:
data = json.load(f)
except Exception as e:
print(f"Error al cargar o parsear JSON: {e}")
traceback.print_exc()
return
print(f"Error al cargar/parsear JSON: {e}"); traceback.print_exc(); return
# --- Extracción de Información y Determinación de Tipo ---
block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number")
block_type = data.get(
"block_type", "Unknown"
) # FC, FB, OB, GlobalDB, PlcUDT, PlcTagTable
block_comment = data.get("block_comment", "")
scl_block_name = format_variable_name(block_name)
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
output_extension = ".scl" # Default
print(
f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})"
)
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")
output_content = generate_udt_markdown(data)
generation_function = generate_udt_markdown
output_extension = ".md"
elif block_type == "PlcTagTable":
print(" -> Modo de generación: Tag Table Markdown")
output_content = generate_tag_table_markdown(data)
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"
# (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"]:
print(f" -> Modo de generación: {block_type} SCL")
generation_function = generate_scl_for_code_block
output_extension = ".scl"
# (Lógica de generación SCL para FC/FB/OB como estaba antes)
scl_block_keyword = "FUNCTION_BLOCK"
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."
)
else: # Tipo desconocido
print(f"Error: Tipo de bloque desconocido '{block_type}'. No se generará archivo.")
return
# --- Escritura del Archivo de Salida (.scl o .md) ---
# Construir nombre de archivo de salida
output_filename_base = (
f"{scl_block_name}{output_extension}" # Usar nombre SCL seguro
)
# --- Llamar a la función generadora ---
if generation_function:
try:
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)
print(f" -> Escribiendo archivo de salida en: {output_filepath}")
try:
# Crear directorio si no existe
os.makedirs(output_directory, exist_ok=True)
with open(output_filepath, "w", encoding="utf-8") as f:
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}")
traceback.print_exc()
# --- Ejecución ---
if __name__ == "__main__":
parser = argparse.ArgumentParser(
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 (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."
)
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file.")
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
if not os.path.exists(source_xml_file): print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'.")
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
base_dir = os.path.dirname(source_xml_file)
input_json_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.json"
)
# 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
parsing_dir = os.path.join(base_dir, "parsing")
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_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)}'")
if not os.path.exists(input_json_file):
print(
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)
print(f"Error Fatal (x3): JSON procesado no encontrado: '{input_json_file}'"); sys.exit(1)
else:
try:
# Pasar el directorio de salida a la función principal
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)
try: generate_scl_or_markdown(input_json_file, output_dir); sys.exit(0)
except Exception as e: print(f"Error Crítico (x3): {e}"); traceback.print_exc(); sys.exit(1)