Adicion del procesamiento de OBs

This commit is contained in:
Miguel 2025-04-20 13:22:11 +02:00
parent 6c604176a1
commit 66c5a076ab
10 changed files with 2032 additions and 1480 deletions

View File

@ -5,18 +5,21 @@ import sys
import locale import locale
import glob # <--- Importar glob para buscar archivos import glob # <--- Importar glob para buscar archivos
# (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior) # (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior)
def get_console_encoding(): def get_console_encoding():
"""Obtiene la codificación preferida de la consola, con fallback.""" """Obtiene la codificación preferida de la consola, con fallback."""
try: try:
return locale.getpreferredencoding(False) return locale.getpreferredencoding(False)
except Exception: except Exception:
return 'cp1252' return "cp1252"
CONSOLE_ENCODING = get_console_encoding() CONSOLE_ENCODING = get_console_encoding()
# Descomenta la siguiente línea si quieres ver la codificación detectada: # Descomenta la siguiente línea si quieres ver la codificación detectada:
# print(f"Detected console encoding: {CONSOLE_ENCODING}") # print(f"Detected console encoding: {CONSOLE_ENCODING}")
# (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING) # (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING)
def run_script(script_name, xml_arg): def run_script(script_name, xml_arg):
"""Runs a given script with the specified XML file argument.""" """Runs a given script with the specified XML file argument."""
@ -24,12 +27,14 @@ def run_script(script_name, xml_arg):
command = [sys.executable, script_path, xml_arg] command = [sys.executable, script_path, xml_arg]
print(f"\n--- Running {script_name} with argument: {xml_arg} ---") print(f"\n--- Running {script_name} with argument: {xml_arg} ---")
try: try:
result = subprocess.run(command, result = subprocess.run(
command,
check=True, check=True,
capture_output=True, capture_output=True,
text=True, text=True,
encoding=CONSOLE_ENCODING, encoding=CONSOLE_ENCODING,
errors='replace') # 'replace' para evitar errores errors="replace",
) # 'replace' para evitar errores
# Imprimir stdout y stderr # Imprimir stdout y stderr
# Eliminar saltos de línea extra al final si existen # Eliminar saltos de línea extra al final si existen
@ -49,8 +54,16 @@ def run_script(script_name, xml_arg):
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error running {script_name}:") print(f"Error running {script_name}:")
print(f"Return code: {e.returncode}") print(f"Return code: {e.returncode}")
stdout_decoded = e.stdout.decode(CONSOLE_ENCODING, errors='replace').strip() if isinstance(e.stdout, bytes) else (e.stdout or "").strip() stdout_decoded = (
stderr_decoded = e.stderr.decode(CONSOLE_ENCODING, errors='replace').strip() if isinstance(e.stderr, bytes) else (e.stderr or "").strip() e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip()
if isinstance(e.stdout, bytes)
else (e.stdout or "").strip()
)
stderr_decoded = (
e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip()
if isinstance(e.stderr, bytes)
else (e.stderr or "").strip()
)
if stdout_decoded: if stdout_decoded:
print("--- Stdout ---") print("--- Stdout ---")
print(stdout_decoded) print(stdout_decoded)
@ -63,12 +76,13 @@ def run_script(script_name, xml_arg):
print(f"An unexpected error occurred while running {script_name}: {e}") print(f"An unexpected error occurred while running {script_name}: {e}")
return False return False
# --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO --- # --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO ---
def select_xml_file(): def select_xml_file():
"""Busca archivos .xml, los lista y pide al usuario que elija uno.""" """Busca archivos .xml, los lista y pide al usuario que elija uno."""
print("No XML file specified. Searching for XML files in current directory...") print("No XML file specified. Searching for XML files in current directory...")
# Buscar archivos .xml en el directorio actual (.) # Buscar archivos .xml en el directorio actual (.)
xml_files = sorted(glob.glob('*.xml')) # sorted para orden alfabético xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético
if not xml_files: if not xml_files:
print("Error: No .xml files found in the current directory.") print("Error: No .xml files found in the current directory.")
@ -80,7 +94,9 @@ def select_xml_file():
while True: while True:
try: try:
choice = input(f"Enter the number of the file to process (1-{len(xml_files)}): ") choice = input(
f"Enter the number of the file to process (1-{len(xml_files)}): "
)
choice_num = int(choice) choice_num = int(choice)
if 1 <= choice_num <= len(xml_files): if 1 <= choice_num <= len(xml_files):
selected_file = xml_files[choice_num - 1] selected_file = xml_files[choice_num - 1]
@ -93,6 +109,8 @@ def select_xml_file():
except EOFError: # Manejar si la entrada se cierra inesperadamente except EOFError: # Manejar si la entrada se cierra inesperadamente
print("\nSelection cancelled.") print("\nSelection cancelled.")
sys.exit(1) sys.exit(1)
# --- FIN NUEVA FUNCIÓN --- # --- FIN NUEVA FUNCIÓN ---
@ -111,18 +129,24 @@ if __name__ == "__main__":
# Verificar si el directorio 'XML Project' existe # Verificar si el directorio 'XML Project' existe
if not os.path.isdir(xml_project_dir): if not os.path.isdir(xml_project_dir):
print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.") print(
print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.") f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio."
)
print(
"Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro."
)
sys.exit(1) sys.exit(1)
# Buscar todos los archivos .xml recursivamente dentro de xml_project_dir # Buscar todos los archivos .xml recursivamente dentro de xml_project_dir
# Usamos os.path.join para construir la ruta de búsqueda correctamente # Usamos os.path.join para construir la ruta de búsqueda correctamente
# y '**/*.xml' para la recursividad con glob # y '**/*.xml' para la recursividad con glob
search_pattern = os.path.join(xml_project_dir, '**', '*.xml') search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
xml_files_found = glob.glob(search_pattern, recursive=True) xml_files_found = glob.glob(search_pattern, recursive=True)
if not xml_files_found: if not xml_files_found:
print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.") print(
f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios."
)
sys.exit(0) # Salir limpiamente si no hay archivos sys.exit(0) # Salir limpiamente si no hay archivos
print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:") print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:")
@ -141,7 +165,9 @@ if __name__ == "__main__":
processed_count = 0 processed_count = 0
failed_count = 0 failed_count = 0
for xml_filepath in xml_files_found: for xml_filepath in xml_files_found:
print(f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---") print(
f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
# Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio # Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio
absolute_xml_filepath = os.path.abspath(xml_filepath) absolute_xml_filepath = os.path.abspath(xml_filepath)
@ -150,26 +176,37 @@ if __name__ == "__main__":
# La función run_script ya está definida en tu script x0_main.py # La función run_script ya está definida en tu script x0_main.py
success = True success = True
if not run_script(script1, absolute_xml_filepath): if not run_script(script1, absolute_xml_filepath):
print(f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") print(
f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
elif not run_script(script2, absolute_xml_filepath): elif not run_script(script2, absolute_xml_filepath):
print(f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") print(
f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
elif not run_script(script3, absolute_xml_filepath): elif not run_script(script3, absolute_xml_filepath):
print(f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}") print(
f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
if success: if success:
print(f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---") print(
f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
processed_count += 1 processed_count += 1
else: else:
failed_count += 1 failed_count += 1
print(f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---") print(
f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
print("\n--- Resumen Final del Procesamiento ---") print("\n--- Resumen Final del Procesamiento ---")
print(f"Total de archivos XML encontrados: {len(xml_files_found)}") print(f"Total de archivos XML encontrados: {len(xml_files_found)}")
print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}") print(
f"Archivos procesados exitosamente por el pipeline completo: {processed_count}"
)
print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}") print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}")
print("---------------------------------------") print("---------------------------------------")
xml_filename = None xml_filename = None

View File

@ -46,6 +46,7 @@ def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
print(f"Advertencia: Error extrayendo MultilingualText: {e}") print(f"Advertencia: Error extrayendo MultilingualText: {e}")
return "" return ""
def get_symbol_name(symbol_element): def get_symbol_name(symbol_element):
# (Sin cambios respecto a la versión anterior) # (Sin cambios respecto a la versión anterior)
if symbol_element is None: if symbol_element is None:
@ -57,6 +58,7 @@ def get_symbol_name(symbol_element):
print(f"Advertencia: Excepción en get_symbol_name: {e}") print(f"Advertencia: Excepción en get_symbol_name: {e}")
return None return None
def parse_access(access_element): def parse_access(access_element):
# (Sin cambios respecto a la versión anterior) # (Sin cambios respecto a la versión anterior)
if access_element is None: if access_element is None:
@ -149,6 +151,7 @@ def parse_access(access_element):
return info return info
return info return info
def parse_part(part_element): def parse_part(part_element):
# (Sin cambios respecto a la versión anterior) # (Sin cambios respecto a la versión anterior)
if part_element is None: if part_element is None:
@ -184,6 +187,7 @@ def parse_part(part_element):
"negated_pins": negated_pins, "negated_pins": negated_pins,
} }
def parse_call(call_element): def parse_call(call_element):
# (Mantiene la corrección para DB de instancia) # (Mantiene la corrección para DB de instancia)
if call_element is None: if call_element is None:
@ -243,8 +247,10 @@ def parse_call(call_element):
call_data["instance_scope"] = instance_scope call_data["instance_scope"] = instance_scope
return call_data return call_data
# SCL (Structured Text) Parser # SCL (Structured Text) Parser
def reconstruct_scl_from_tokens(st_node): def reconstruct_scl_from_tokens(st_node):
""" """
Reconstruye SCL desde <StructuredText>, mejorando el manejo de Reconstruye SCL desde <StructuredText>, mejorando el manejo de
@ -263,10 +269,10 @@ def reconstruct_scl_from_tokens(st_node):
scl_parts.append(elem.get("Text", "")) scl_parts.append(elem.get("Text", ""))
elif tag == "Blank": elif tag == "Blank":
# Añadir espacios simples, evitar múltiples si ya hay uno antes/después # Añadir espacios simples, evitar múltiples si ya hay uno antes/después
if not scl_parts or not scl_parts[-1].endswith(' '): if not scl_parts or not scl_parts[-1].endswith(" "):
scl_parts.append(" " * int(elem.get("Num", 1))) scl_parts.append(" " * int(elem.get("Num", 1)))
elif int(elem.get("Num", 1)) > 1: # Añadir extras si son más de 1 elif int(elem.get("Num", 1)) > 1: # Añadir extras si son más de 1
scl_parts.append(" " * (int(elem.get("Num", 1))-1)) scl_parts.append(" " * (int(elem.get("Num", 1)) - 1))
elif tag == "NewLine": elif tag == "NewLine":
# Limpiar espacios antes del salto de línea real # Limpiar espacios antes del salto de línea real
if scl_parts: if scl_parts:
@ -276,7 +282,15 @@ def reconstruct_scl_from_tokens(st_node):
scope = elem.get("Scope") scope = elem.get("Scope")
access_str = f"/*_ERR_Scope_{scope}_*/" # Fallback más informativo access_str = f"/*_ERR_Scope_{scope}_*/" # Fallback más informativo
if scope in ["GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable", "InputVariable", "OutputVariable", "ConstantVariable"]: # Tipos comunes de variables if scope in [
"GlobalVariable",
"LocalVariable",
"TempVariable",
"InOutVariable",
"InputVariable",
"OutputVariable",
"ConstantVariable",
]: # Tipos comunes de variables
symbol_elem = elem.xpath("./st:Symbol", namespaces=ns) symbol_elem = elem.xpath("./st:Symbol", namespaces=ns)
if symbol_elem: if symbol_elem:
components = symbol_elem[0].xpath("./st:Component", namespaces=ns) components = symbol_elem[0].xpath("./st:Component", namespaces=ns)
@ -288,11 +302,18 @@ def reconstruct_scl_from_tokens(st_node):
symbol_text_parts.append(".") symbol_text_parts.append(".")
# Reconstrucción de comillas (heurística) # Reconstrucción de comillas (heurística)
has_quotes_elem = comp.xpath("../st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) has_quotes_elem = comp.xpath(
has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true" "../st:BooleanAttribute[@Name='HasQuotes']/text()",
is_temp = name.startswith('#') namespaces=ns,
)
has_quotes = (
has_quotes_elem and has_quotes_elem[0].lower() == "true"
)
is_temp = name.startswith("#")
if has_quotes or (i == 0 and not is_temp): # Comillas si HasQuotes o primer componente (no temp) if has_quotes or (
i == 0 and not is_temp
): # Comillas si HasQuotes o primer componente (no temp)
symbol_text_parts.append(f'"{name}"') symbol_text_parts.append(f'"{name}"')
else: else:
symbol_text_parts.append(name) symbol_text_parts.append(name)
@ -301,7 +322,10 @@ def reconstruct_scl_from_tokens(st_node):
index_access = comp.xpath("./st:Access", namespaces=ns) index_access = comp.xpath("./st:Access", namespaces=ns)
if index_access: if index_access:
# Llama recursivamente para obtener el texto de cada índice # Llama recursivamente para obtener el texto de cada índice
indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access] indices_text = [
reconstruct_scl_from_tokens(idx_node)
for idx_node in index_access
]
symbol_text_parts.append(f"[{','.join(indices_text)}]") symbol_text_parts.append(f"[{','.join(indices_text)}]")
access_str = "".join(symbol_text_parts) access_str = "".join(symbol_text_parts)
@ -309,8 +333,12 @@ def reconstruct_scl_from_tokens(st_node):
elif scope == "LiteralConstant": elif scope == "LiteralConstant":
constant_elem = elem.xpath("./st:Constant", namespaces=ns) constant_elem = elem.xpath("./st:Constant", namespaces=ns)
if constant_elem: if constant_elem:
val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns) val_elem = constant_elem[0].xpath(
type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns) "./st:ConstantValue/text()", namespaces=ns
)
type_elem = constant_elem[0].xpath(
"./st:ConstantType/text()", namespaces=ns
)
const_type = type_elem[0] if type_elem else "" const_type = type_elem[0] if type_elem else ""
const_val = val_elem[0] if val_elem else "_ERR_CONSTVAL_" const_val = val_elem[0] if val_elem else "_ERR_CONSTVAL_"
@ -340,29 +368,38 @@ def reconstruct_scl_from_tokens(st_node):
# Unir partes, limpiar espacios extra alrededor de operadores y saltos de línea # Unir partes, limpiar espacios extra alrededor de operadores y saltos de línea
full_scl = "".join(scl_parts) full_scl = "".join(scl_parts)
# Re-indentar líneas después de IF/THEN, etc. (Simplificado) # Re-indentar líneas después de IF/THEN, etc. (Simplificado)
output_lines = [] output_lines = []
indent_level = 0 indent_level = 0
for line in full_scl.split('\n'): for line in full_scl.split("\n"):
line = line.strip() line = line.strip()
if not line: continue # Saltar líneas vacías if not line:
continue # Saltar líneas vacías
# Reducir indentación antes de procesar END_IF, ELSE, etc. (simplificado) # Reducir indentación antes de procesar END_IF, ELSE, etc. (simplificado)
if line.startswith(('END_IF', 'END_WHILE', 'END_FOR', 'END_CASE', 'ELSE', 'ELSIF')): if line.startswith(
("END_IF", "END_WHILE", "END_FOR", "END_CASE", "ELSE", "ELSIF")
):
indent_level = max(0, indent_level - 1) indent_level = max(0, indent_level - 1)
output_lines.append(" " * indent_level + line) # Aplicar indentación output_lines.append(" " * indent_level + line) # Aplicar indentación
# Aumentar indentación después de IF, WHILE, FOR, CASE, ELSE, ELSIF (simplificado) # Aumentar indentación después de IF, WHILE, FOR, CASE, ELSE, ELSIF (simplificado)
if line.endswith('THEN') or line.endswith('DO') or line.endswith('OF') or line == 'ELSE': if (
line.endswith("THEN")
or line.endswith("DO")
or line.endswith("OF")
or line == "ELSE"
):
indent_level += 1 indent_level += 1
# Nota: Esto no maneja bloques BEGIN/END dentro de SCL # Nota: Esto no maneja bloques BEGIN/END dentro de SCL
return "\n".join(output_lines) return "\n".join(output_lines)
# STL (Statement List) Parser # STL (Statement List) Parser
def get_access_text(access_element): def get_access_text(access_element):
"""Reconstruye una representación textual simple de un Access en STL.""" """Reconstruye una representación textual simple de un Access en STL."""
if access_element is None: if access_element is None:
@ -379,7 +416,9 @@ def get_access_text(access_element):
for comp in components: for comp in components:
name = comp.get("Name", "_ERR_COMP_") name = comp.get("Name", "_ERR_COMP_")
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
has_quotes_elem = comp.xpath("../stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns) has_quotes_elem = comp.xpath(
"../stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns
)
has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true" has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true"
# Usar nombre tal cual por ahora # Usar nombre tal cual por ahora
@ -400,12 +439,16 @@ def get_access_text(access_element):
if constant_elem: if constant_elem:
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns) val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns)
type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns) # Obtener tipo para mejor formato type_elem = constant_elem[0].xpath(
"./stl:ConstantType/text()", namespaces=ns
) # Obtener tipo para mejor formato
const_type = type_elem[0] if type_elem else "" const_type = type_elem[0] if type_elem else ""
const_val = val_elem[0] if val_elem else "_ERR_CONST_" const_val = val_elem[0] if val_elem else "_ERR_CONST_"
# Añadir prefijo de tipo si es necesario (ej. T# , L#) - Simplificado # Añadir prefijo de tipo si es necesario (ej. T# , L#) - Simplificado
if const_type == "Time": return f"T#{const_val}" if const_type == "Time":
if const_type == "ARef": return f"{const_val}" # No necesita prefijo return f"T#{const_val}"
if const_type == "ARef":
return f"{const_val}" # No necesita prefijo
# Añadir más tipos si es necesario # Añadir más tipos si es necesario
return const_val # Valor directo para otros tipos return const_val # Valor directo para otros tipos
@ -436,7 +479,9 @@ def get_access_text(access_element):
# Formatear ancho # Formatear ancho
width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D"} width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D"}
width_char = width_map.get(width, width[0] if width else "?") # Usa primera letra si no mapeado width_char = width_map.get(
width, width[0] if width else "?"
) # Usa primera letra si no mapeado
return f"{area}{width_char}[{reg},{p_format_offset}]" return f"{area}{width_char}[{reg},{p_format_offset}]"
@ -453,16 +498,27 @@ def get_access_text(access_element):
bit_in_byte = bit_offset % 8 bit_in_byte = bit_offset % 8
# Determinar ancho basado en tipo (simplificación) # Determinar ancho basado en tipo (simplificación)
addr_width = "X" # Default a Bit addr_width = "X" # Default a Bit
if addr_type_str == "Byte": addr_width = "B" if addr_type_str == "Byte":
elif addr_type_str == "Word": addr_width = "W" addr_width = "B"
elif addr_type_str in ["DWord", "DInt"]: addr_width = "D" elif addr_type_str == "Word":
addr_width = "W"
elif addr_type_str in ["DWord", "DInt"]:
addr_width = "D"
# Añadir más tipos si es necesario (Real, etc.) # Añadir más tipos si es necesario (Real, etc.)
# Mapear Area para STL estándar # Mapear Area para STL estándar
area_map = { "Input": "I", "Output": "Q", "Memory": "M", area_map = {
"PeripheryInput": "PI", "PeripheryOutput": "PQ", "Input": "I",
"DB": "DB", "DI": "DI", "Local": "L", # L no siempre válido aquí "Output": "Q",
"Timer": "T", "Counter": "C" } "Memory": "M",
"PeripheryInput": "PI",
"PeripheryOutput": "PQ",
"DB": "DB",
"DI": "DI",
"Local": "L", # L no siempre válido aquí
"Timer": "T",
"Counter": "C",
}
stl_area = area_map.get(area, area) stl_area = area_map.get(area, area)
# Manejar DB/DI que necesitan número de bloque # Manejar DB/DI que necesitan número de bloque
@ -482,13 +538,19 @@ def get_access_text(access_element):
return f"_{scope}_?" # Fallback return f"_{scope}_?" # Fallback
def get_comment_text(comment_element): def get_comment_text(comment_element):
"""Extrae texto de un LineComment o Comment.""" """Extrae texto de un LineComment o Comment."""
if comment_element is None: return "" if comment_element is None:
return ""
# Usar get_multilingual_text si los comentarios son multilingües # Usar get_multilingual_text si los comentarios son multilingües
# Si no, extraer texto directamente # Si no, extraer texto directamente
ml_texts = comment_element.xpath(".//mlt:MultilingualTextItem/mlt:AttributeList/mlt:Text/text()", ml_texts = comment_element.xpath(
namespaces={'mlt': "http://www.siemens.com/automation/Openness/SW/Interface/v5"}) # Asumiendo ns ".//mlt:MultilingualTextItem/mlt:AttributeList/mlt:Text/text()",
namespaces={
"mlt": "http://www.siemens.com/automation/Openness/SW/Interface/v5"
},
) # Asumiendo ns
if ml_texts: if ml_texts:
# Podrías intentar obtener un idioma específico o simplemente el primero # Podrías intentar obtener un idioma específico o simplemente el primero
return ml_texts[0].strip() if ml_texts else "" return ml_texts[0].strip() if ml_texts else ""
@ -497,6 +559,7 @@ def get_comment_text(comment_element):
text_nodes = comment_element.xpath("./text()") text_nodes = comment_element.xpath("./text()")
return "".join(text_nodes).strip() return "".join(text_nodes).strip()
def reconstruct_stl_from_statementlist(statement_list_node): def reconstruct_stl_from_statementlist(statement_list_node):
"""Reconstruye el código STL como una cadena de texto desde <StatementList>.""" """Reconstruye el código STL como una cadena de texto desde <StatementList>."""
if statement_list_node is None: if statement_list_node is None:
@ -512,7 +575,9 @@ def reconstruct_stl_from_statementlist(statement_list_node):
# 1. Comentarios al inicio de la línea (como líneas separadas //) # 1. Comentarios al inicio de la línea (como líneas separadas //)
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
initial_comments = stmt.xpath("child::stl:Comment | child::stl:LineComment", namespaces=ns) initial_comments = stmt.xpath(
"child::stl:Comment | child::stl:LineComment", namespaces=ns
)
for comm in initial_comments: for comm in initial_comments:
comment_text = get_comment_text(comm) comment_text = get_comment_text(comm)
if comment_text: if comment_text:
@ -531,10 +596,15 @@ def reconstruct_stl_from_statementlist(statement_list_node):
label_str = f"{label_name_nodes[0]}:" label_str = f"{label_name_nodes[0]}:"
# Buscar comentarios DENTRO de LabelDeclaration pero después de Label # Buscar comentarios DENTRO de LabelDeclaration pero después de Label
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
label_comments = label_decl[0].xpath("./stl:Comment | ./stl:LineComment", namespaces=ns) label_comments = label_decl[0].xpath(
"./stl:Comment | ./stl:LineComment", namespaces=ns
)
for lcomm in label_comments: for lcomm in label_comments:
comment_text = get_comment_text(lcomm) comment_text = get_comment_text(lcomm)
if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea if comment_text:
line_comment += (
f" // {comment_text}" # Añadir al comentario de línea
)
# 3. Token de Instrucción STL # 3. Token de Instrucción STL
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
@ -545,10 +615,15 @@ def reconstruct_stl_from_statementlist(statement_list_node):
instruction_str = token_text instruction_str = token_text
# Comentarios asociados directamente al token # Comentarios asociados directamente al token
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
token_comments = instruction_token[0].xpath("./stl:Comment | ./stl:LineComment", namespaces=ns) token_comments = instruction_token[0].xpath(
"./stl:Comment | ./stl:LineComment", namespaces=ns
)
for tcomm in token_comments: for tcomm in token_comments:
comment_text = get_comment_text(tcomm) comment_text = get_comment_text(tcomm)
if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea if comment_text:
line_comment += (
f" // {comment_text}" # Añadir al comentario de línea
)
# 4. Acceso/Operando STL # 4. Acceso/Operando STL
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
@ -559,10 +634,15 @@ def reconstruct_stl_from_statementlist(statement_list_node):
access_str = access_text access_str = access_text
# Comentarios DENTRO del Access (pueden ser de línea o bloque) # Comentarios DENTRO del Access (pueden ser de línea o bloque)
# CORREGIDO: Añadido namespaces=ns # CORREGIDO: Añadido namespaces=ns
access_comments = access_elem[0].xpath("child::stl:LineComment | child::stl:Comment", namespaces=ns) access_comments = access_elem[0].xpath(
"child::stl:LineComment | child::stl:Comment", namespaces=ns
)
for acc_comm in access_comments: for acc_comm in access_comments:
comment_text = get_comment_text(acc_comm) comment_text = get_comment_text(acc_comm)
if comment_text: line_comment += f" // {comment_text}" # Añadir al comentario de línea if comment_text:
line_comment += (
f" // {comment_text}" # Añadir al comentario de línea
)
# Construir la línea: Etiqueta (si hay) + Tab + Instrucción + Espacio + Operando (si hay) + Comentario(s) # Construir la línea: Etiqueta (si hay) + Tab + Instrucción + Espacio + Operando (si hay) + Comentario(s)
current_line = "" current_line = ""
@ -589,12 +669,112 @@ def reconstruct_stl_from_statementlist(statement_list_node):
return "\n".join(stl_lines) return "\n".join(stl_lines)
# DB Parser
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.
"""
members_data = []
if not member_elements:
return members_data
for member in member_elements:
member_name = member.get("Name")
member_dtype = member.get("Datatype")
member_remanence = member.get("Remanence", "NonRetain") # Default si no existe
member_accessibility = member.get("Accessibility", "Public") # Default
if not member_name or not member_dtype:
print(
f"Advertencia: Miembro sin nombre o tipo de dato encontrado. Saltando."
)
continue
member_info = {
"name": member_name,
"datatype": member_dtype,
"remanence": member_remanence,
"accessibility": member_accessibility,
"start_value": None, # Para valores simples o structs/arrays inicializados globalmente
"comment": None,
"children": [], # Para structs
"array_elements": {}, # Para arrays (índice -> valor)
}
# Extraer comentario del miembro
# Usar namespace iface
comment_node = member.xpath("./iface:Comment", namespaces=ns)
if comment_node:
# Llama a get_multilingual_text que ya maneja el namespace iface internamente
member_info["comment"] = get_multilingual_text(comment_node[0])
# Extraer valor inicial (para tipos simples)
# Usar namespace iface
start_value_node = member.xpath("./iface:StartValue", namespaces=ns)
if start_value_node:
constant_name = start_value_node[0].get("ConstantName")
if constant_name:
member_info["start_value"] = constant_name
else:
member_info["start_value"] = (
start_value_node[0].text
if start_value_node[0].text is not None
else ""
)
# --- Manejar Structs Anidados ---
# Usar namespace iface
nested_sections = member.xpath(
"./iface:Sections/iface:Section/iface:Member", namespaces=ns
)
if nested_sections:
member_info["children"] = parse_interface_members(
nested_sections
) # Llamada recursiva
# --- Manejar Arrays ---
if member_dtype.lower().startswith("array["):
# Usar namespace iface
subelements = member.xpath("./iface:Subelement", namespaces=ns)
for sub in subelements:
path = sub.get("Path")
# Usar namespace iface
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
if path and sub_start_value_node:
constant_name = sub_start_value_node[0].get("ConstantName")
value = (
constant_name
if constant_name
else (
sub_start_value_node[0].text
if sub_start_value_node[0].text is not None
else ""
)
)
member_info["array_elements"][path] = value
else:
# Usar namespace iface
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
if path and sub_comment_node:
# member_info["array_comments"][path] = get_multilingual_text(sub_comment_node[0])
pass
members_data.append(member_info)
return members_data
# --- Main Parsing Function --- # --- Main Parsing Function ---
def parse_network(network_element): def parse_network(network_element):
""" """
Parsea una red, extrae lógica y añade conexiones EN implícitas. Parsea una red, extrae lógica y añade conexiones EN implícitas.
Maneja wires con múltiples destinos. Maneja wires con múltiples destinos. (Función original adaptada para namespaces)
""" """
if network_element is None: if network_element is None:
return { return {
@ -607,15 +787,16 @@ def parse_network(network_element):
network_id = network_element.get("ID") network_id = network_element.get("ID")
# --- Extracción Título/Comentario (sin cambios respecto a la última versión) --- # Extracción Título/Comentario (usar namespace iface para MultilingualText)
title_element = network_element.xpath( title_element = network_element.xpath(
".//*[local-name()='MultilingualText'][@CompositionName='Title']" ".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
) )
network_title = ( network_title = (
get_multilingual_text(title_element[0]) get_multilingual_text(title_element[0])
if title_element if title_element
else f"Network {network_id}" else f"Network {network_id}"
) )
# Asume que el comentario está en ObjectList dentro de CompileUnit
comment_element = network_element.xpath( comment_element = network_element.xpath(
"./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']" "./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']"
) )
@ -623,31 +804,31 @@ def parse_network(network_element):
get_multilingual_text(comment_element[0]) if comment_element else "" get_multilingual_text(comment_element[0]) if comment_element else ""
) )
# Buscar FlgNet usando namespace flg
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns) flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
if not flgnet_list: if not flgnet_list:
# print(f"Advertencia: FlgNet no encontrado en Red ID={network_id}. Puede estar vacía o ser comentario.")
return { return {
"id": network_id, "id": network_id,
"title": network_title, "title": network_title,
"comment": network_comment, "comment": network_comment,
"logic": [], "logic": [],
"language": "Unknown",
"error": "FlgNet not found", "error": "FlgNet not found",
} }
flgnet = flgnet_list[0] flgnet = flgnet_list[0]
# 1. Parsear Access, Parts y Calls (sin cambios) # 1. Parsear Access, Parts y Calls (llaman a funciones que ya usan ns)
access_map = { access_map = {
acc_info["uid"]: acc_info acc_info["uid"]: acc_info
for acc in flgnet.xpath(".//flg:Access", namespaces=ns) for acc in flgnet.xpath(".//flg:Access", namespaces=ns) # Usa ns
if (acc_info := parse_access(acc)) and acc_info["type"] != "unknown" if (acc_info := parse_access(acc)) and acc_info["type"] != "unknown"
} }
parts_and_calls_map = {} parts_and_calls_map = {}
# Usa ns
instruction_elements = flgnet.xpath(".//flg:Part | .//flg:Call", namespaces=ns) instruction_elements = flgnet.xpath(".//flg:Part | .//flg:Call", namespaces=ns)
for element in instruction_elements: for element in instruction_elements:
parsed_info = None parsed_info = None
tag_name = etree.QName( tag_name = etree.QName(element.tag).localname
element.tag
).localname # Obtener nombre local de la etiqueta
if tag_name == "Part": if tag_name == "Part":
parsed_info = parse_part(element) parsed_info = parse_part(element)
elif tag_name == "Call": elif tag_name == "Call":
@ -659,85 +840,50 @@ def parse_network(network_element):
f"Advertencia: Se ignoró un Part/Call inválido en la red {network_id}" f"Advertencia: Se ignoró un Part/Call inválido en la red {network_id}"
) )
# --- 2. Parsear Wires (MODIFICADO para multi-destino) --- # 2. Parsear Wires (con namespaces)
wire_connections = defaultdict( wire_connections = defaultdict(list)
list source_connections = defaultdict(list)
) # (dest_uid, dest_pin) -> [(src_uid, src_pin), ...] eno_outputs = defaultdict(list)
source_connections = defaultdict( # Cachear QNames con namespace flg
list flg_ns_uri = ns["flg"]
) # (src_uid, src_pin) -> [(dest_uid, dest_pin), ...]
eno_outputs = defaultdict(
list
) # src_uid -> [(dest_uid, dest_pin), ...] (conexiones DESDE eno)
flg_ns_uri = ns["flg"] # Cache namespace URI
qname_powerrail = etree.QName(flg_ns_uri, "Powerrail") qname_powerrail = etree.QName(flg_ns_uri, "Powerrail")
qname_identcon = etree.QName(flg_ns_uri, "IdentCon") qname_identcon = etree.QName(flg_ns_uri, "IdentCon")
qname_namecon = etree.QName(flg_ns_uri, "NameCon") qname_namecon = etree.QName(flg_ns_uri, "NameCon")
# Usa ns
for wire in flgnet.xpath(".//flg:Wire", namespaces=ns): for wire in flgnet.xpath(".//flg:Wire", namespaces=ns):
children = wire.getchildren() children = wire.getchildren()
if len(children) < 2: if len(children) < 2:
continue # Ignorar wires sin fuente y al menos un destino continue
source_elem = children[0] source_elem = children[0]
source_uid, source_pin = None, None source_uid, source_pin = None, None
# Determinar fuente
if source_elem.tag == qname_powerrail: if source_elem.tag == qname_powerrail:
source_uid, source_pin = "POWERRAIL", "out" source_uid, source_pin = "POWERRAIL", "out"
elif source_elem.tag == qname_identcon: elif source_elem.tag == qname_identcon:
source_uid, source_pin = ( source_uid, source_pin = source_elem.get("UId"), "value"
source_elem.get("UId"),
"value",
) # Acceso a variable/constante
elif source_elem.tag == qname_namecon: elif source_elem.tag == qname_namecon:
source_uid, source_pin = source_elem.get("UId"), source_elem.get( source_uid, source_pin = source_elem.get("UId"), source_elem.get("Name")
"Name"
) # Salida de instrucción
if source_uid is None: if source_uid is None:
continue # No se pudo determinar la fuente continue
source_info = (source_uid, source_pin)
source_info = (source_uid, source_pin) # Par de fuente
# Iterar sobre TODOS los posibles destinos (desde el segundo hijo en adelante)
for dest_elem in children[1:]: for dest_elem in children[1:]:
dest_uid, dest_pin = None, None dest_uid, dest_pin = None, None
# Determinar destino
if dest_elem.tag == qname_identcon: if dest_elem.tag == qname_identcon:
dest_uid, dest_pin = ( dest_uid, dest_pin = dest_elem.get("UId"), "value"
dest_elem.get("UId"),
"value",
) # Entrada a variable/constante (Coil, etc.)
elif dest_elem.tag == qname_namecon: elif dest_elem.tag == qname_namecon:
dest_uid, dest_pin = dest_elem.get("UId"), dest_elem.get( dest_uid, dest_pin = dest_elem.get("UId"), dest_elem.get("Name")
"Name"
) # Entrada a instrucción
# Guardar conexiones si son válidas
if dest_uid is not None and dest_pin is not None: if dest_uid is not None and dest_pin is not None:
# Mapa de Conexiones (Destino -> [Fuentes])
dest_key = (dest_uid, dest_pin) dest_key = (dest_uid, dest_pin)
if source_info not in wire_connections[dest_key]: if source_info not in wire_connections[dest_key]:
wire_connections[dest_key].append(source_info) wire_connections[dest_key].append(source_info)
# Mapa de Fuentes (Fuente -> [Destinos])
source_key = (source_uid, source_pin) source_key = (source_uid, source_pin)
dest_info = (dest_uid, dest_pin) dest_info = (dest_uid, dest_pin)
if dest_info not in source_connections[source_key]: if dest_info not in source_connections[source_key]:
source_connections[source_key].append(dest_info) source_connections[source_key].append(dest_info)
# Registrar conexiones que SALEN de un pin 'eno'
if source_pin == "eno" and source_uid in parts_and_calls_map: if source_pin == "eno" and source_uid in parts_and_calls_map:
if dest_info not in eno_outputs[source_uid]: if dest_info not in eno_outputs[source_uid]:
eno_outputs[source_uid].append(dest_info) eno_outputs[source_uid].append(dest_info)
# else: # Debug opcional si un elemento no es destino válido
# print(f"Advertencia: Elemento en Wire {wire.get('UId')} no es destino válido: {etree.tostring(dest_elem)}")
# --- FIN MODIFICACIÓN Wire ---
# 3. Construcción Lógica Inicial (sin cambios) # 3. Construcción Lógica Inicial (sin cambios en lógica, pero verificar llamadas)
all_logic_steps = {} all_logic_steps = {}
functional_block_types = [ functional_block_types = [
"Move", "Move",
@ -751,7 +897,13 @@ def parse_network(network_element):
"Se", "Se",
"Sd", "Sd",
"BLKMOV", "BLKMOV",
] "TON",
"TOF",
"TP",
"CTU",
"CTD",
"CTUD",
] # Añadidos timers/counters SCL
rlo_generators = [ rlo_generators = [
"Contact", "Contact",
"O", "O",
@ -765,46 +917,42 @@ def parse_network(network_element):
"Xor", "Xor",
"PBox", "PBox",
"NBox", "NBox",
] "Not",
] # Añadido Not
for instruction_uid, instruction_info in parts_and_calls_map.items(): for instruction_uid, instruction_info in parts_and_calls_map.items():
# Copiar info básica
instruction_repr = {"instruction_uid": instruction_uid, **instruction_info} instruction_repr = {"instruction_uid": instruction_uid, **instruction_info}
instruction_repr["inputs"] = {} instruction_repr["inputs"] = {}
instruction_repr["outputs"] = {} instruction_repr["outputs"] = {}
# --- INICIO: Manejo Especial SdCoil y otros Timers ---
original_type = instruction_info["type"] original_type = instruction_info["type"]
current_type = original_type current_type = original_type
input_pin_mapping = {} # Mapa XML pin -> JSON pin input_pin_mapping = {}
output_pin_mapping = {} # Mapa XML pin -> JSON pin output_pin_mapping = {}
# --- Manejo Especial Tipos ---
if original_type == "SdCoil": if original_type == "SdCoil":
print( current_type = "Se"
f" Advertencia: Reinterpretando 'SdCoil' (UID: {instruction_uid}) como 'Se' (Pulse Timer)." input_pin_mapping = {"in": "s", "operand": "timer", "value": "tv"}
) output_pin_mapping = {"out": "q"}
current_type = "Se" # Tratarlo como Se (TP) elif original_type in ["Se", "Sd", "TON", "TOF", "TP"]:
input_pin_mapping = { input_pin_mapping = {
"in": "s", # Pin XML 'in' mapea a JSON 's' (Start) "s": "s",
"operand": "timer", # Pin XML 'operand' mapea a JSON 'timer' (Instance) "in": "in",
"value": "tv", # Pin XML 'value' mapea a JSON 'tv' (Time Value) "tv": "tv",
"pt": "pt",
"r": "r",
"timer": "timer",
} }
output_pin_mapping = {"out": "q"} # Pin XML 'out' mapea a JSON 'q' (Output) output_pin_mapping = {"q": "q", "Q": "Q", "rt": "rt", "ET": "ET"}
elif original_type in ["Se", "Sd"]: elif original_type in ["CTU", "CTD", "CTUD"]:
# Mapear pines estándar de Se/Sd para consistencia con TP/TON input_pin_mapping = {
input_pin_mapping = {"s": "s", "tv": "tv", "r": "r", "timer": "timer"} "cu": "CU",
output_pin_mapping = { "cd": "CD",
"q": "q", "r": "R",
"rt": "rt", # "rtbcd": "rtbcd" (ignorar BCD) "ld": "LD",
"pv": "PV",
"counter": "counter",
} }
# Añadir otros mapeos si son necesarios para otros bloques (ej. Contadores) output_pin_mapping = {"qu": "QU", "qd": "QD", "cv": "CV"}
# elif original_type == "CTU": instruction_repr["type"] = current_type
# input_pin_mapping = {"cu": "cu", "r": "r", "pv": "pv", "counter": "counter"} # 'counter' es inventado para instancia
# output_pin_mapping = {"qu": "qu", "cv": "cv"}
# --- FIN Manejo Especial ---
instruction_repr["type"] = current_type # Actualizar el tipo si se cambió
# Mapear Entradas usando el mapeo de pines
possible_input_pins = set( possible_input_pins = set(
[ [
"en", "en",
@ -825,14 +973,14 @@ def parse_network(network_element):
"ld", "ld",
"pre", "pre",
"SRCBLK", "SRCBLK",
"PT",
] ]
) # Ampliar con pines conocidos ) # Añadido PT
for xml_pin_name in possible_input_pins: for xml_pin_name in possible_input_pins:
dest_key = (instruction_uid, xml_pin_name) dest_key = (instruction_uid, xml_pin_name)
if dest_key in wire_connections: if dest_key in wire_connections:
sources_list = wire_connections[dest_key] sources_list = wire_connections[dest_key]
input_sources_repr = [] input_sources_repr = []
# ... (lógica existente para obtener input_sources_repr de sources_list) ...
for source_uid, source_pin in sources_list: for source_uid, source_pin in sources_list:
if source_uid == "POWERRAIL": if source_uid == "POWERRAIL":
input_sources_repr.append({"type": "powerrail"}) input_sources_repr.append({"type": "powerrail"})
@ -841,36 +989,38 @@ def parse_network(network_element):
elif source_uid in parts_and_calls_map: elif source_uid in parts_and_calls_map:
source_instr_info = parts_and_calls_map[source_uid] source_instr_info = parts_and_calls_map[source_uid]
source_original_type = source_instr_info["type"] source_original_type = source_instr_info["type"]
# Obtener el mapeo de salida para el tipo de la fuente (si existe)
source_output_mapping = {} source_output_mapping = {}
if source_original_type == "SdCoil": if source_original_type == "SdCoil":
source_output_mapping = {"out": "q"} source_output_mapping = {"out": "q"}
elif source_original_type in ["Se", "Sd"]: elif source_original_type in ["Se", "Sd", "TON", "TOF", "TP"]:
source_output_mapping = {"q": "q", "rt": "rt"} source_output_mapping = {
"q": "q",
# Usar el pin mapeado si existe, sino el original "Q": "Q",
mapped_source_pin = source_output_mapping.get(source_pin, source_pin) "rt": "rt",
"ET": "ET",
input_sources_repr.append({ }
elif source_original_type in ["CTU", "CTD", "CTUD"]:
source_output_mapping = {"qu": "QU", "qd": "QD", "cv": "CV"}
mapped_source_pin = source_output_mapping.get(
source_pin, source_pin
)
input_sources_repr.append(
{
"type": "connection", "type": "connection",
"source_instruction_type": source_original_type, # Guardar tipo original puede ser útil "source_instruction_type": source_original_type,
"source_instruction_uid": source_uid, "source_instruction_uid": source_uid,
"source_pin": mapped_source_pin # <-- USAR PIN MAPEADO "source_pin": mapped_source_pin,
}) }
)
else: else:
input_sources_repr.append( input_sources_repr.append(
{"type": "unknown_source", "uid": source_uid} {"type": "unknown_source", "uid": source_uid}
) )
# Usar el nombre de pin mapeado para el JSON
json_pin_name = input_pin_mapping.get(xml_pin_name, xml_pin_name) json_pin_name = input_pin_mapping.get(xml_pin_name, xml_pin_name)
if len(input_sources_repr) == 1: if len(input_sources_repr) == 1:
instruction_repr["inputs"][json_pin_name] = input_sources_repr[0] instruction_repr["inputs"][json_pin_name] = input_sources_repr[0]
elif len(input_sources_repr) > 1: elif len(input_sources_repr) > 1:
instruction_repr["inputs"][json_pin_name] = input_sources_repr instruction_repr["inputs"][json_pin_name] = input_sources_repr
# Mapear Salidas usando el mapeo de pines
possible_output_pins = set( possible_output_pins = set(
[ [
"out", "out",
@ -886,17 +1036,15 @@ def parse_network(network_element):
"cvbcd", "cvbcd",
"QU", "QU",
"QD", "QD",
"ET",
] ]
) ) # Añadido ET
for xml_pin_name in possible_output_pins: for xml_pin_name in possible_output_pins:
source_key = (instruction_uid, xml_pin_name) source_key = (instruction_uid, xml_pin_name)
if source_key in source_connections: if source_key in source_connections:
# Usar el nombre de pin mapeado para el JSON
json_pin_name = output_pin_mapping.get(xml_pin_name, xml_pin_name) json_pin_name = output_pin_mapping.get(xml_pin_name, xml_pin_name)
if json_pin_name not in instruction_repr["outputs"]: if json_pin_name not in instruction_repr["outputs"]:
instruction_repr["outputs"][json_pin_name] = [] instruction_repr["outputs"][json_pin_name] = []
for dest_uid, dest_pin in source_connections[source_key]: for dest_uid, dest_pin in source_connections[source_key]:
if dest_uid in access_map: if dest_uid in access_map:
if ( if (
@ -906,10 +1054,9 @@ def parse_network(network_element):
instruction_repr["outputs"][json_pin_name].append( instruction_repr["outputs"][json_pin_name].append(
access_map[dest_uid] access_map[dest_uid]
) )
all_logic_steps[instruction_uid] = instruction_repr all_logic_steps[instruction_uid] = instruction_repr
# 4. Inferencia EN (sin cambios) # 4. Inferencia EN (sin cambios en lógica)
processed_blocks_en_inference = set() processed_blocks_en_inference = set()
something_changed = True something_changed = True
inference_passes = 0 inference_passes = 0
@ -930,8 +1077,8 @@ def parse_network(network_element):
for i, instruction in enumerate(ordered_logic_list_for_en): for i, instruction in enumerate(ordered_logic_list_for_en):
part_uid = instruction["instruction_uid"] part_uid = instruction["instruction_uid"]
part_type_original = ( part_type_original = (
instruction["type"].replace("_scl", "").replace("_error", "") instruction["type"].replace(SCL_SUFFIX, "").replace("_error", "")
) ) # Usa SCL_SUFFIX
if ( if (
part_type_original in functional_block_types part_type_original in functional_block_types
and "en" not in instruction["inputs"] and "en" not in instruction["inputs"]
@ -943,7 +1090,9 @@ def parse_network(network_element):
prev_instr = ordered_logic_list_for_en[j] prev_instr = ordered_logic_list_for_en[j]
prev_uid = prev_instr["instruction_uid"] prev_uid = prev_instr["instruction_uid"]
prev_type_original = ( prev_type_original = (
prev_instr["type"].replace("_scl", "").replace("_error", "") prev_instr["type"]
.replace(SCL_SUFFIX, "")
.replace("_error", "")
) )
if prev_type_original in rlo_generators: if prev_type_original in rlo_generators:
inferred_en_source = { inferred_en_source = {
@ -979,7 +1128,7 @@ def parse_network(network_element):
processed_blocks_en_inference.add(part_uid) processed_blocks_en_inference.add(part_uid)
something_changed = True something_changed = True
# 5. Añadir lógica ENO interesante (sin cambios) # 5. Añadir lógica ENO interesante (sin cambios en lógica)
for source_instr_uid, eno_destinations in eno_outputs.items(): for source_instr_uid, eno_destinations in eno_outputs.items():
if source_instr_uid not in all_logic_steps: if source_instr_uid not in all_logic_steps:
continue continue
@ -1025,17 +1174,30 @@ def parse_network(network_element):
if interesting_eno_logic: if interesting_eno_logic:
all_logic_steps[source_instr_uid]["eno_logic"] = interesting_eno_logic all_logic_steps[source_instr_uid]["eno_logic"] = interesting_eno_logic
# 6. Ordenar y Devolver (sin cambios) # 6. Ordenar y Devolver
network_logic_final = [ network_logic_final = [
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
] ]
# Determinar lenguaje de la red para devolverlo
network_lang = "Unknown"
if network_element is not None:
attr_list_net = network_element.xpath("./*[local-name()='AttributeList']")
if attr_list_net:
lang_node_net = attr_list_net[0].xpath(
"./*[local-name()='ProgrammingLanguage']/text()"
)
if lang_node_net:
network_lang = lang_node_net[0].strip()
return { return {
"id": network_id, "id": network_id,
"title": network_title, "title": network_title,
"comment": network_comment, "comment": network_comment,
"language": network_lang,
"logic": network_logic_final, "logic": network_logic_final,
} }
def convert_xml_to_json(xml_filepath, json_filepath): def convert_xml_to_json(xml_filepath, json_filepath):
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
if not os.path.exists(xml_filepath): if not os.path.exists(xml_filepath):
@ -1047,25 +1209,37 @@ def convert_xml_to_json(xml_filepath, json_filepath):
tree = etree.parse(xml_filepath, parser) tree = etree.parse(xml_filepath, parser)
root = tree.getroot() root = tree.getroot()
print("Paso 1: Parseo XML completado.") print("Paso 1: Parseo XML completado.")
print("Paso 2: Buscando el bloque SW.Blocks.FC...") # Asume FC primero print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB o SW.Blocks.GlobalDB...")
block_list = root.xpath("//*[local-name()='SW.Blocks.FC']") # --- MODIFICADO: Buscar FC, FB o GlobalDB ---
block_type_found = "FC" block_list = root.xpath("//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB']")
if not block_list: block_type_found = None
block_list = root.xpath( the_block = None
"//*[local-name()='SW.Blocks.FB']"
) # Busca FB si no hay FC if block_list:
block_type_found = "FB"
if not block_list:
print("Error Crítico: No se encontró <SW.Blocks.FC> ni <SW.Blocks.FB>.")
return
else:
print(
"Advertencia: Se encontró <SW.Blocks.FB> en lugar de <SW.Blocks.FC>."
)
the_block = block_list[0] the_block = block_list[0]
print( # Obtener el nombre real de la etiqueta encontrada
f"Paso 2: Bloque SW.Blocks.{block_type_found} encontrado (ID={the_block.get('ID')})." 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" # Identificar el tipo DB
print(f"Paso 2: Bloque {block_tag_name} encontrado (ID={the_block.get('ID')}).")
else:
# Mensaje de error más específico y añadimos depuración
print("Error Crítico: No se encontró el elemento raíz del bloque (<SW.Blocks.FC>, <SW.Blocks.FB> o <SW.Blocks.GlobalDB>) usando XPath.")
# --- Añadir Debugging ---
print(f"DEBUG: Tag del elemento raíz del XML: {root.tag}")
print(f"DEBUG: Primeros hijos del raíz:")
for i, child in enumerate(root.getchildren()):
if i < 5: # Imprimir solo los primeros 5 para no saturar
print(f"DEBUG: - Hijo {i+1}: {child.tag}")
else:
print("DEBUG: - ... (más hijos)")
break
# --- Fin Debugging ---
return # Salir si no se encuentra el bloque principal
print("Paso 3: Extrayendo atributos del bloque...") print("Paso 3: Extrayendo atributos del bloque...")
attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']") attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']")
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown" block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
@ -1241,12 +1415,18 @@ def convert_xml_to_json(xml_filepath, json_filepath):
reconstructed_stl = f"// STL extraction failed for Network {network_id}: StatementList node not found.\n" reconstructed_stl = f"// STL extraction failed for Network {network_id}: StatementList node not found.\n"
if statement_list_node: if statement_list_node:
print(f" Reconstruyendo STL desde StatementList para red {network_id}...") print(
f" Reconstruyendo STL desde StatementList para red {network_id}..."
)
# Llama a la nueva función de reconstrucción STL # Llama a la nueva función de reconstrucción STL
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node[0]) reconstructed_stl = reconstruct_stl_from_statementlist(
statement_list_node[0]
)
# print(f" ... STL reconstruido (parcial):\n{reconstructed_stl[:200]}...") # Preview opcional # print(f" ... STL reconstruido (parcial):\n{reconstructed_stl[:200]}...") # Preview opcional
else: else:
print(f" Advertencia: No se encontró nodo <StatementList> para red STL {network_id}.") print(
f" Advertencia: No se encontró nodo <StatementList> para red STL {network_id}."
)
# Guardar como un chunk de texto crudo # Guardar como un chunk de texto crudo
parsed_network_data = { parsed_network_data = {
@ -1340,6 +1520,7 @@ def convert_xml_to_json(xml_filepath, json_filepath):
traceback.print_exc() traceback.print_exc()
print("--- Fin Traceback ---") print("--- Fin Traceback ---")
if __name__ == "__main__": if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal # Imports necesarios solo para la ejecución como script principal
import argparse import argparse
@ -1371,7 +1552,9 @@ if __name__ == "__main__":
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
json_output_file = os.path.join(output_dir, f"{xml_filename_base}_simplified.json") json_output_file = os.path.join(output_dir, f"{xml_filename_base}_simplified.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 principal de conversión del script # Llamar a la función principal de conversión del script
# Asumiendo que tu función principal se llama convert_xml_to_json(input_path, output_path) # Asumiendo que tu función principal se llama convert_xml_to_json(input_path, output_path)
@ -1380,6 +1563,6 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}") print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla

View File

@ -28,6 +28,7 @@ SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be us
# which works but passing it explicitly might be cleaner. # which works but passing it explicitly might be cleaner.
data = {} data = {}
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
""" """
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map) Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
@ -37,24 +38,45 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
(Esta es la implementación de la función como la tenías en el archivo original) (Esta es la implementación de la función como la tenías en el archivo original)
""" """
instr_uid = instruction["instruction_uid"] instr_uid = instruction["instruction_uid"]
instr_type_original = instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "") instr_type_original = (
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
)
made_change = False made_change = False
# Check if this instruction *could* generate a condition suitable for grouping # Check if this instruction *could* generate a condition suitable for grouping
# It must have been processed by the new SymPy method # It must have been processed by the new SymPy method
if ( if (
not instruction.get("type", "").endswith(SCL_SUFFIX) # Check if processed by new method not instruction.get("type", "").endswith(
SCL_SUFFIX
) # Check if processed by new method
or "_error" in instruction.get("type", "") or "_error" in instruction.get("type", "")
or instruction.get("grouped", False) or instruction.get("grouped", False)
or instr_type_original not in [ # Original types that produce boolean results or instr_type_original
"Contact", "O", "Eq", "Ne", "Gt", "Lt", "Ge", "Le", "PBox", "NBox", "And", "Xor", "Not" # Add others like comparison not in [ # Original types that produce boolean results
"Contact",
"O",
"Eq",
"Ne",
"Gt",
"Lt",
"Ge",
"Le",
"PBox",
"NBox",
"And",
"Xor",
"Not", # Add others like comparison
] ]
): ):
return False return False
# Avoid reagruping if SCL already contains a complex IF (less likely now) # Avoid reagruping if SCL already contains a complex IF (less likely now)
current_scl = instruction.get("scl", "") current_scl = instruction.get("scl", "")
if current_scl.strip().startswith("IF") and "END_IF;" in current_scl and GROUPED_COMMENT not in current_scl: if (
current_scl.strip().startswith("IF")
and "END_IF;" in current_scl
and GROUPED_COMMENT not in current_scl
):
return False return False
# *** Get the SymPy expression for the condition *** # *** Get the SymPy expression for the condition ***
@ -62,20 +84,34 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
sympy_condition_expr = sympy_map.get(map_key_out) sympy_condition_expr = sympy_map.get(map_key_out)
# No SymPy expression found or trivial conditions # No SymPy expression found or trivial conditions
if sympy_condition_expr is None or sympy_condition_expr in [sympy.true, sympy.false]: if sympy_condition_expr is None or sympy_condition_expr in [
sympy.true,
sympy.false,
]:
return False return False
# --- Find consumer instructions (logic similar to before) --- # --- Find consumer instructions (logic similar to before) ---
grouped_instructions_cores = [] grouped_instructions_cores = []
consumer_instr_list = [] consumer_instr_list = []
network_logic = next((net["logic"] for net in data["networks"] if net["id"] == network_id), []) network_logic = next(
if not network_logic: return False (net["logic"] for net in data["networks"] if net["id"] == network_id), []
)
if not network_logic:
return False
groupable_types = [ # Types whose *final SCL* we want to group groupable_types = [ # Types whose *final SCL* we want to group
"Move", "Add", "Sub", "Mul", "Div", "Mod", "Convert", "Move",
"Call_FC", "Call_FB", # Assuming these generate final SCL in their processors now "Add",
"Sub",
"Mul",
"Div",
"Mod",
"Convert",
"Call_FC",
"Call_FB", # Assuming these generate final SCL in their processors now
# SCoil/RCoil might also be groupable if their SCL is final assignment # SCoil/RCoil might also be groupable if their SCL is final assignment
"SCoil", "RCoil" "SCoil",
"RCoil",
] ]
for consumer_instr in network_logic: for consumer_instr in network_logic:
@ -85,19 +121,27 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
consumer_en = consumer_instr.get("inputs", {}).get("en") consumer_en = consumer_instr.get("inputs", {}).get("en")
consumer_type = consumer_instr.get("type", "") # Current type suffix matters consumer_type = consumer_instr.get("type", "") # Current type suffix matters
consumer_type_original = consumer_type.replace(SCL_SUFFIX, "").replace("_error", "") consumer_type_original = consumer_type.replace(SCL_SUFFIX, "").replace(
"_error", ""
)
is_enabled_by_us = False is_enabled_by_us = False
if ( isinstance(consumer_en, dict) and consumer_en.get("type") == "connection" and if (
consumer_en.get("source_instruction_uid") == instr_uid and isinstance(consumer_en, dict)
consumer_en.get("source_pin") == "out"): and consumer_en.get("type") == "connection"
and consumer_en.get("source_instruction_uid") == instr_uid
and consumer_en.get("source_pin") == "out"
):
is_enabled_by_us = True is_enabled_by_us = True
# Check if consumer is groupable AND has its final SCL generated # Check if consumer is groupable AND has its final SCL generated
# The suffix check needs adjustment based on how terminating processors set it. # The suffix check needs adjustment based on how terminating processors set it.
# Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix. # Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix.
if ( is_enabled_by_us and consumer_type.endswith(SCL_SUFFIX) and # Or a specific "final_scl" suffix if (
consumer_type_original in groupable_types ): is_enabled_by_us
and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix
and consumer_type_original in groupable_types
):
consumer_scl = consumer_instr.get("scl", "") consumer_scl = consumer_instr.get("scl", "")
# Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now) # Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now)
@ -105,9 +149,15 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
if consumer_scl: if consumer_scl:
# If consumer SCL itself is an IF generated by EN, take the body # If consumer SCL itself is an IF generated by EN, take the body
if consumer_scl.strip().startswith("IF"): if consumer_scl.strip().startswith("IF"):
match = re.search(r"THEN\s*(.*?)\s*END_IF;", consumer_scl, re.DOTALL | re.IGNORECASE) match = re.search(
r"THEN\s*(.*?)\s*END_IF;",
consumer_scl,
re.DOTALL | re.IGNORECASE,
)
core_scl = match.group(1).strip() if match else None core_scl = match.group(1).strip() if match else None
elif not consumer_scl.strip().startswith("//"): # Otherwise, take the whole line if not comment elif not consumer_scl.strip().startswith(
"//"
): # Otherwise, take the whole line if not comment
core_scl = consumer_scl.strip() core_scl = consumer_scl.strip()
if core_scl: if core_scl:
@ -116,12 +166,16 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
# --- If groupable consumers found --- # --- If groupable consumers found ---
if len(grouped_instructions_cores) > 1: if len(grouped_instructions_cores) > 1:
print(f"INFO: Agrupando {len(grouped_instructions_cores)} instr. bajo condición de {instr_type_original} UID {instr_uid}") print(
f"INFO: Agrupando {len(grouped_instructions_cores)} instr. bajo condición de {instr_type_original} UID {instr_uid}"
)
# *** Simplify the SymPy condition *** # *** Simplify the SymPy condition ***
try: try:
#simplified_expr = sympy.simplify_logic(sympy_condition_expr, force=True) # simplified_expr = sympy.simplify_logic(sympy_condition_expr, force=True)
simplified_expr = sympy.logic.boolalg.to_dnf(sympy_condition_expr, simplify=True) simplified_expr = sympy.logic.boolalg.to_dnf(
sympy_condition_expr, simplify=True
)
except Exception as e: except Exception as e:
print(f"Error simplifying condition for grouping UID {instr_uid}: {e}") print(f"Error simplifying condition for grouping UID {instr_uid}: {e}")
simplified_expr = sympy_condition_expr # Fallback simplified_expr = sympy_condition_expr # Fallback
@ -132,7 +186,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
# *** Build the grouped IF SCL *** # *** Build the grouped IF SCL ***
scl_grouped_lines = [f"IF {condition_scl_simplified} THEN"] scl_grouped_lines = [f"IF {condition_scl_simplified} THEN"]
for core_line in grouped_instructions_cores: for core_line in grouped_instructions_cores:
indented_core = "\n".join([f" {line.strip()}" for line in core_line.splitlines()]) indented_core = "\n".join(
[f" {line.strip()}" for line in core_line.splitlines()]
)
scl_grouped_lines.append(indented_core) scl_grouped_lines.append(indented_core)
scl_grouped_lines.append("END_IF;") scl_grouped_lines.append("END_IF;")
final_grouped_scl = "\n".join(scl_grouped_lines) final_grouped_scl = "\n".join(scl_grouped_lines)
@ -147,6 +203,7 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
return made_change return made_change
def load_processors(processors_dir="processors"): def load_processors(processors_dir="processors"):
""" """
Escanea el directorio, importa módulos, construye el mapa y una lista Escanea el directorio, importa módulos, construye el mapa y una lista
@ -170,7 +227,9 @@ def load_processors(processors_dir="processors"):
try: try:
module = importlib.import_module(full_module_name) module = importlib.import_module(full_module_name)
if hasattr(module, 'get_processor_info') and callable(module.get_processor_info): if hasattr(module, "get_processor_info") and callable(
module.get_processor_info
):
processor_info = module.get_processor_info() processor_info = module.get_processor_info()
info_list = [] info_list = []
if isinstance(processor_info, dict): if isinstance(processor_info, dict):
@ -178,29 +237,51 @@ def load_processors(processors_dir="processors"):
elif isinstance(processor_info, list): elif isinstance(processor_info, list):
info_list = processor_info info_list = processor_info
else: else:
print(f" Advertencia: get_processor_info en {full_module_name} devolvió tipo inesperado. Se ignora.") print(
f" Advertencia: get_processor_info en {full_module_name} devolvió tipo inesperado. Se ignora."
)
continue continue
for info in info_list: for info in info_list:
if isinstance(info, dict) and 'type_name' in info and 'processor_func' in info: if (
type_name = info['type_name'].lower() isinstance(info, dict)
processor_func = info['processor_func'] and "type_name" in info
and "processor_func" in info
):
type_name = info["type_name"].lower()
processor_func = info["processor_func"]
# Obtener prioridad, usar default si no existe # Obtener prioridad, usar default si no existe
priority = info.get('priority', default_priority) priority = info.get("priority", default_priority)
if callable(processor_func): if callable(processor_func):
if type_name in processor_map: if type_name in processor_map:
print(f" Advertencia: '{type_name}' en {full_module_name} sobrescribe definición anterior.") print(
f" Advertencia: '{type_name}' en {full_module_name} sobrescribe definición anterior."
)
processor_map[type_name] = processor_func processor_map[type_name] = processor_func
# Añadir a la lista para ordenar # Añadir a la lista para ordenar
processor_list_unsorted.append({'priority': priority, 'type_name': type_name, 'func': processor_func}) processor_list_unsorted.append(
print(f" - Cargado '{type_name}' (Prio: {priority}) desde {module_name_rel}.py") {
"priority": priority,
"type_name": type_name,
"func": processor_func,
}
)
print(
f" - Cargado '{type_name}' (Prio: {priority}) desde {module_name_rel}.py"
)
else: else:
print(f" Advertencia: 'processor_func' para '{type_name}' en {full_module_name} no es callable.") print(
f" Advertencia: 'processor_func' para '{type_name}' en {full_module_name} no es callable."
)
else: else:
print(f" Advertencia: Entrada inválida en {full_module_name}: {info}") print(
f" Advertencia: Entrada inválida en {full_module_name}: {info}"
)
else: else:
print(f" Advertencia: Módulo {module_name_rel}.py no tiene 'get_processor_info'.") print(
f" Advertencia: Módulo {module_name_rel}.py no tiene 'get_processor_info'."
)
except ImportError as e: except ImportError as e:
print(f"Error importando {full_module_name}: {e}") print(f"Error importando {full_module_name}: {e}")
@ -209,22 +290,24 @@ def load_processors(processors_dir="processors"):
traceback.print_exc() traceback.print_exc()
# Ordenar la lista por prioridad (menor primero) # Ordenar la lista por prioridad (menor primero)
processor_list_sorted = sorted(processor_list_unsorted, key=lambda x: x['priority']) processor_list_sorted = sorted(processor_list_unsorted, key=lambda x: x["priority"])
print(f"\nTotal de tipos de procesadores cargados: {len(processor_map)}") print(f"\nTotal de tipos de procesadores cargados: {len(processor_map)}")
print(f"Orden de procesamiento por prioridad: {[item['type_name'] for item in processor_list_sorted]}") print(
f"Orden de procesamiento por prioridad: {[item['type_name'] for item in processor_list_sorted]}"
)
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada # Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
return processor_map, processor_list_sorted return processor_map, processor_list_sorted
# --- Bucle Principal de Procesamiento (Modificado para STL) --- # --- Bucle Principal de Procesamiento (Modificado para STL) ---
def process_json_to_scl(json_filepath): def process_json_to_scl(json_filepath):
""" """
Lee el JSON simplificado, aplica los procesadores dinámicamente cargados Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
siguiendo un orden de prioridad (ignorando redes STL), y guarda el JSON procesado. y guarda JSON procesado.
""" """
global data # Necesario para que load_processors y process_group_ifs (definidas fuera) puedan acceder a ella. global data
# Considerar pasar 'data' como argumento si es posible refactorizar.
if not os.path.exists(json_filepath): if not os.path.exists(json_filepath):
print(f"Error: JSON no encontrado: {json_filepath}") print(f"Error: JSON no encontrado: {json_filepath}")
@ -232,48 +315,83 @@ def process_json_to_scl(json_filepath):
print(f"Cargando JSON desde: {json_filepath}") print(f"Cargando JSON desde: {json_filepath}")
try: try:
with open(json_filepath, "r", encoding="utf-8") as f: with open(json_filepath, "r", encoding="utf-8") as f:
data = json.load(f) # Carga en 'data' global data = json.load(f)
except Exception as e: except Exception as e:
print(f"Error al cargar JSON: {e}") print(f"Error al cargar JSON: {e}")
traceback.print_exc() traceback.print_exc()
return return
# --- Carga dinámica de procesadores --- # --- Obtener lenguaje del bloque principal ---
block_language = data.get("language", "Unknown")
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}")
# --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO ---
if block_language == "DB":
print(
"INFO: El bloque es un Data Block (DB). 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}")
try:
with open(output_filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print("Guardado de DB completado.")
except Exception as e:
print(f"Error Crítico al guardar JSON del DB: {e}")
traceback.print_exc()
return # <<< SALIR TEMPRANO PARA DBs
# --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) ---
print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...")
script_dir = os.path.dirname(__file__) script_dir = os.path.dirname(__file__)
processors_dir_path = os.path.join(script_dir, 'processors') processors_dir_path = os.path.join(script_dir, "processors")
processor_map, sorted_processors = load_processors(processors_dir_path) processor_map, sorted_processors = load_processors(processors_dir_path)
if not processor_map: if not processor_map:
print("Error crítico: No se cargaron procesadores. Abortando.") print("Error crítico: No se cargaron procesadores. Abortando.")
return return
# --- Crear mapas de acceso por red ---
network_access_maps = {} network_access_maps = {}
# (La lógica para llenar network_access_maps no cambia, puedes copiarla de tu original) # Crear mapas de acceso por red (copiado/adaptado de versión anterior)
for network in data.get("networks", []): for network in data.get("networks", []):
net_id = network["id"] net_id = network["id"]
current_access_map = {} current_access_map = {}
for instr in network.get("logic", []): for instr in network.get("logic", []):
for _, source in instr.get("inputs", {}).items(): for _, source in instr.get("inputs", {}).items():
sources_to_check = (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: 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 current_access_map[src["uid"]] = src
for _, dest_list in instr.get("outputs", {}).items(): for _, dest_list in instr.get("outputs", {}).items():
if isinstance(dest_list, list): if isinstance(dest_list, list):
for dest in dest_list: for dest in dest_list:
if (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 current_access_map[dest["uid"]] = dest
network_access_maps[net_id] = current_access_map network_access_maps[net_id] = current_access_map
# --- Inicializar mapa SymPy y SymbolManager ---
symbol_manager = SymbolManager() symbol_manager = SymbolManager()
sympy_map = {} sympy_map = {}
max_passes = 30 max_passes = 30
passes = 0 passes = 0
processing_complete = False processing_complete = False
print("\n--- Iniciando Bucle de Procesamiento Iterativo (con SymPy y prioridad) ---") print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---")
while passes < max_passes and not processing_complete: while passes < max_passes and not processing_complete:
passes += 1 passes += 1
made_change_in_base_pass = False made_change_in_base_pass = False
@ -284,90 +402,99 @@ def process_json_to_scl(json_filepath):
# --- FASE 1: Procesadores Base (Ignorando STL) --- # --- FASE 1: Procesadores Base (Ignorando STL) ---
print(f" Fase 1 (SymPy Base - Orden por Prioridad):") print(f" Fase 1 (SymPy Base - Orden por Prioridad):")
num_sympy_processed_this_pass = 0 num_sympy_processed_this_pass = 0 # Resetear contador para el pase
for processor_info in sorted_processors: for processor_info in sorted_processors:
current_type_name = processor_info['type_name'] current_type_name = processor_info["type_name"]
func_to_call = processor_info['func'] func_to_call = processor_info["func"]
for network in data.get("networks", []): for network in data.get("networks", []):
network_id = network["id"] network_id = network["id"]
network_lang = network.get("language", "LAD") # Obtener lenguaje de la red network_lang = network.get("language", "LAD")
# *** IGNORAR REDES STL EN ESTA FASE ***
if network_lang == "STL": if network_lang == "STL":
continue # Saltar al siguiente network continue # Saltar STL
access_map = network_access_maps.get(network_id, {}) access_map = network_access_maps.get(network_id, {})
network_logic = network.get("logic", []) network_logic = network.get("logic", [])
for instruction in network_logic: for instruction in network_logic:
instr_uid = instruction.get("instruction_uid") instr_uid = instruction.get("instruction_uid")
instr_type_original = instruction.get("type", "Unknown") instr_type_original = instruction.get("type", "Unknown")
if (
# Saltar si ya procesado, error, agrupado o es chunk STL/SCL/Unsupported instr_type_original.endswith(SCL_SUFFIX)
if (instr_type_original.endswith(SCL_SUFFIX)
or "_error" in instr_type_original or "_error" in instr_type_original
or instruction.get("grouped", False) or instruction.get("grouped", False)
or instr_type_original in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]): or instr_type_original
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]
):
continue continue
# Determinar tipo efectivo (como antes)
lookup_key = instr_type_original.lower() lookup_key = instr_type_original.lower()
effective_type_name = lookup_key effective_type_name = lookup_key
if instr_type_original == "Call": if instr_type_original == "Call":
block_type = instruction.get("block_type", "").upper() block_type = instruction.get("block_type", "").upper()
if block_type == "FC": effective_type_name = "call_fc" if block_type == "FC":
elif block_type == "FB": effective_type_name = "call_fb" effective_type_name = "call_fc"
elif block_type == "FB":
effective_type_name = "call_fb"
# Llamar al procesador si coincide el tipo
if effective_type_name == current_type_name: if effective_type_name == current_type_name:
try: try:
# Pasa sympy_map, symbol_manager y data changed = func_to_call(
changed = func_to_call(instruction, network_id, sympy_map, symbol_manager, data) instruction, network_id, sympy_map, symbol_manager, data
)
if changed: if changed:
made_change_in_base_pass = True made_change_in_base_pass = True
num_sympy_processed_this_pass += 1 num_sympy_processed_this_pass += 1
except Exception as e: except Exception as e:
print(f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}") print(
f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}"
)
traceback.print_exc() traceback.print_exc()
instruction["scl"] = f"// ERROR en SymPy procesador base: {e}" instruction["scl"] = (
f"// ERROR en SymPy procesador base: {e}"
)
instruction["type"] = instr_type_original + "_error" instruction["type"] = instr_type_original + "_error"
made_change_in_base_pass = True # Marcar cambio aunque sea error made_change_in_base_pass = True
print(f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy.") print(
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
)
# --- FASE 2: Agrupación IF (Ignorando STL) --- # --- FASE 2: Agrupación IF (Ignorando STL) ---
if made_change_in_base_pass or passes == 1: if (
made_change_in_base_pass or passes == 1
): # Ejecutar siempre en el primer pase
print(f" Fase 2 (Agrupación IF con Simplificación):") print(f" Fase 2 (Agrupación IF con Simplificación):")
num_grouped_this_pass = 0 num_grouped_this_pass = 0 # Resetear contador para el pase
for network in data.get("networks", []): for network in data.get("networks", []):
network_id = network["id"] network_id = network["id"]
network_lang = network.get("language", "LAD") # Obtener lenguaje network_lang = network.get("language", "LAD")
# *** IGNORAR REDES STL EN ESTA FASE ***
if network_lang == "STL": if network_lang == "STL":
continue # Saltar red STL continue # Saltar STL
network_logic = network.get("logic", []) network_logic = network.get("logic", [])
for instruction in network_logic: for instruction in network_logic:
try: try:
# Llama a process_group_ifs (que necesita acceso a 'data' global o pasado) group_changed = process_group_ifs(
group_changed = process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data) instruction, network_id, sympy_map, symbol_manager, data
)
if group_changed: if group_changed:
made_change_in_group_pass = True made_change_in_group_pass = True
num_grouped_this_pass += 1 num_grouped_this_pass += 1
except Exception as e: except Exception as e:
print(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() 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ó el procesamiento ---
if not made_change_in_base_pass and not made_change_in_group_pass: if not made_change_in_base_pass and not made_change_in_group_pass:
print(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 processing_complete = True
else: else:
print(f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando...") print(
f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..."
)
# --- Comprobar límite de pases --- # --- Comprobar límite de pases ---
if passes == max_passes and not processing_complete: if passes == max_passes and not processing_complete:
@ -376,46 +503,51 @@ def process_json_to_scl(json_filepath):
# --- FIN BUCLE ITERATIVO --- # --- FIN BUCLE ITERATIVO ---
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) --- # --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
print("\n--- Verificación Final de Instrucciones No Procesadas ---") print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---")
unprocessed_count = 0 unprocessed_count = 0
unprocessed_details = [] unprocessed_details = []
# Añadir RAW_STL_CHUNK a los tipos ignorados ignored_types = [
ignored_types = ['raw_scl_chunk', 'unsupported_lang', 'raw_stl_chunk'] # Añadido raw_stl_chunk "raw_scl_chunk",
"unsupported_lang",
"raw_stl_chunk",
] # Añadido raw_stl_chunk
for network in data.get("networks", []): for network in data.get("networks", []):
network_id = network.get("id", "Unknown ID") network_id = network.get("id", "Unknown ID")
network_title = network.get("title", f"Network {network_id}") network_title = network.get("title", f"Network {network_id}")
network_lang = network.get("language", "LAD") # Obtener lenguaje network_lang = network.get("language", "LAD")
# No verificar instrucciones dentro de redes STL, ya que no se procesan
if network_lang == "STL": if network_lang == "STL":
continue continue # No verificar redes STL
for instruction in network.get("logic", []): for instruction in network.get("logic", []):
instr_uid = instruction.get("instruction_uid", "Unknown UID") instr_uid = instruction.get("instruction_uid", "Unknown UID")
instr_type = instruction.get("type", "Unknown Type") instr_type = instruction.get("type", "Unknown Type")
is_grouped = instruction.get("grouped", False) is_grouped = instruction.get("grouped", False)
if (
# Condición revisada para ignorar los chunks crudos not instr_type.endswith(SCL_SUFFIX)
if (not instr_type.endswith(SCL_SUFFIX) and and "_error" not in instr_type
"_error" not in instr_type and and not is_grouped
not is_grouped and and instr_type.lower() not in ignored_types
instr_type.lower() not in ignored_types): # Verifica contra lista actualizada ):
unprocessed_count += 1 unprocessed_count += 1
unprocessed_details.append( unprocessed_details.append(
f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), " f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), "
f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'" f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'"
) )
if unprocessed_count > 0: if unprocessed_count > 0:
print(f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:") print(
for detail in unprocessed_details: print(detail) f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:"
)
for detail in unprocessed_details:
print(detail)
else: else:
print("INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas.") print(
"INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas."
)
# --- Guardar JSON Final --- # --- Guardar JSON Final ---
output_filename = json_filepath.replace("_simplified.json", "_simplified_processed.json") output_filename = json_filepath.replace(
print(f"\nGuardando JSON procesado en: {output_filename}") "_simplified.json", "_simplified_processed.json"
)
print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}")
try: try:
with open(output_filename, "w", encoding="utf-8") as f: with open(output_filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False) json.dump(data, f, indent=4, ensure_ascii=False)
@ -424,6 +556,7 @@ def process_json_to_scl(json_filepath):
print(f"Error Crítico al guardar JSON procesado: {e}") print(f"Error Crítico al guardar JSON procesado: {e}")
traceback.print_exc() traceback.print_exc()
# --- Ejecución (sin cambios) --- # --- Ejecución (sin cambios) ---
if __name__ == "__main__": if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal # Imports necesarios solo para la ejecución como script principal
@ -446,7 +579,9 @@ if __name__ == "__main__":
# Verificar si el archivo XML original existe (como referencia, útil para depuración) # Verificar si el archivo XML original existe (como referencia, útil para depuración)
# No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar # No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar
if not os.path.exists(source_xml_file): 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."
)
# No salir necesariamente, pero es bueno saberlo. # No salir necesariamente, pero es bueno saberlo.
# Derivar nombre del archivo JSON de entrada (_simplified.json) # Derivar nombre del archivo JSON de entrada (_simplified.json)
@ -456,15 +591,22 @@ if __name__ == "__main__":
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json") input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
# Determinar el nombre esperado del archivo JSON procesado de salida # 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") 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)}'") )
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 # Verificar si el archivo JSON de entrada (_simplified.json) EXISTE antes de procesar
if not os.path.exists(input_json_file): if not os.path.exists(input_json_file):
print(f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'") print(
print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.") f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'"
)
print(
f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
)
sys.exit(1) # Salir si el archivo necesario no está sys.exit(1) # Salir si el archivo necesario no está
else: else:
# Llamar a la función principal de procesamiento del script # Llamar a la función principal de procesamiento del script
@ -472,7 +614,10 @@ if __name__ == "__main__":
try: try:
process_json_to_scl(input_json_file) process_json_to_scl(input_json_file)
except Exception as e: except Exception as e:
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}") print(
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
)
import traceback import traceback
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla

View File

@ -11,284 +11,475 @@ import traceback # Importar traceback para errores
try: try:
# Intenta importar desde el paquete de procesadores si está estructurado así # Intenta importar desde el paquete de procesadores si está estructurado así
from processors.processor_utils import format_variable_name from processors.processor_utils import format_variable_name
# Definir SCL_SUFFIX aquí o importarlo si está centralizado # Definir SCL_SUFFIX aquí o importarlo si está centralizado
SCL_SUFFIX = "_sympy_processed" # Asegúrate que coincida con x2_process.py 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 GROUPED_COMMENT = (
"// Logic included in grouped IF" # Opcional, si se usa para filtrar
)
except ImportError: except ImportError:
print("Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils.") print(
print("Usando una implementación local básica (¡PUEDE FALLAR CON NOMBRES COMPLEJOS!).") "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) # Implementación local BÁSICA como fallback (MENOS RECOMENDADA)
def format_variable_name(name): def format_variable_name(name):
if not name: return "_INVALID_NAME_" if not name:
if name.startswith('"') and name.endswith('"'): return name # Mantener comillas return "_INVALID_NAME_"
if name.startswith('"') and name.endswith('"'):
return name # Mantener comillas
prefix = "#" if name.startswith("#") else "" prefix = "#" if name.startswith("#") else ""
if prefix: name = name[1:] if prefix:
if name and name[0].isdigit(): name = "_" + name name = name[1:]
if name and name[0].isdigit():
name = "_" + name
name = re.sub(r"[^a-zA-Z0-9_]", "_", name) name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
return prefix + name return prefix + name
SCL_SUFFIX = "_sympy_processed" SCL_SUFFIX = "_sympy_processed"
GROUPED_COMMENT = "// Logic included in grouped IF" GROUPED_COMMENT = "// Logic included in grouped IF"
# --- Función Principal de Generación SCL --- # para formatear valores iniciales
def format_scl_start_value(value, datatype):
"""Formatea un valor para la inicialización SCL según el tipo."""
if value is None:
return None
datatype_lower = datatype.lower() if datatype else ""
value_str = str(value)
if "bool" in datatype_lower:
return "TRUE" if value_str.lower() == "true" else "FALSE"
elif "string" in datatype_lower:
escaped_value = value_str.replace("'", "''")
if escaped_value.startswith("'") and escaped_value.endswith("'"):
escaped_value = escaped_value[1:-1]
return f"'{escaped_value}'"
elif "char" in datatype_lower: # Añadido Char
escaped_value = value_str.replace("'", "''")
if escaped_value.startswith("'") and escaped_value.endswith("'"):
escaped_value = escaped_value[1:-1]
return f"'{escaped_value}'"
elif any(
t in datatype_lower
for t in [
"int",
"byte",
"word",
"dint",
"dword",
"lint",
"lword",
"sint",
"usint",
"uint",
"udint",
"ulint",
]
): # Ampliado
try:
return str(int(value_str))
except ValueError:
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str):
return value_str
return f"'{value_str}'" # O como string si no es entero ni símbolo
elif "real" in datatype_lower or "lreal" in datatype_lower:
try:
f_val = float(value_str)
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):
return value_str
return f"'{value_str}'"
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime
# Quitar T#, LT#, S5T# si existen
prefix = ""
if value_str.upper().startswith("T#"):
prefix = "T#"
value_str = value_str[2:]
elif value_str.upper().startswith("LT#"):
prefix = "LT#"
value_str = value_str[3:]
elif value_str.upper().startswith("S5T#"):
prefix = "S5T#"
value_str = value_str[4:]
# Devolver con el prefijo correcto o T# por defecto si no había
if prefix:
return f"{prefix}{value_str}"
elif "s5time" in datatype_lower:
return f"S5T#{value_str}"
elif "ltime" in datatype_lower:
return f"LT#{value_str}"
else:
return f"T#{value_str}" # Default a TIME
elif "date" in datatype_lower: # Añadido Date, DT, TOD
if value_str.upper().startswith("D#"):
return value_str
elif "dt" in datatype_lower or "date_and_time" in datatype_lower:
if value_str.upper().startswith("DT#"):
return value_str
else:
return f"DT#{value_str}" # Añadir prefijo DT#
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
if value_str.upper().startswith("TOD#"):
return value_str
else:
return f"TOD#{value_str}" # Añadir prefijo TOD#
else:
return f"D#{value_str}" # Default a Date
# Fallback genérico
else:
if re.match(
r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str
): # Permitir más caracteres en símbolos/tipos
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas
if value_str.startswith('"') and value_str.endswith('"'):
return value_str[1:-1]
return value_str
else:
escaped_value = value_str.replace("'", "''")
return f"'{escaped_value}'"
# --- 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")
# Limpiar comillas de tipos de datos UDT ("MyType" -> MyType)
var_dtype = (
var_dtype_raw.strip('"')
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"')
else var_dtype_raw
)
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
# Manejar tipos de datos Array especiales
array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE)
base_type_for_init = var_dtype
declaration_dtype = var_dtype
if array_match:
array_prefix = array_match.group(1)
base_type_raw = array_match.group(2).strip()
# Limpiar comillas del tipo base del array
base_type_for_init = (
base_type_raw.strip('"')
if base_type_raw.startswith('"') and base_type_raw.endswith('"')
else base_type_raw
)
declaration_dtype = (
f'{array_prefix}"{base_type_for_init}"'
if '"' not in base_type_raw
else f"{array_prefix}{base_type_raw}"
) # Reconstruir con comillas si es UDT
# Reconstruir declaración con comillas si es UDT y no array
elif (
not array_match and var_dtype != base_type_for_init
): # Es un tipo que necesita comillas (UDT)
declaration_dtype = f'"{var_dtype}"'
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
init_value = None
# ---- Arrays ----
if array_elements:
# Ordenar índices (asumiendo que son numéricos)
try:
sorted_indices = sorted(array_elements.keys(), key=int)
except ValueError:
sorted_indices = sorted(
array_elements.keys()
) # Fallback a orden alfabético
init_values = [
format_scl_start_value(array_elements[idx], base_type_for_init)
for idx in sorted_indices
]
valid_inits = [v for v in init_values if v is not None]
if valid_inits:
init_value = f"[{', '.join(valid_inits)}]"
# ---- Structs ----
elif children:
# No añadir comentario // Struct aquí, es redundante
scl_lines.append(declaration_line) # Añadir línea de declaración base
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("") # Línea extra
continue # Saltar resto para Struct
# ---- Tipos Simples ----
else:
if start_value is not None:
init_value = format_scl_start_value(start_value, var_dtype)
# Añadir inicialización si existe
if init_value:
declaration_line += f" := {init_value}"
declaration_line += ";"
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): def generate_scl(processed_json_filepath, output_scl_filepath):
"""Genera un archivo SCL a partir del JSON procesado por x2_process (versión SymPy).""" """Genera un archivo SCL a partir del JSON procesado (FC/FB o DB)."""
if not os.path.exists(processed_json_filepath): if not os.path.exists(processed_json_filepath):
print(f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'") print(
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
)
return return
print(f"Cargando JSON procesado desde: {processed_json_filepath}") print(f"Cargando JSON procesado desde: {processed_json_filepath}")
try: try:
with open(processed_json_filepath, 'r', encoding='utf-8') as f: with open(processed_json_filepath, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
except Exception as e: except Exception as e:
print(f"Error al cargar o parsear JSON: {e}") print(f"Error al cargar o parsear JSON: {e}")
traceback.print_exc() traceback.print_exc()
return return
# --- Extracción de Información del Bloque --- # --- Extracción de Información del Bloque (Común) ---
block_name = data.get('block_name', 'UnknownBlock') block_name = data.get("block_name", "UnknownBlock")
block_number = data.get('block_number') block_number = data.get("block_number")
block_lang_original = data.get('language', 'LAD') # Lenguaje original block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks
# Determinar tipo de bloque SCL (Asumir FB si no se especifica) block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
# Idealmente, x1_to_json.py guardaría esto en data['block_type_scl'] = 'FC' o 'FB' block_comment = data.get("block_comment", "")
block_type_scl = data.get('block_type_scl', 'FUNCTION_BLOCK') scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
block_comment = data.get('block_comment', '') print(
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})"
# Usar format_variable_name para el nombre del bloque en SCL )
scl_block_name = format_variable_name(block_name)
print(f"Generando SCL para {block_type_scl}: {scl_block_name} (Original: {block_name})")
# --- Identificación de Variables Temporales y Estáticas ---
# La detección basada en regex sobre el SCL final debería seguir funcionando
temp_vars = set()
stat_vars = set()
# Regex mejorado para capturar variables temporales que empiezan con # o _temp_
# y estáticas (si usas un prefijo como 'stat_' o para bits de memoria de flanco)
temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?') # Captura con o sin #
stat_pattern = re.compile(r'"?(stat_[a-zA-Z0-9_]+)"?') # Para memorias de flanco si usan prefijo 'stat_'
edge_memory_bits = set() # Para detectar bits de memoria de flanco por nombre
for network in data.get('networks', []):
for instruction in network.get('logic', []):
scl_code = instruction.get('scl', '')
# Buscar también en _edge_mem_update_scl si existe
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:
# Buscar #_temp_... o _temp_...
found_temps = temp_pattern.findall(code_to_scan)
for temp_tuple in found_temps:
# findall devuelve tuplas por los grupos de captura, tomar el no vacío
temp_name = next((t for t in temp_tuple if t), None)
if temp_name:
temp_vars.add("#"+temp_name if not temp_name.startswith("#") else temp_name) # Asegurar que empiece con #
# Buscar estáticas (ej: stat_...)
found_stats = stat_pattern.findall(code_to_scan)
stat_vars.update(found_stats)
# Identificar explícitamente bits de memoria usados por PBox/NBox
# Asumiendo que el nombre se guarda en el JSON (requiere ajuste en x1/x2)
# if instruction.get("type","").startswith(("PBox", "NBox")):
# mem_bit_info = instruction.get("inputs", {}).get("bit")
# if mem_bit_info and mem_bit_info.get("type") == "variable":
# edge_memory_bits.add(format_variable_name(mem_bit_info.get("name")))
print(f"Variables temporales (#_temp_...) detectadas: {len(temp_vars)}")
# Si se detectan memorias de flanco, añadirlas a stat_vars si no tienen prefijo 'stat_'
# stat_vars.update(edge_memory_bits - stat_vars) # Añadir solo las nuevas
print(f"Variables estáticas (stat_...) detectadas: {len(stat_vars)}")
# --- Construcción del String SCL ---
scl_output = [] scl_output = []
# Cabecera del Bloque # --- GENERACIÓN PARA DATA BLOCK (DB) ---
if block_lang_original == "DB":
print("Modo de generación: DATA_BLOCK")
scl_output.append(f"// Block Type: {block_type}")
scl_output.append(f"// Block Name (Original): {block_name}") scl_output.append(f"// Block Name (Original): {block_name}")
if block_number: scl_output.append(f"// Block Number: {block_number}") if block_number:
scl_output.append(f"// Original Language: {block_lang_original}") scl_output.append(f"// Block Number: {block_number}")
if block_comment: scl_output.append(f"// Block Comment: {block_comment}") if block_comment:
scl_output.append(f"// Block Comment: {block_comment}")
scl_output.append("") scl_output.append("")
scl_output.append(f"{block_type_scl} \"{scl_block_name}\"") 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", {})
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")
scl_output.append("")
else:
print(
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
)
scl_output.append("VAR")
scl_output.append("END_VAR")
scl_output.append("")
scl_output.append("BEGIN")
scl_output.append("")
scl_output.append("END_DATA_BLOCK")
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) ---
else:
print("Modo de generación: FUNCTION_BLOCK / FUNCTION")
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION"
# 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}")
scl_output.append(f"// Original Language: {block_lang_original}")
if block_comment:
scl_output.append(f"// Block Comment: {block_comment}")
scl_output.append("")
# Manejar tipo de retorno para FUNCTION
return_type = "Void" # Default
interface_data = data.get("interface", {})
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
return_member = interface_data["Return"][
0
] # Asumir un solo valor de retorno
return_type_raw = return_member.get("datatype", "Void")
return_type = (
return_type_raw.strip('"')
if return_type_raw.startswith('"') and return_type_raw.endswith('"')
else return_type_raw
)
# Añadir comillas si es UDT
if return_type != return_type_raw:
return_type = f'"{return_type}"'
scl_output.append(
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
if scl_block_keyword == "FUNCTION"
else f'{scl_block_keyword} "{scl_block_name}"'
)
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
scl_output.append("VERSION : 0.1") scl_output.append("VERSION : 0.1")
scl_output.append("") scl_output.append("")
# Declaraciones de Interfaz (Implementación básica) # Declaraciones de Interfaz FC/FB
interface_sections = ["Input", "Output", "InOut", "Static", "Temp", "Constant", "Return"] section_order = [
interface_data = data.get('interface', {}) "Input",
"Output",
for section_name in interface_sections: "InOut",
scl_section_name = section_name "Static",
# Ajustar nombres de sección para SCL (Static -> STAT, Temp -> TEMP) "Temp",
if section_name == "Static": scl_section_name = "STAT" "Constant",
if section_name == "Temp": scl_section_name = "TEMP" # Usar VAR_TEMP para variables #temp ] # Return ya está en cabecera
vars_in_section = interface_data.get(section_name, [])
# No declarar VAR_TEMP aquí, se hará después con las detectadas/originales
if section_name == "Temp": continue
# No declarar VAR_STAT aquí si ya lo hacemos abajo con las detectadas
if section_name == "Static" and stat_vars: continue
if vars_in_section or (section_name == "Static" and stat_vars): # Incluir STAT si hay detectadas
# Usar VAR para Input/Output/InOut/Constant/Return
var_keyword = "VAR" if section_name != "Static" else "VAR_STAT"
scl_output.append(f"{var_keyword}_{section_name.upper()}")
for var in vars_in_section:
var_name = var.get('name')
var_dtype = var.get('datatype', 'VARIANT') # Default a VARIANT
if var_name:
# Usar format_variable_name CORRECTO
scl_name = format_variable_name(var_name)
scl_output.append(f" {scl_name} : {var_dtype};")
# Declarar stat_vars detectadas si esta es la sección STAT
if section_name == "Static" and stat_vars:
for var_name in sorted(list(stat_vars)):
# Asumir Bool para stat_, podría necesitar inferencia
scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT")
scl_output.append("END_VAR")
scl_output.append("")
# Declaraciones Estáticas (Si no estaban en la interfaz y se detectaron)
# Esto es redundante si la sección VAR_STAT ya se generó arriba
# if stat_vars and not interface_data.get("Static"):
# scl_output.append("VAR_STAT")
# for var_name in sorted(list(stat_vars)):
# scl_output.append(f" {format_variable_name(var_name)} : Bool; // Auto-detected STAT")
# scl_output.append("END_VAR")
# scl_output.append("")
# Declaraciones Temporales (Interfaz Temp + _temp_ detectadas)
scl_output.append("VAR_TEMP")
declared_temps = set() declared_temps = set()
interface_temps = interface_data.get('Temp', []) for section_name in section_order:
if interface_temps: vars_in_section = interface_data.get(section_name, [])
for var in interface_temps: if vars_in_section:
var_name = var.get('name') scl_section_keyword = f"VAR_{section_name.upper()}"
var_dtype = var.get('datatype', 'VARIANT') if section_name == "Static":
if var_name: scl_section_keyword = "VAR_STAT"
scl_name = format_variable_name(var_name) if section_name == "Temp":
scl_output.append(f" {scl_name} : {var_dtype};") scl_section_keyword = "VAR_TEMP"
declared_temps.add(scl_name) # Marcar como declarada if section_name == "Constant":
scl_section_keyword = "CONSTANT"
# Declarar las _temp_ generadas si no estaban ya en la interfaz Temp scl_output.append(scl_section_keyword)
if temp_vars: scl_output.extend(
for var_name in sorted(list(temp_vars)): generate_scl_declarations(vars_in_section, indent_level=1)
scl_name = format_variable_name(var_name) # #_temp_... )
if scl_name not in declared_temps: if section_name == "Temp":
# Inferencia básica de tipo declared_temps.update(
inferred_type = "Bool" # Asumir Bool para la mayoría de temps de lógica format_variable_name(v.get("name"))
# Se podría mejorar si los procesadores añadieran info de tipo for v in vars_in_section
scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary") if v.get("name")
declared_temps.add(scl_name) )
scl_output.append("END_VAR") scl_output.append("END_VAR")
scl_output.append("") scl_output.append("")
# Cuerpo del Bloque # Declaraciones VAR_TEMP adicionales detectadas
temp_vars = set()
temp_pattern = re.compile(
r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?'
)
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_tuple in found_temps:
temp_name = next((t for t in temp_tuple if t), None)
if temp_name:
temp_vars.add(
"#" + temp_name
if not temp_name.startswith("#")
else temp_name
)
additional_temps = sorted(list(temp_vars - declared_temps))
if additional_temps:
if not interface_data.get("Temp"):
scl_output.append("VAR_TEMP")
for var_name in additional_temps:
scl_name = format_variable_name(var_name)
inferred_type = "Bool" # Asumir Bool
scl_output.append(
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
)
if not interface_data.get("Temp"):
scl_output.append("END_VAR")
scl_output.append("")
# Cuerpo del Bloque FC/FB
scl_output.append("BEGIN") scl_output.append("BEGIN")
scl_output.append("") scl_output.append("")
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown)
# Iterar por redes y lógica for i, network in enumerate(data.get("networks", [])):
for i, network in enumerate(data.get('networks', [])): network_title = network.get("title", f'Network {network.get("id")}')
network_title = network.get('title', f'Network {network.get("id")}') network_comment = network.get("comment", "")
network_comment = network.get('comment', '') network_lang = network.get("language", "LAD")
network_lang = network.get('language', 'LAD') # O el lenguaje original scl_output.append(
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
scl_output.append(f" // Network {i+1}: {network_title} (Original Language: {network_lang})") )
if network_comment: if network_comment:
for line in network_comment.splitlines(): for line in network_comment.splitlines():
scl_output.append(f" // {line}") scl_output.append(f" // {line}")
scl_output.append("") scl_output.append("")
network_has_code = False network_has_code = False
# --- NUEVO MANEJO STL con formato Markdown ---
if network_lang == "STL": if network_lang == "STL":
network_has_code = True # Marcar que la red tiene contenido network_has_code = True
if network.get('logic') and isinstance(network['logic'], list) and len(network['logic']) > 0: if (
stl_chunk = network['logic'][0] network.get("logic")
if stl_chunk.get("type") == "RAW_STL_CHUNK" and "stl" in stl_chunk: and network["logic"][0].get("type") == "RAW_STL_CHUNK"
raw_stl_code = stl_chunk["stl"] ):
# Añadir marcador de inicio (como comentario SCL para evitar errores) raw_stl_code = network["logic"][0].get(
scl_output.append(f" {'//'} ```STL") # Doble '//' para asegurar que sea comentario "stl", "// ERROR: STL code missing"
# Escribir el código STL crudo, indentado )
scl_output.append(f" {'//'} ```STL")
for stl_line in raw_stl_code.splitlines(): for stl_line in raw_stl_code.splitlines():
# Añadir indentación estándar de SCL scl_output.append(f" {stl_line}")
scl_output.append(f" {stl_line}") # <-- STL sin comentar
# Añadir marcador de fin (como comentario SCL)
scl_output.append(f" {'//'} ```") scl_output.append(f" {'//'} ```")
else: else:
scl_output.append(" // ERROR: Contenido STL inesperado en JSON.") scl_output.append(" // ERROR: Contenido STL inesperado.")
else: else: # LAD, FBD, SCL, etc.
scl_output.append(" // ERROR: No se encontró lógica STL en JSON para esta red.") for instruction in network.get("logic", []):
scl_output.append("") # Línea en blanco después de la red STL
# --- FIN NUEVO MANEJO STL con formato Markdown ---
else:
# Iterar sobre la 'logica' de la red
for instruction in network.get('logic', []):
instruction_type = instruction.get("type", "") instruction_type = instruction.get("type", "")
scl_code = instruction.get('scl', "") # Obtener SCL generado por x2 scl_code = instruction.get("scl", "")
is_grouped = instruction.get("grouped", False)
# Saltar instrucciones agrupadas if is_grouped:
if instruction.get("grouped", False):
continue continue
if (
# Escribir SCL si es un tipo procesado y tiene código relevante instruction_type.endswith(SCL_SUFFIX)
# (Ignorar comentarios de depuración de SymPy) or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]
if instruction_type.endswith(SCL_SUFFIX) and scl_code: ) and scl_code:
is_internal_sympy_comment_only = scl_code.strip().startswith("// SymPy") or \ is_only_comment = all(
scl_code.strip().startswith("// PBox SymPy processed") or \ line.strip().startswith("//")
scl_code.strip().startswith("// NBox SymPy processed") for line in scl_code.splitlines()
# O podría ser más genérico: ignorar cualquier línea que solo sea comentario SCL if line.strip()
is_only_comment = all(line.strip().startswith("//") for line in scl_code.splitlines()) )
is_if_block = scl_code.strip().startswith("IF")
if not is_only_comment or is_if_block:
# Escribir solo si NO es un comentario interno de SymPy O si es un bloque IF (que sí debe escribirse)
if not is_only_comment or scl_code.strip().startswith("IF"):
network_has_code = True network_has_code = True
for line in scl_code.splitlines(): for line in scl_code.splitlines():
# Añadir indentación estándar
scl_output.append(f" {line}") scl_output.append(f" {line}")
# Incluir también tipos especiales directamente
elif instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] and scl_code:
network_has_code = True
for line in scl_code.splitlines():
scl_output.append(f" {line}") # Indentar
# Podríamos añadir comentarios para errores si se desea
# elif "_error" in instruction_type:
# network_has_code = True
# scl_output.append(f" // ERROR processing instruction UID {instruction.get('instruction_uid')}: {instruction.get('scl', 'No details')}")
if network_has_code: if network_has_code:
scl_output.append("") # Línea en blanco después del código de la red scl_output.append("")
else: else:
scl_output.append(f" // Network did not produce printable SCL code.") scl_output.append(f" // Network did not produce printable SCL code.")
scl_output.append("") scl_output.append("")
# Fin del bloque FC/FB
scl_output.append(f"END_{scl_block_keyword}")
# Fin del bloque # --- Escritura del Archivo SCL (Común) ---
scl_output.append("END_FUNCTION_BLOCK") # O END_FUNCTION si es FC
# --- Escritura del Archivo SCL ---
print(f"Escribiendo archivo SCL en: {output_scl_filepath}") print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
try: try:
with open(output_scl_filepath, 'w', encoding='utf-8') as f: with open(output_scl_filepath, "w", encoding="utf-8") as f:
for line in scl_output: for line in scl_output:
f.write(line + '\n') f.write(line + "\n")
print("Generación de SCL completada.") print("Generación de SCL completada.")
except Exception as e: except Exception as e:
print(f"Error al escribir el archivo SCL: {e}") print(f"Error al escribir el archivo SCL: {e}")
@ -317,7 +508,9 @@ if __name__ == "__main__":
# Verificar si el archivo XML original existe (como referencia) # Verificar si el archivo XML original existe (como referencia)
if not os.path.exists(source_xml_file): 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.") print(
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
)
# No salir necesariamente. # No salir necesariamente.
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL) # Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
@ -325,15 +518,25 @@ if __name__ == "__main__":
# Asumir que los archivos están en el mismo directorio que el XML original # 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 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") input_json_file = os.path.join(
output_scl_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.scl") base_dir, f"{xml_filename_base}_simplified_processed.json"
)
output_scl_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.scl"
)
print(f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'") 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 # Verificar si el archivo JSON procesado de entrada EXISTE
if not os.path.exists(input_json_file): if not os.path.exists(input_json_file):
print(f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'") print(
print(f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.") 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á sys.exit(1) # Salir si el archivo necesario no está
else: else:
# Llamar a la función principal de generación SCL del script # Llamar a la función principal de generación SCL del script
@ -341,7 +544,9 @@ if __name__ == "__main__":
try: try:
generate_scl(input_json_file, output_scl_file) generate_scl(input_json_file, output_scl_file)
except Exception as e: except Exception as e:
print(f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}") print(
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
)
# traceback ya debería estar importado si generate_scl lo necesita # traceback ya debería estar importado si generate_scl lo necesita
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla

BIN
log.txt Normal file

Binary file not shown.

273
paste.py
View File

@ -1,273 +0,0 @@
# x3_generate_scl.py
# -*- coding: utf-8 -*-
import json
import os
import re
import argparse
import sys
import traceback
# --- Importar Utilidades (mantener como estaba) ---
try:
from processors.processor_utils import format_variable_name
SCL_SUFFIX = "_sympy_processed"
GROUPED_COMMENT = "// Logic included in grouped IF"
except ImportError:
print("Advertencia: No se pudo importar 'format_variable_name'. Usando fallback.")
def format_variable_name(name): # Fallback BÁSICO
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
SCL_SUFFIX = "_sympy_processed"
GROUPED_COMMENT = "// Logic included in grouped IF"
# --- NUEVA FUNCIÓN para formatear valores iniciales SCL ---
def format_scl_start_value(value, datatype):
"""Formatea un valor para la inicialización SCL según el tipo."""
if value is None: return None
datatype_lower = datatype.lower() if datatype else ""
value_str = str(value)
if "bool" in datatype_lower:
return "TRUE" if value_str.lower() == 'true' else "FALSE"
elif "string" in datatype_lower:
escaped_value = value_str.replace("'", "''")
if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1]
return f"'{escaped_value}'"
elif "char" in datatype_lower: # Añadido Char
escaped_value = value_str.replace("'", "''")
if escaped_value.startswith("'") and escaped_value.endswith("'"): escaped_value = escaped_value[1:-1]
return f"'{escaped_value}'"
elif any(t in datatype_lower for t in ["int", "byte", "word", "dint", "dword", "lint", "lword", "sint", "usint", "uint", "udint", "ulint"]): # Ampliado
try: return str(int(value_str))
except ValueError:
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str
return f"'{value_str}'" # O como string si no es entero ni símbolo
elif "real" in datatype_lower or "lreal" in datatype_lower:
try:
f_val = float(value_str); 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): return value_str
return f"'{value_str}'"
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime
# Quitar T#, LT#, S5T# si existen
prefix = ""
if value_str.upper().startswith("T#"): prefix="T#"; value_str = value_str[2:]
elif value_str.upper().startswith("LT#"): prefix="LT#"; value_str = value_str[3:]
elif value_str.upper().startswith("S5T#"): prefix="S5T#"; value_str = value_str[4:]
# Devolver con el prefijo correcto o T# por defecto si no había
if prefix: return f"{prefix}{value_str}"
elif "s5time" in datatype_lower: return f"S5T#{value_str}"
elif "ltime" in datatype_lower: return f"LT#{value_str}"
else: return f"T#{value_str}" # Default a TIME
elif "date" in datatype_lower: # Añadido Date, DT, TOD
if value_str.upper().startswith("D#"): return value_str
elif "dt" in datatype_lower or "date_and_time" in datatype_lower:
if value_str.upper().startswith("DT#"): return value_str
else: return f"DT#{value_str}" # Añadir prefijo DT#
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
if value_str.upper().startswith("TOD#"): return value_str
else: return f"TOD#{value_str}" # Añadir prefijo TOD#
else: return f"D#{value_str}" # Default a Date
# Fallback genérico
else:
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str): # Permitir más caracteres en símbolos/tipos
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas
if value_str.startswith('"') and value_str.endswith('"'):
return value_str[1:-1]
return value_str
else:
escaped_value = value_str.replace("'", "''")
return f"'{escaped_value}'"
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
# --- Función Principal de Generación SCL (MODIFICADA) ---
def generate_scl(processed_json_filepath, output_scl_filepath):
"""Genera un archivo SCL a partir del JSON procesado (FC/FB o DB)."""
if not os.path.exists(processed_json_filepath):
print(f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'")
return
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
try:
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
# --- 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') # Será "DB" para Data Blocks
block_type = data.get('block_type', 'Unknown') # FC, FB, GlobalDB
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}, Lang: {block_lang_original})")
scl_output = []
# --- GENERACIÓN PARA DATA BLOCK (DB) ---
if block_lang_original == "DB":
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: scl_output.append(f"// Block Comment: {block_comment}")
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', {})
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")
scl_output.append("")
else:
print("Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB.")
scl_output.append("VAR"); scl_output.append("END_VAR"); scl_output.append("")
scl_output.append("BEGIN"); scl_output.append(""); scl_output.append("END_DATA_BLOCK")
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) ---
else:
print("Modo de generación: FUNCTION_BLOCK / FUNCTION")
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION"
# 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}")
scl_output.append(f"// Original Language: {block_lang_original}")
if block_comment: scl_output.append(f"// Block Comment: {block_comment}")
scl_output.append("")
# Manejar tipo de retorno para FUNCTION
return_type = "Void" # Default
interface_data = data.get('interface', {})
if scl_block_keyword == "FUNCTION" and interface_data.get('Return'):
return_member = interface_data['Return'][0] # Asumir un solo valor de retorno
return_type_raw = return_member.get('datatype', 'Void')
return_type = return_type_raw.strip('"') if return_type_raw.startswith('"') and return_type_raw.endswith('"') else return_type_raw
# Añadir comillas si es UDT
if return_type != return_type_raw: return_type = f'"{return_type}"'
scl_output.append(f"{scl_block_keyword} \"{scl_block_name}\" : {return_type}" if scl_block_keyword == "FUNCTION" else f"{scl_block_keyword} \"{scl_block_name}\"")
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
scl_output.append("VERSION : 0.1"); scl_output.append("")
# Declaraciones de Interfaz FC/FB
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"] # Return ya está en cabecera
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"
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))
if section_name == "Temp": declared_temps.update(format_variable_name(v.get("name")) for v in vars_in_section if v.get("name"))
scl_output.append("END_VAR"); scl_output.append("")
# Declaraciones VAR_TEMP adicionales detectadas
temp_vars = set()
temp_pattern = re.compile(r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?')
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_tuple in found_temps:
temp_name = next((t for t in temp_tuple if t), None)
if temp_name: temp_vars.add("#"+temp_name if not temp_name.startswith("#") else temp_name)
additional_temps = sorted(list(temp_vars - declared_temps))
if additional_temps:
if not interface_data.get("Temp"): scl_output.append("VAR_TEMP")
for var_name in additional_temps:
scl_name = format_variable_name(var_name); inferred_type = "Bool" # Asumir Bool
scl_output.append(f" {scl_name} : {inferred_type}; // Auto-generated temporary")
if not interface_data.get("Temp"): scl_output.append("END_VAR"); scl_output.append("")
# Cuerpo del Bloque FC/FB
scl_output.append("BEGIN"); scl_output.append("")
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown)
for i, network in enumerate(data.get('networks', [])):
network_title = network.get('title', f'Network {network.get("id")}')
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:
for line in network_comment.splitlines(): scl_output.append(f" // {line}")
scl_output.append("")
network_has_code = False
if network_lang == "STL":
network_has_code = True
if network.get('logic') and network['logic'][0].get("type") == "RAW_STL_CHUNK":
raw_stl_code = network['logic'][0].get("stl", "// ERROR: STL code missing")
scl_output.append(f" {'//'} ```STL")
for stl_line in raw_stl_code.splitlines(): scl_output.append(f" {stl_line}")
scl_output.append(f" {'//'} ```")
else: scl_output.append(" // ERROR: Contenido STL inesperado.")
else: # LAD, FBD, SCL, etc.
for instruction in network.get('logic', []):
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"]) 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:
network_has_code = True
for line in scl_code.splitlines(): scl_output.append(f" {line}")
if network_has_code: scl_output.append("")
else: scl_output.append(f" // Network did not produce printable SCL code."); scl_output.append("")
# Fin del bloque FC/FB
scl_output.append(f"END_{scl_block_keyword}")
# --- Escritura del Archivo SCL (Común) ---
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
try:
with open(output_scl_filepath, 'w', encoding='utf-8') as f:
for line in scl_output: f.write(line + '\n')
print("Generación de SCL completada.")
except Exception as e:
print(f"Error al escribir el archivo SCL: {e}"); traceback.print_exc()
# --- Ejecución (sin cambios) ---
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate final SCL file from processed JSON (FC/FB/DB).")
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}', pero se intentará encontrar el JSON procesado.")
# Continuar, pero verificar existencia del JSON procesado
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")
output_scl_file = os.path.join(base_dir, f"{xml_filename_base}_simplified_processed.scl")
print(f"(x3) Generando SCL: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'")
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 'x1_to_json.py' y 'x2_process.py' se ejecutaron correctamente para '{os.path.relpath(source_xml_file)}'.")
sys.exit(1)
else:
try:
generate_scl(input_json_file, output_scl_file)
except Exception as e:
print(f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}")
traceback.print_exc()
sys.exit(1)

View File

@ -3,255 +3,163 @@ import subprocess
import os import os
import sys import sys
import locale import locale
import glob # <--- Importar glob para buscar archivos import glob
# (Función get_console_encoding y variable CONSOLE_ENCODING como antes)
# (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior)
def get_console_encoding(): def get_console_encoding():
"""Obtiene la codificación preferida de la consola, con fallback.""" """Obtiene la codificación preferida de la consola, con fallback."""
try: try:
return locale.getpreferredencoding(False) return locale.getpreferredencoding(False)
except Exception: except Exception:
return "cp1252" # Fallback común en Windows si falla getpreferredencoding
return "cp1252" # O prueba con 'utf-8' si cp1252 da problemas
CONSOLE_ENCODING = get_console_encoding() CONSOLE_ENCODING = get_console_encoding()
# Descomenta la siguiente línea si quieres ver la codificación detectada:
# print(f"Detected console encoding: {CONSOLE_ENCODING}") # print(f"Detected console encoding: {CONSOLE_ENCODING}")
# (Función run_script como antes, usando CONSOLE_ENCODING)
# (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING)
def run_script(script_name, xml_arg): def run_script(script_name, xml_arg):
"""Runs a given script with the specified XML file argument.""" """Runs a given script with the specified XML file argument."""
script_path = os.path.join(os.path.dirname(__file__), script_name) # Asegurarse que la ruta al script sea absoluta o relativa al script actual
command = [sys.executable, script_path, xml_arg] script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name)
# Usar la ruta absoluta al ejecutable de Python actual
python_executable = sys.executable
command = [python_executable, script_path, xml_arg] # Usar la ruta absoluta de python
print(f"\n--- Running {script_name} with argument: {xml_arg} ---") print(f"\n--- Running {script_name} with argument: {xml_arg} ---")
try: try:
# Ejecutar el proceso hijo
result = subprocess.run( result = subprocess.run(
command, command,
check=True, check=True, # Lanza excepción si el script falla (return code != 0)
capture_output=True, capture_output=True,# Captura stdout y stderr
text=True, text=True, # Decodifica stdout/stderr como texto
encoding=CONSOLE_ENCODING, encoding=CONSOLE_ENCODING, # Usa la codificación detectada
errors="replace", errors='replace' # Reemplaza caracteres no decodificables
) # 'replace' para evitar errores )
# Imprimir stdout y stderr si no están vacíos
stdout_clean = result.stdout.strip() if result.stdout else ""
stderr_clean = result.stderr.strip() if result.stderr else ""
# Imprimir stdout y stderr
# Eliminar saltos de línea extra al final si existen
stdout_clean = result.stdout.strip()
stderr_clean = result.stderr.strip()
if stdout_clean: if stdout_clean:
print(stdout_clean) print(stdout_clean)
if stderr_clean: if stderr_clean:
print("--- Stderr ---") # Imprimir stderr claramente para errores del script hijo
print(stderr_clean) print(f"--- Stderr ({script_name}) ---", file=sys.stderr) # Imprimir en stderr
print("--------------") print(stderr_clean, file=sys.stderr)
print("--------------------------", file=sys.stderr)
print(f"--- {script_name} finished successfully ---") print(f"--- {script_name} finished successfully ---")
return True return True # Indicar éxito
except FileNotFoundError: except FileNotFoundError:
print(f"Error: Script '{script_path}' not found.") # Error si el script python o el ejecutable no se encuentran
print(f"Error: Script '{script_path}' or Python executable '{python_executable}' not found.", file=sys.stderr)
return False return False
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error running {script_name}:") # Error si el script hijo devuelve un código de error (ej., sys.exit(1))
print(f"Return code: {e.returncode}") print(f"Error running {script_name}: Script returned non-zero exit code {e.returncode}.", file=sys.stderr)
stdout_decoded = (
e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip() # Decodificar e imprimir stdout/stderr del proceso fallido
if isinstance(e.stdout, bytes) stdout_decoded = e.stdout.strip() if e.stdout else ""
else (e.stdout or "").strip() stderr_decoded = e.stderr.strip() if e.stderr else ""
)
stderr_decoded = (
e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip()
if isinstance(e.stderr, bytes)
else (e.stderr or "").strip()
)
if stdout_decoded: if stdout_decoded:
print("--- Stdout ---") print(f"--- Stdout ({script_name}) ---", file=sys.stderr)
print(stdout_decoded) print(stdout_decoded, file=sys.stderr)
if stderr_decoded: if stderr_decoded:
print("--- Stderr ---") print(f"--- Stderr ({script_name}) ---", file=sys.stderr)
print(stderr_decoded) print(stderr_decoded, file=sys.stderr)
print("--------------") print("--------------------------", file=sys.stderr)
return False return False # Indicar fallo
except Exception as e: except Exception as e:
print(f"An unexpected error occurred while running {script_name}: {e}") # Otros errores inesperados
return False print(f"An unexpected error occurred while running {script_name}: {e}", file=sys.stderr)
# Imprimir traceback para depuración
import traceback
traceback.print_exc(file=sys.stderr)
return False # Indicar fallo
# --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO --- # --- NO SE NECESITA select_xml_file() si procesamos todos ---
def select_xml_file():
"""Busca archivos .xml, los lista y pide al usuario que elija uno."""
print("No XML file specified. Searching for XML files in current directory...")
# Buscar archivos .xml en el directorio actual (.)
xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético
if not xml_files:
print("Error: No .xml files found in the current directory.")
sys.exit(1)
print("\nAvailable XML files:")
for i, filename in enumerate(xml_files, start=1):
print(f" {i}: {filename}")
while True:
try:
choice = input(
f"Enter the number of the file to process (1-{len(xml_files)}): "
)
choice_num = int(choice)
if 1 <= choice_num <= len(xml_files):
selected_file = xml_files[choice_num - 1]
print(f"Selected: {selected_file}")
return selected_file
else:
print("Invalid choice. Please enter a number from the list.")
except ValueError:
print("Invalid input. Please enter a number.")
except EOFError: # Manejar si la entrada se cierra inesperadamente
print("\nSelection cancelled.")
sys.exit(1)
# --- FIN NUEVA FUNCIÓN ---
if __name__ == "__main__": if __name__ == "__main__":
# Imports necesarios para esta sección # --- PARTE 1: BUSCAR ARCHIVOS ---
import os
import sys
import glob # Asegúrate de que glob esté importado al principio del archivo
# Directorio base donde buscar los archivos XML (relativo al script) # Directorio base donde buscar los archivos XML (relativo al script)
base_search_dir = "XML Project" base_search_dir = "XML Project"
script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py # Obtener la ruta absoluta del directorio donde está x0_main.py
script_dir = os.path.dirname(os.path.abspath(__file__))
xml_project_dir = os.path.join(script_dir, base_search_dir) xml_project_dir = os.path.join(script_dir, base_search_dir)
print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'") print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'")
# Verificar si el directorio 'XML Project' existe # Verificar si el directorio 'XML Project' existe
if not os.path.isdir(xml_project_dir): if not os.path.isdir(xml_project_dir):
print( print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.", file=sys.stderr)
f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio." print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.")
) sys.exit(1) # Salir con error
print(
"Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro."
)
sys.exit(1)
# Buscar todos los archivos .xml recursivamente dentro de xml_project_dir # Buscar todos los archivos .xml recursivamente
# Usamos os.path.join para construir la ruta de búsqueda correctamente
# y '**/*.xml' para la recursividad con glob
search_pattern = os.path.join(xml_project_dir, "**", "*.xml") search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
xml_files_found = glob.glob(search_pattern, recursive=True) xml_files_found = glob.glob(search_pattern, recursive=True)
if not xml_files_found: if not xml_files_found:
print( print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.")
f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios."
)
sys.exit(0) # Salir limpiamente si no hay archivos sys.exit(0) # Salir limpiamente si no hay archivos
print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:") print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:")
# Ordenar para un procesamiento predecible (opcional) xml_files_found.sort() # Ordenar para consistencia
xml_files_found.sort()
for xml_file in xml_files_found: for xml_file in xml_files_found:
# Imprimir la ruta relativa desde el directorio del script para claridad
print(f" - {os.path.relpath(xml_file, script_dir)}") print(f" - {os.path.relpath(xml_file, script_dir)}")
# Scripts a ejecutar en secuencia (asegúrate que los nombres son correctos) # --- PARTE 2: PROCESAR CADA ARCHIVO ---
# Scripts a ejecutar en secuencia
script1 = "x1_to_json.py" script1 = "x1_to_json.py"
script2 = "x2_process.py" script2 = "x2_process.py"
script3 = "x3_generate_scl.py" script3 = "x3_generate_scl.py"
# Procesar cada archivo encontrado
processed_count = 0 processed_count = 0
failed_count = 0 failed_count = 0
for xml_filepath in xml_files_found:
print(
f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
# Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio # Procesar cada archivo encontrado en el bucle
for xml_filepath in xml_files_found:
relative_path = os.path.relpath(xml_filepath, script_dir)
print(f"\n--- Iniciando pipeline para: {relative_path} ---")
# Usar la ruta absoluta para los scripts hijos
absolute_xml_filepath = os.path.abspath(xml_filepath) absolute_xml_filepath = os.path.abspath(xml_filepath)
# Ejecutar los scripts en secuencia para el archivo actual # Ejecutar los scripts en secuencia
# La función run_script ya está definida en tu script x0_main.py
success = True success = True
if not run_script(script1, absolute_xml_filepath): if not run_script(script1, absolute_xml_filepath):
print( print(f"\nPipeline falló en el script '{script1}' para el archivo: {relative_path}", file=sys.stderr)
f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
elif not run_script(script2, absolute_xml_filepath): elif not run_script(script2, absolute_xml_filepath):
print( print(f"\nPipeline falló en el script '{script2}' para el archivo: {relative_path}", file=sys.stderr)
f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
elif not run_script(script3, absolute_xml_filepath): elif not run_script(script3, absolute_xml_filepath):
print( print(f"\nPipeline falló en el script '{script3}' para el archivo: {relative_path}", file=sys.stderr)
f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
)
success = False success = False
# Actualizar contadores y mostrar estado
if success: if success:
print( print(f"--- Pipeline completado exitosamente para: {relative_path} ---")
f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
processed_count += 1 processed_count += 1
else: else:
failed_count += 1 failed_count += 1
print( print(f"--- Pipeline falló para: {relative_path} ---", file=sys.stderr) # Indicar fallo
f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---"
)
# --- PARTE 3: RESUMEN FINAL ---
print("\n--- Resumen Final del Procesamiento ---") print("\n--- Resumen Final del Procesamiento ---")
print(f"Total de archivos XML encontrados: {len(xml_files_found)}") print(f"Total de archivos XML encontrados: {len(xml_files_found)}")
print( print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}")
f"Archivos procesados exitosamente por el pipeline completo: {processed_count}"
)
print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}") print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}")
print("---------------------------------------") print("---------------------------------------")
xml_filename = None
# Comprobar si se pasó un argumento de línea de comandos # Salir con código 0 si todo fue bien, 1 si hubo fallos
# sys.argv[0] es el nombre del script, sys.argv[1] sería el primer argumento if failed_count > 0:
if len(sys.argv) > 1:
# Si hay argumentos, usar argparse para parsearlo (permite -h, etc.)
parser = argparse.ArgumentParser(
description="Run the Simatic XML processing pipeline."
)
parser.add_argument(
"xml_file",
# Ya no necesitamos nargs='?' ni default aquí porque sabemos que hay un argumento
help="Path to the XML file to process.",
)
# Parsear solo los argumentos conocidos, ignorar extras si los hubiera
args, unknown = parser.parse_known_args()
xml_filename = args.xml_file
print(f"XML file specified via argument: {xml_filename}")
else:
# Si no hay argumentos, llamar a la función interactiva
xml_filename = select_xml_file()
# --- El resto del script continúa igual, usando xml_filename ---
# Verificar si el archivo XML de entrada (seleccionado o pasado) existe
if not os.path.exists(xml_filename):
print(f"Error: Selected or specified XML file not found: {xml_filename}")
sys.exit(1) sys.exit(1)
print(f"\nStarting pipeline for: {xml_filename}")
# Run scripts sequentially (asegúrate que los nombres son correctos)
script1 = "x1_to_json.py"
script2 = "x2_process.py"
script3 = "x3_generate_scl.py"
if run_script(script1, xml_filename):
if run_script(script2, xml_filename):
if run_script(script3, xml_filename):
print("\nPipeline completed successfully.")
else: else:
print("\nPipeline failed at script:", script3) sys.exit(0)
else:
print("\nPipeline failed at script:", script2) # --- FIN: Se elimina la lógica redundante que venía después del bucle ---
else:
print("\nPipeline failed at script:", script1)

View File

@ -18,6 +18,8 @@ ns = {
# --- Helper Functions --- # --- Helper Functions ---
# ... (El resto de las funciones helper: get_multilingual_text, get_symbol_name, etc. permanecen igual) ...
# --- (Incluye aquí todas tus funciones helper sin cambios) ---
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"): def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
# (Sin cambios respecto a la versión anterior) # (Sin cambios respecto a la versión anterior)
if element is None: if element is None:
@ -247,9 +249,7 @@ def parse_call(call_element):
call_data["instance_scope"] = instance_scope call_data["instance_scope"] = instance_scope
return call_data return call_data
# ... (Incluye aquí las funciones reconstruct_scl_from_tokens, get_access_text, get_comment_text, reconstruct_stl_from_statementlist, parse_interface_members, parse_network SIN CAMBIOS) ...
# SCL (Structured Text) Parser
def reconstruct_scl_from_tokens(st_node): def reconstruct_scl_from_tokens(st_node):
""" """
@ -807,13 +807,18 @@ def parse_network(network_element):
# Buscar FlgNet usando namespace flg # Buscar FlgNet usando namespace flg
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns) flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
if not flgnet_list: if not flgnet_list:
# Intentar buscar directamente si está en la raíz (caso SCL/STL simple?)
# Esta parte puede necesitar ajuste si FlgNet no es el contenedor principal
# para lógica SCL/STL tokenizada.
# Si no hay FlgNet, no podemos parsear lógica LAD/FBD.
# El parseo de SCL/STL se hace en convert_xml_to_json, así que aquí devolvemos error si no hay FlgNet.
return { return {
"id": network_id, "id": network_id,
"title": network_title, "title": network_title,
"comment": network_comment, "comment": network_comment,
"logic": [], "logic": [],
"language": "Unknown", "language": "Unknown", # Podríamos intentar leer el lenguaje aquí también
"error": "FlgNet not found", "error": "FlgNet not found for LAD/FBD parsing",
} }
flgnet = flgnet_list[0] flgnet = flgnet_list[0]
@ -885,6 +890,8 @@ def parse_network(network_element):
# 3. Construcción Lógica Inicial (sin cambios en lógica, pero verificar llamadas) # 3. Construcción Lógica Inicial (sin cambios en lógica, pero verificar llamadas)
all_logic_steps = {} all_logic_steps = {}
# Define SCL_SUFFIX, por ejemplo, "_scl" o "_sympy_processed"
SCL_SUFFIX = "_sympy_processed" # Asegúrate que esto coincida con x2_process.py
functional_block_types = [ functional_block_types = [
"Move", "Move",
"Add", "Add",
@ -1198,6 +1205,7 @@ def parse_network(network_element):
} }
# --- Main Conversion Function ---
def convert_xml_to_json(xml_filepath, json_filepath): def convert_xml_to_json(xml_filepath, json_filepath):
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
if not os.path.exists(xml_filepath): if not os.path.exists(xml_filepath):
@ -1209,9 +1217,11 @@ def convert_xml_to_json(xml_filepath, json_filepath):
tree = etree.parse(xml_filepath, parser) tree = etree.parse(xml_filepath, parser)
root = tree.getroot() root = tree.getroot()
print("Paso 1: Parseo XML completado.") print("Paso 1: Parseo XML completado.")
print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB o SW.Blocks.GlobalDB...")
# --- MODIFICADO: Buscar FC, FB o GlobalDB --- # --- MODIFICADO: Buscar FC, FB, GlobalDB o OB ---
block_list = root.xpath("//*[local-name()='SW.Blocks.FC' or local-name()='SW.Blocks.FB' or local-name()='SW.Blocks.GlobalDB']") print("Paso 2: Buscando el bloque SW.Blocks.FC, SW.Blocks.FB, SW.Blocks.GlobalDB o SW.Blocks.OB...")
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']") # <-- Añadido OB
block_type_found = None block_type_found = None
the_block = None the_block = None
@ -1224,11 +1234,13 @@ def convert_xml_to_json(xml_filepath, json_filepath):
elif block_tag_name == "SW.Blocks.FB": elif block_tag_name == "SW.Blocks.FB":
block_type_found = "FB" block_type_found = "FB"
elif block_tag_name == "SW.Blocks.GlobalDB": elif block_tag_name == "SW.Blocks.GlobalDB":
block_type_found = "GlobalDB" # Identificar el tipo DB block_type_found = "GlobalDB"
elif block_tag_name == "SW.Blocks.OB": # <-- Añadido caso OB
block_type_found = "OB" # <-- Establecer tipo OB
print(f"Paso 2: Bloque {block_tag_name} encontrado (ID={the_block.get('ID')}).") print(f"Paso 2: Bloque {block_tag_name} encontrado (ID={the_block.get('ID')}).")
else: else:
# Mensaje de error más específico y añadimos depuración # Mensaje de error actualizado
print("Error Crítico: No se encontró el elemento raíz del bloque (<SW.Blocks.FC>, <SW.Blocks.FB> o <SW.Blocks.GlobalDB>) usando XPath.") print("Error Crítico: No se encontró el elemento raíz del bloque (<SW.Blocks.FC>, <SW.Blocks.FB>, <SW.Blocks.GlobalDB> o <SW.Blocks.OB>) usando XPath.")
# --- Añadir Debugging --- # --- Añadir Debugging ---
print(f"DEBUG: Tag del elemento raíz del XML: {root.tag}") print(f"DEBUG: Tag del elemento raíz del XML: {root.tag}")
print(f"DEBUG: Primeros hijos del raíz:") print(f"DEBUG: Primeros hijos del raíz:")
@ -1240,6 +1252,7 @@ def convert_xml_to_json(xml_filepath, json_filepath):
break break
# --- Fin Debugging --- # --- Fin Debugging ---
return # Salir si no se encuentra el bloque principal return # Salir si no se encuentra el bloque principal
print("Paso 3: Extrayendo atributos del bloque...") print("Paso 3: Extrayendo atributos del bloque...")
attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']") attribute_list_node = the_block.xpath("./*[local-name()='AttributeList']")
block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown" block_name_val, block_number_val, block_lang_val = "Unknown", None, "Unknown"
@ -1255,7 +1268,9 @@ def convert_xml_to_json(xml_filepath, json_filepath):
lang_node = attr_list.xpath( lang_node = attr_list.xpath(
"./*[local-name()='ProgrammingLanguage']/text()" "./*[local-name()='ProgrammingLanguage']/text()"
) )
block_lang_val = lang_node[0].strip() if lang_node else block_lang_val # Para DBs, el lenguaje principal es 'DB'. Para OBs/FCs/FBs puede ser SCL, STL, LAD, FBD etc.
block_lang_val = lang_node[0].strip() if lang_node else ("DB" if block_type_found == "GlobalDB" else "Unknown")
print( print(
f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje='{block_lang_val}'" f"Paso 3: Atributos: Nombre='{block_name_val}', Número={block_number_val}, Lenguaje='{block_lang_val}'"
) )
@ -1263,6 +1278,10 @@ def convert_xml_to_json(xml_filepath, json_filepath):
print( print(
f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}." f"Advertencia: No se encontró AttributeList para el bloque {block_type_found}."
) )
# Asignar 'DB' como lenguaje si es un GlobalDB y no se encontró explícitamente
if block_type_found == "GlobalDB":
block_lang_val = "DB"
block_comment_val = "" block_comment_val = ""
comment_node_list = the_block.xpath( comment_node_list = the_block.xpath(
"./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']" "./*[local-name()='ObjectList']/*[local-name()='MultilingualText'][@CompositionName='Comment']"
@ -1270,51 +1289,65 @@ def convert_xml_to_json(xml_filepath, json_filepath):
if comment_node_list: if comment_node_list:
block_comment_val = get_multilingual_text(comment_node_list[0]) block_comment_val = get_multilingual_text(comment_node_list[0])
print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'") print(f"Paso 3b: Comentario bloque: '{block_comment_val[:50]}...'")
# --- MODIFICADO: Añadir block_type al resultado ---
result = { result = {
"block_name": block_name_val, "block_name": block_name_val,
"block_number": block_number_val, "block_number": block_number_val,
"language": block_lang_val, "language": block_lang_val, # Lenguaje del bloque (SCL, LAD, DB, etc.)
"block_type": block_type_found, # Tipo de bloque (FC, FB, GlobalDB, OB)
"block_comment": block_comment_val, "block_comment": block_comment_val,
"interface": {}, "interface": {},
"networks": [], "networks": [],
} }
print("Paso 4: Extrayendo la interfaz del bloque...") print("Paso 4: Extrayendo la interfaz del bloque...")
if attribute_list_node: # La estructura de la interfaz suele ser la misma para FC/FB/OB y la sección Static para DB
interface_node_list = attribute_list_node[0].xpath( # Si Interface está dentro de AttributeList
".//*[local-name()='Interface']" interface_node_list = attribute_list_node[0].xpath(".//*[local-name()='Interface']") if attribute_list_node else []
)
if interface_node_list: if interface_node_list:
interface_node = interface_node_list[0] interface_node = interface_node_list[0]
print("Paso 4: Nodo Interface encontrado.") print("Paso 4: Nodo Interface encontrado.")
for section in interface_node.xpath(".//iface:Section", namespaces=ns): # Usar parse_interface_members para extraer todas las secciones
# Esta función ya maneja structs anidados y arrays
all_sections = interface_node.xpath(".//iface:Section", namespaces=ns)
for section in all_sections:
section_name = section.get("Name") section_name = section.get("Name")
if not section_name: if not section_name:
continue continue
members = [] # Obtener los miembros directos de esta sección
for member in section.xpath("./iface:Member", namespaces=ns): # Asegurarse de no obtener miembros de secciones anidadas accidentalmente
member_name = member.get("Name") members_in_section = section.xpath("./iface:Member", namespaces=ns)
member_dtype = member.get("Datatype") if members_in_section:
if member_name and member_dtype: result["interface"][section_name] = parse_interface_members(members_in_section)
members.append(
{"name": member_name, "datatype": member_dtype}
)
if members:
result["interface"][section_name] = members
if not result["interface"]: if not result["interface"]:
print("Advertencia: Interface sin secciones iface:Section válidas.") print("Advertencia: Interface encontrada pero sin secciones iface:Section válidas.")
else: else:
print( # Para GlobalDB, la interfaz podría no estar explícita o ser solo la sección Static
"Advertencia: No se encontró <Interface> dentro de <AttributeList>." if block_type_found == "GlobalDB":
) # Intentar buscar directamente los miembros bajo Sections/Section Name="Static"
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.")
result["interface"]["Static"] = parse_interface_members(static_members)
else:
print("Advertencia: No se encontró sección 'Static' para GlobalDB.")
else: # FC/FB/OB
print(f"Advertencia: No se encontró <Interface> para bloque {block_type_found}.")
if not result["interface"]: if not result["interface"]:
print("Advertencia: No se pudo extraer información de la interfaz.") print("Advertencia: No se pudo extraer información de la interfaz.")
print("Paso 5: Extrayendo y PROCESANDO lógica de redes (CompileUnits)...") print("Paso 5: Extrayendo y PROCESANDO lógica de redes (CompileUnits)...")
networks_processed_count = 0 networks_processed_count = 0
result["networks"] = [] # Initialize networks list here result["networks"] = [] # Initialize networks list here
object_list_node = the_block.xpath("./*[local-name()='ObjectList']") object_list_node = the_block.xpath("./*[local-name()='ObjectList']")
if object_list_node: if object_list_node:
# Buscar CompileUnits (para FC/FB/OB)
compile_units = object_list_node[0].xpath( compile_units = object_list_node[0].xpath(
"./*[local-name()='SW.Blocks.CompileUnit']" "./*[local-name()='SW.Blocks.CompileUnit']"
) )
@ -1443,9 +1476,8 @@ def convert_xml_to_json(xml_filepath, json_filepath):
], ],
} }
elif programming_language in ["LAD", "FBD"]: elif programming_language in ["LAD", "FBD", "GRAPH"]: # GRAPH también usa FlgNet
# Para LAD/FBD, llamar a parse_network (que espera FlgNet dentro de NetworkSource) # Para LAD/FBD/GRAPH, llamar a parse_network
# parse_network ya maneja su propio título/comentario si es necesario, pero podemos pasar los extraídos
# Nota: parse_network espera el *CompileUnit* element, no el NetworkSource # Nota: parse_network espera el *CompileUnit* element, no el NetworkSource
parsed_network_data = parse_network(network_elem) parsed_network_data = parse_network(network_elem)
if parsed_network_data: if parsed_network_data:
@ -1453,19 +1485,42 @@ def convert_xml_to_json(xml_filepath, json_filepath):
programming_language # Asegurar que el lenguaje se guarda programming_language # Asegurar que el lenguaje se guarda
) )
if parsed_network_data.get("error"): if parsed_network_data.get("error"):
# Si parse_network devuelve error (ej. no encontró FlgNet), lo registramos
print( print(
f" Error al parsear red {programming_language} ID={network_id}: {parsed_network_data['error']}" f" Error al parsear red {programming_language} ID={network_id}: {parsed_network_data['error']}"
) )
# parsed_network_data = None # Descomentar para omitir redes con error # Si es un error esperado para este lenguaje (ej. GRAPH sin FlgNet?), creamos placeholder
if "FlgNet not found" in parsed_network_data.get("error", ""):
parsed_network_data = {
"id": network_id,
"title": network_title,
"comment": network_comment,
"language": programming_language,
"logic": [{"instruction_uid": f"PLC_{network_id}", "type": "UNSUPPORTED_CONTENT", "info": f"Contenido {programming_language} sin FlgNet"}],
"error": parsed_network_data.get("error") # Mantener el error original
}
else: else:
print(f" Red {programming_language} ID={network_id} parseada.")
else: # parse_network devolvió None (error interno)
print( print(
f" Error: parse_network devolvió None para red {programming_language} ID={network_id}" f" Error: parse_network devolvió None para red {programming_language} ID={network_id}"
) )
# Crear placeholder de error
parsed_network_data = {
"id": network_id,
"title": network_title,
"comment": network_comment,
"language": programming_language,
"logic": [{"instruction_uid": f"ERR_{network_id}", "type": "PARSING_ERROR", "info": "parse_network returned None"}],
"error": "parse_network returned None"
}
else: else:
# Manejar otros lenguajes o casos inesperados # Manejar otros lenguajes o casos inesperados
print( print(
f" Advertencia: Lenguaje no soportado '{programming_language}' en red ID={network_id}. Creando placeholder." f" Advertencia: Lenguaje no soportado o inesperado '{programming_language}' en red ID={network_id}. Creando placeholder."
) )
parsed_network_data = { parsed_network_data = {
"id": network_id, "id": network_id,
@ -1476,9 +1531,10 @@ def convert_xml_to_json(xml_filepath, json_filepath):
{ {
"instruction_uid": f"UNS_{network_id}", "instruction_uid": f"UNS_{network_id}",
"type": "UNSUPPORTED_LANG", "type": "UNSUPPORTED_LANG",
"scl": f"// Network {network_id} uses unsupported language: {programming_language}\n", "info": f"Network {network_id} uses unsupported language: {programming_language}",
} }
], ],
"error": f"Unsupported language: {programming_language}"
} }
# Añadir la red procesada (si es válida) al resultado # Añadir la red procesada (si es válida) al resultado
@ -1487,17 +1543,21 @@ def convert_xml_to_json(xml_filepath, json_filepath):
# --- Fin del bucle for network_elem --- # --- Fin del bucle for network_elem ---
if networks_processed_count == 0: if networks_processed_count == 0 and block_type_found != "GlobalDB":
print( print(
"Advertencia: ObjectList no contenía elementos SW.Blocks.CompileUnit." f"Advertencia: ObjectList para bloque {block_type_found} no contenía elementos SW.Blocks.CompileUnit."
) )
else: # Para DBs, no esperamos CompileUnits
print("Advertencia: No se encontró ObjectList para el bloque.") elif block_type_found == "GlobalDB":
print("Paso 5: Saltando búsqueda de CompileUnits para GlobalDB.")
else: # No se encontró ObjectList
print(f"Advertencia: No se encontró ObjectList para el bloque {block_type_found}.")
print("Paso 6: Escribiendo el resultado en el archivo JSON...") print("Paso 6: Escribiendo el resultado en el archivo JSON...")
if not result["interface"]: if not result["interface"]:
print("ADVERTENCIA FINAL: 'interface' está vacía.") print("ADVERTENCIA FINAL: 'interface' está vacía.")
if not result["networks"]: if not result["networks"] and block_type_found != "GlobalDB":
print("ADVERTENCIA FINAL: 'networks' está vacía.") print("ADVERTENCIA FINAL: 'networks' está vacía.")
try: try:
with open(json_filepath, "w", encoding="utf-8") as f: with open(json_filepath, "w", encoding="utf-8") as f:
@ -1510,6 +1570,19 @@ def convert_xml_to_json(xml_filepath, json_filepath):
) )
except TypeError as e: except TypeError as e:
print(f"Error Crítico: Problema al serializar a JSON. Error: {e}") print(f"Error Crítico: Problema al serializar a JSON. Error: {e}")
print("--- Datos problemáticos (parcial) ---")
# Intentar imprimir partes del diccionario para depurar
for k, v in result.items():
try:
json.dumps({k: v}) # Intentar serializar cada parte
except TypeError:
print(f"Error serializando clave '{k}': {type(v)}")
if isinstance(v, list) and v:
print(f" Primer elemento tipo: {type(v[0])}")
elif isinstance(v, dict) and v:
print(f" Primeras claves: {list(v.keys())[:5]}")
print("--- Fin Datos problemáticos ---")
except etree.XMLSyntaxError as e: except etree.XMLSyntaxError as e:
print( print(
f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}" f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}"
@ -1529,7 +1602,7 @@ if __name__ == "__main__":
# Configurar ArgumentParser para recibir la ruta del XML obligatoria # Configurar ArgumentParser para recibir la ruta del XML obligatoria
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Convert Simatic XML (LAD/FBD/SCL/STL) to simplified JSON. Expects XML filepath as argument." description="Convert Simatic XML (LAD/FBD/SCL/STL/OB/DB) to simplified JSON. Expects XML filepath as argument." # Actualizada descripción
) )
parser.add_argument( parser.add_argument(
"xml_filepath", # Argumento posicional obligatorio "xml_filepath", # Argumento posicional obligatorio
@ -1557,12 +1630,11 @@ if __name__ == "__main__":
) )
# Llamar a la función principal de conversión del script # Llamar a la función principal de conversión del script
# Asumiendo que tu función principal se llama convert_xml_to_json(input_path, output_path)
try: try:
convert_xml_to_json(xml_input_file, json_output_file) convert_xml_to_json(xml_input_file, json_output_file)
except Exception as e: except Exception as e:
print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}") print(f"Error Crítico (x1) durante la conversión de '{xml_input_file}': {e}")
import traceback import traceback # Asegurarse de que traceback está importado aquí
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla

View File

@ -18,17 +18,14 @@ from processors.processor_utils import (
from processors.symbol_manager import SymbolManager # Import the manager from processors.symbol_manager import SymbolManager # Import the manager
# --- Constantes y Configuración --- # --- Constantes y Configuración ---
# SCL_SUFFIX = "_scl" # Old suffix
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
GROUPED_COMMENT = "// Logic included in grouped IF" GROUPED_COMMENT = "// Logic included in grouped IF"
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
# Global data dictionary (consider passing 'data' as argument if needed elsewhere) # Global data dictionary
# It's currently used by process_group_ifs implicitly via the outer scope,
# which works but passing it explicitly might be cleaner.
data = {} data = {}
# --- (Incluye aquí las funciones process_group_ifs y load_processors SIN CAMBIOS) ---
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data): def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
""" """
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map) Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
@ -112,6 +109,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
# SCoil/RCoil might also be groupable if their SCL is final assignment # SCoil/RCoil might also be groupable if their SCL is final assignment
"SCoil", "SCoil",
"RCoil", "RCoil",
"BLKMOV", # Added BLKMOV
"TON", "TOF", "TP", "Se", "Sd", # Added timers
"CTU", "CTD", "CTUD", # Added counters
] ]
for consumer_instr in network_logic: for consumer_instr in network_logic:
@ -135,26 +135,26 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
is_enabled_by_us = True is_enabled_by_us = True
# Check if consumer is groupable AND has its final SCL generated # Check if consumer is groupable AND has its final SCL generated
# The suffix check needs adjustment based on how terminating processors set it.
# Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix.
if ( if (
is_enabled_by_us is_enabled_by_us
and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix and consumer_type.endswith(SCL_SUFFIX) # Check if processed
and consumer_type_original in groupable_types and consumer_type_original in groupable_types
): ):
consumer_scl = consumer_instr.get("scl", "") consumer_scl = consumer_instr.get("scl", "")
# Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now) # Extract core SCL
core_scl = None core_scl = None
if consumer_scl: if consumer_scl:
# If consumer SCL itself is an IF generated by EN, take the body # If consumer SCL itself is an IF generated by EN, take the body
if consumer_scl.strip().startswith("IF"): if consumer_scl.strip().startswith("IF"):
match = re.search( match = re.search(
r"THEN\s*(.*?)\s*END_IF;", r"IF\s+.*?THEN\s*(.*?)\s*END_IF;", # More robust regex
consumer_scl, consumer_scl,
re.DOTALL | re.IGNORECASE, re.DOTALL | re.IGNORECASE,
) )
core_scl = match.group(1).strip() if match else None core_scl = match.group(1).strip() if match else None
# If body contains another IF, maybe don't group? (optional complexity)
# if core_scl and core_scl.strip().startswith("IF"): core_scl = None
elif not consumer_scl.strip().startswith( elif not consumer_scl.strip().startswith(
"//" "//"
): # Otherwise, take the whole line if not comment ): # Otherwise, take the whole line if not comment
@ -300,8 +300,7 @@ def load_processors(processors_dir="processors"):
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada # Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
return processor_map, processor_list_sorted return processor_map, processor_list_sorted
# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) ---
# --- Bucle Principal de Procesamiento (Modificado para STL) ---
def process_json_to_scl(json_filepath): def process_json_to_scl(json_filepath):
""" """
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB), Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
@ -321,15 +320,14 @@ def process_json_to_scl(json_filepath):
traceback.print_exc() traceback.print_exc()
return return
# --- Obtener lenguaje del bloque principal --- # --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) ---
block_language = data.get("language", "Unknown") block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}")
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}")
# --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO --- # --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO ---
if block_language == "DB": if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
print( print(
"INFO: El bloque es un Data Block (DB). Saltando procesamiento lógico de x2." "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) # Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo)
output_filename = json_filepath.replace( output_filename = json_filepath.replace(
@ -345,8 +343,8 @@ def process_json_to_scl(json_filepath):
traceback.print_exc() traceback.print_exc()
return # <<< SALIR TEMPRANO PARA DBs return # <<< SALIR TEMPRANO PARA DBs
# --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) --- # --- SI NO ES DB (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...") print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...") # <-- Mensaje actualizado
script_dir = os.path.dirname(__file__) script_dir = os.path.dirname(__file__)
processors_dir_path = os.path.join(script_dir, "processors") processors_dir_path = os.path.join(script_dir, "processors")
@ -391,7 +389,7 @@ def process_json_to_scl(json_filepath):
passes = 0 passes = 0
processing_complete = False processing_complete = False
print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---") print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---") # <-- Mensaje actualizado
while passes < max_passes and not processing_complete: while passes < max_passes and not processing_complete:
passes += 1 passes += 1
made_change_in_base_pass = False made_change_in_base_pass = False
@ -408,34 +406,44 @@ def process_json_to_scl(json_filepath):
func_to_call = processor_info["func"] func_to_call = processor_info["func"]
for network in data.get("networks", []): for network in data.get("networks", []):
network_id = network["id"] network_id = network["id"]
network_lang = network.get("language", "LAD") network_lang = network.get("language", "LAD") # Lenguaje de la red
if network_lang == "STL": if network_lang == "STL": # Saltar redes STL
continue # Saltar STL continue
access_map = network_access_maps.get(network_id, {}) access_map = network_access_maps.get(network_id, {})
network_logic = network.get("logic", []) network_logic = network.get("logic", [])
for instruction in network_logic: for instruction in network_logic:
instr_uid = instruction.get("instruction_uid") instr_uid = instruction.get("instruction_uid")
instr_type_original = instruction.get("type", "Unknown") # 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 ( if (
instr_type_original.endswith(SCL_SUFFIX) instr_type_current.endswith(SCL_SUFFIX)
or "_error" in instr_type_original or "_error" in instr_type_current
or instruction.get("grouped", False) or instruction.get("grouped", False)
or instr_type_original or instr_type_current
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]
): ):
continue continue
lookup_key = instr_type_original.lower()
effective_type_name = lookup_key
if instr_type_original == "Call":
block_type = instruction.get("block_type", "").upper()
if block_type == "FC":
effective_type_name = "call_fc"
elif block_type == "FB":
effective_type_name = "call_fb"
# 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
# Si el tipo efectivo coincide con el procesador actual
if effective_type_name == current_type_name: if effective_type_name == current_type_name:
try: try:
# Pasar 'data' a la función del procesador
changed = func_to_call( changed = func_to_call(
instruction, network_id, sympy_map, symbol_manager, data instruction, network_id, sympy_map, symbol_manager, data
) )
@ -444,22 +452,24 @@ def process_json_to_scl(json_filepath):
num_sympy_processed_this_pass += 1 num_sympy_processed_this_pass += 1
except Exception as e: except Exception as e:
print( print(
f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}" f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
) )
traceback.print_exc() traceback.print_exc()
instruction["scl"] = ( instruction["scl"] = (
f"// ERROR en SymPy procesador base: {e}" f"// ERROR en SymPy procesador base: {e}"
) )
instruction["type"] = instr_type_original + "_error" # Añadir sufijo de error al tipo actual
made_change_in_base_pass = True instruction["type"] = instr_type_current + "_error"
made_change_in_base_pass = True # Se hizo un cambio (marcar como error)
print( print(
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy." f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
) )
# --- FASE 2: Agrupación IF (Ignorando STL) --- # --- FASE 2: Agrupación IF (Ignorando STL) ---
if ( if (
made_change_in_base_pass or passes == 1 made_change_in_base_pass or passes == 1
): # Ejecutar siempre en el primer pase ): # Ejecutar siempre en el primer pase o si hubo cambios
print(f" Fase 2 (Agrupación IF con Simplificación):") print(f" Fase 2 (Agrupación IF con Simplificación):")
num_grouped_this_pass = 0 # Resetear contador para el pase num_grouped_this_pass = 0 # Resetear contador para el pase
for network in data.get("networks", []): for network in data.get("networks", []):
@ -468,7 +478,18 @@ def process_json_to_scl(json_filepath):
if network_lang == "STL": if network_lang == "STL":
continue # Saltar STL continue # Saltar STL
network_logic = network.get("logic", []) network_logic = network.get("logic", [])
for instruction in network_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("type", "").endswith(SCL_SUFFIX):
try: try:
group_changed = process_group_ifs( group_changed = process_group_ifs(
instruction, network_id, sympy_map, symbol_manager, data instruction, network_id, sympy_map, symbol_manager, data
@ -503,14 +524,16 @@ def process_json_to_scl(json_filepath):
# --- FIN BUCLE ITERATIVO --- # --- FIN BUCLE ITERATIVO ---
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) --- # --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---") print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
unprocessed_count = 0 unprocessed_count = 0
unprocessed_details = [] unprocessed_details = []
ignored_types = [ ignored_types = [
"raw_scl_chunk", "raw_scl_chunk",
"unsupported_lang", "unsupported_lang",
"raw_stl_chunk", "raw_stl_chunk",
] # Añadido raw_stl_chunk "unsupported_content", # Añadido de x1
"parsing_error", # Añadido de x1
]
for network in data.get("networks", []): for network in data.get("networks", []):
network_id = network.get("id", "Unknown ID") network_id = network.get("id", "Unknown ID")
network_title = network.get("title", f"Network {network_id}") network_title = network.get("title", f"Network {network_id}")
@ -547,7 +570,7 @@ def process_json_to_scl(json_filepath):
output_filename = json_filepath.replace( output_filename = json_filepath.replace(
"_simplified.json", "_simplified_processed.json" "_simplified.json", "_simplified_processed.json"
) )
print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}") print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
try: try:
with open(output_filename, "w", encoding="utf-8") as f: with open(output_filename, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False) json.dump(data, f, indent=4, ensure_ascii=False)
@ -557,7 +580,7 @@ def process_json_to_scl(json_filepath):
traceback.print_exc() traceback.print_exc()
# --- Ejecución (sin cambios) --- # --- Ejecución (sin cambios en esta parte) ---
if __name__ == "__main__": if __name__ == "__main__":
# Imports necesarios solo para la ejecución como script principal # Imports necesarios solo para la ejecución como script principal
import argparse import argparse
@ -577,12 +600,10 @@ if __name__ == "__main__":
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original 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) # Verificar si el archivo XML original existe (como referencia, útil para depuración)
# No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar
if not os.path.exists(source_xml_file): if not os.path.exists(source_xml_file):
print( print(
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente." f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente."
) )
# No salir necesariamente, pero es bueno saberlo.
# Derivar nombre del archivo JSON de entrada (_simplified.json) # Derivar nombre del archivo JSON de entrada (_simplified.json)
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
@ -610,14 +631,13 @@ if __name__ == "__main__":
sys.exit(1) # Salir si el archivo necesario no está sys.exit(1) # Salir si el archivo necesario no está
else: else:
# Llamar a la función principal de procesamiento del script # Llamar a la función principal de procesamiento del script
# Asumiendo que tu función principal se llama process_json_to_scl(input_json_path)
try: try:
process_json_to_scl(input_json_file) process_json_to_scl(input_json_file)
except Exception as e: except Exception as e:
print( print(
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}" f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
) )
import traceback import traceback # Asegurar que traceback está importado
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla

View File

@ -46,24 +46,23 @@ except ImportError:
# para formatear valores iniciales # para formatear valores iniciales
def format_scl_start_value(value, datatype): def format_scl_start_value(value, datatype):
"""Formatea un valor para la inicialización SCL según el tipo.""" """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: if value is None:
return None return None # Retornar None si no hay valor
datatype_lower = datatype.lower() if datatype else "" datatype_lower = datatype.lower() if datatype else ""
value_str = str(value) value_str = str(value)
if "bool" in datatype_lower: # Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE)
return "TRUE" if value_str.lower() == "true" else "FALSE" if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1:
elif "string" in datatype_lower: value_str_unquoted = value_str[1:-1]
escaped_value = value_str.replace("'", "''") elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1:
if escaped_value.startswith("'") and escaped_value.endswith("'"): value_str_unquoted = value_str[1:-1]
escaped_value = escaped_value[1:-1] else:
return f"'{escaped_value}'" value_str_unquoted = value_str
elif "char" in datatype_lower: # Añadido Char
escaped_value = value_str.replace("'", "''") # --- Integer-like types ---
if escaped_value.startswith("'") and escaped_value.endswith("'"): if any(
escaped_value = escaped_value[1:-1]
return f"'{escaped_value}'"
elif any(
t in datatype_lower t in datatype_lower
for t in [ for t in [
"int", "int",
@ -79,72 +78,169 @@ def format_scl_start_value(value, datatype):
"udint", "udint",
"ulint", "ulint",
] ]
): # Ampliado ):
try: try:
return str(int(value_str)) # Intentar convertir el valor (sin comillas) a entero
return str(int(value_str_unquoted))
except ValueError: except ValueError:
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): # Si no es un entero válido, podría ser una constante simbólica
return value_str if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
return f"'{value_str}'" # O como string si no es entero ni símbolo 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: elif "real" in datatype_lower or "lreal" in datatype_lower:
try: try:
f_val = float(value_str) # Intentar convertir a float
f_val = float(value_str_unquoted)
s_val = str(f_val) 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(): if "." not in s_val and "e" not in s_val.lower():
s_val += ".0" s_val += ".0"
return s_val return s_val
except ValueError: except ValueError:
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str): # Podría ser constante simbólica
return value_str if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
return f"'{value_str}'" return value_str_unquoted
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime print(
# Quitar T#, LT#, S5T# si existen 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 = "" prefix = ""
if value_str.upper().startswith("T#"): val_to_use = value_str_unquoted # Usar valor sin comillas
if val_to_use.upper().startswith("T#"):
prefix = "T#" prefix = "T#"
value_str = value_str[2:] val_to_use = val_to_use[2:]
elif value_str.upper().startswith("LT#"): elif val_to_use.upper().startswith("LT#"):
prefix = "LT#" prefix = "LT#"
value_str = value_str[3:] val_to_use = val_to_use[3:]
elif value_str.upper().startswith("S5T#"): elif val_to_use.upper().startswith("S5T#"):
prefix = "S5T#" prefix = "S5T#"
value_str = value_str[4:] val_to_use = val_to_use[4:]
# Devolver con el prefijo correcto o T# por defecto si no había
if prefix: if "s5time" in datatype_lower:
return f"{prefix}{value_str}" return f"S5T#{val_to_use}"
elif "s5time" in datatype_lower:
return f"S5T#{value_str}"
elif "ltime" in datatype_lower: elif "ltime" in datatype_lower:
return f"LT#{value_str}" return f"LT#{val_to_use}"
else: else:
return f"T#{value_str}" # Default a TIME return f"T#{val_to_use}" # Default a TIME
elif "date" in datatype_lower: # Añadido Date, DT, TOD elif "date" in datatype_lower:
if value_str.upper().startswith("D#"): val_to_use = value_str_unquoted
return value_str # Handle DTL first as it's longer
elif "dt" in datatype_lower or "date_and_time" in datatype_lower: if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
if value_str.upper().startswith("DT#"): prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"
return value_str val_to_use = (
else: val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use
return f"DT#{value_str}" # Añadir prefijo DT# )
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: elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
if value_str.upper().startswith("TOD#"): prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"
return value_str val_to_use = (
else: val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use
return f"TOD#{value_str}" # Añadir prefijo TOD# )
else: return f"{prefix}{val_to_use}"
return f"D#{value_str}" # Default a Date else: # Default a Date D#
# Fallback genérico 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: 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( if re.match(
r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str
): # Permitir más caracteres en símbolos/tipos ): # Permitir % para accesos tipo %DB1.DBD0
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas # Quitar comillas externas si es un UDT o struct complejo
if value_str.startswith('"') and value_str.endswith('"'): if (
value_str.startswith('"')
and value_str.endswith('"')
and len(value_str) > 1
):
return value_str[1:-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 return value_str
else: else:
escaped_value = value_str.replace("'", "''") # return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante
return f"'{escaped_value}'" 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) --- # --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
@ -155,87 +251,132 @@ def generate_scl_declarations(variables, indent_level=1):
for var in variables: for var in variables:
var_name_scl = format_variable_name(var.get("name")) var_name_scl = format_variable_name(var.get("name"))
var_dtype_raw = var.get("datatype", "VARIANT") var_dtype_raw = var.get("datatype", "VARIANT")
# Limpiar comillas de tipos de datos UDT ("MyType" -> MyType)
var_dtype = (
var_dtype_raw.strip('"')
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"')
else var_dtype_raw
)
var_comment = var.get("comment") var_comment = var.get("comment")
start_value = var.get("start_value") start_value = var.get("start_value")
children = var.get("children") # Para structs children = var.get("children") # Para structs
array_elements = var.get("array_elements") # Para arrays array_elements = var.get("array_elements") # Para arrays
# Manejar tipos de datos Array especiales # Limpiar comillas del tipo de dato si es UDT/String/etc.
array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE) var_dtype_cleaned = var_dtype_raw
base_type_for_init = var_dtype if isinstance(var_dtype_raw, str):
declaration_dtype = var_dtype if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'):
if array_match: var_dtype_cleaned = var_dtype_raw[1:-1]
array_prefix = array_match.group(1) # Manejar caso 'Array [...] of "MyUDT"'
base_type_raw = array_match.group(2).strip() array_match = re.match(
# Limpiar comillas del tipo base del array r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE
base_type_for_init = (
base_type_raw.strip('"')
if base_type_raw.startswith('"') and base_type_raw.endswith('"')
else base_type_raw
) )
declaration_dtype = ( if array_match:
f'{array_prefix}"{base_type_for_init}"' var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
if '"' not in base_type_raw
else f"{array_prefix}{base_type_raw}"
) # Reconstruir con comillas si es UDT
# Reconstruir declaración con comillas si es UDT y no array # Determinar tipo base para inicialización (importante para arrays)
elif ( base_type_for_init = var_dtype_cleaned
not array_match and var_dtype != base_type_for_init array_prefix_for_decl = ""
): # Es un tipo que necesita comillas (UDT) if var_dtype_cleaned.lower().startswith("array["):
declaration_dtype = f'"{var_dtype}"' 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}" declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
init_value = None init_value_scl = None
# ---- Arrays ---- # ---- Arrays ----
if array_elements: if array_elements:
# Ordenar índices (asumiendo que son numéricos) # Ordenar índices (asumiendo que son numéricos '0', '1', ...)
try: try:
sorted_indices = sorted(array_elements.keys(), key=int) # 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: except ValueError:
sorted_indices = sorted( # Fallback a orden alfabético si los índices no son números
array_elements.keys() print(
) # Fallback a orden alfabético 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 = [ init_values = []
format_scl_start_value(array_elements[idx], base_type_for_init) for idx_str in sorted_indices_str:
for idx in sorted_indices 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] valid_inits = [v for v in init_values if v is not None]
if valid_inits: if valid_inits:
init_value = f"[{', '.join(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 ---- # ---- Structs ----
elif children: elif children:
# No añadir comentario // Struct aquí, es redundante # El valor inicial de un struct se maneja recursivamente dentro
scl_lines.append(declaration_line) # Añadir línea de declaración base # 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") scl_lines.append(f"{indent}STRUCT")
# Llamada recursiva para los miembros internos
scl_lines.extend(generate_scl_declarations(children, indent_level + 1)) scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
scl_lines.append(f"{indent}END_STRUCT;") scl_lines.append(f"{indent}END_STRUCT;")
if var_comment: if var_comment: # Comentario después de END_STRUCT
scl_lines.append(f"{indent}// {var_comment}") scl_lines.append(f"{indent}// {var_comment}")
scl_lines.append("") # Línea extra scl_lines.append("") # Línea extra para legibilidad
continue # Saltar resto para Struct continue # Saltar el resto de la lógica para este struct
# ---- Tipos Simples ---- # ---- Tipos Simples ----
else: else:
if start_value is not None: if start_value is not None:
init_value = format_scl_start_value(start_value, var_dtype) 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}"
# Añadir inicialización si existe
if init_value:
declaration_line += f" := {init_value}"
declaration_line += ";" declaration_line += ";"
# Añadir comentario si existe
if var_comment: if var_comment:
declaration_line += f" // {var_comment}" declaration_line += f" // {var_comment}"
scl_lines.append(declaration_line) scl_lines.append(declaration_line)
return scl_lines return scl_lines
@ -243,7 +384,7 @@ def generate_scl_declarations(variables, indent_level=1):
# --- Función Principal de Generación SCL --- # --- Función Principal de Generación SCL ---
def generate_scl(processed_json_filepath, output_scl_filepath): def generate_scl(processed_json_filepath, output_scl_filepath):
"""Genera un archivo SCL a partir del JSON procesado (FC/FB o DB).""" """Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
if not os.path.exists(processed_json_filepath): if not os.path.exists(processed_json_filepath):
print( print(
@ -263,33 +404,41 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
# --- Extracción de Información del Bloque (Común) --- # --- Extracción de Información del Bloque (Común) ---
block_name = data.get("block_name", "UnknownBlock") block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number") block_number = data.get("block_number")
block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks # block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB block_type = data.get(
"block_type", "Unknown"
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
block_comment = data.get("block_comment", "") block_comment = data.get("block_comment", "")
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
print( print(
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})" f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
) )
scl_output = [] scl_output = []
# --- GENERACIÓN PARA DATA BLOCK (DB) --- # --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
if block_lang_original == "DB": if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
print("Modo de generación: DATA_BLOCK") print("Modo de generación: DATA_BLOCK")
scl_output.append(f"// Block Type: {block_type}") scl_output.append(f"// Block Type: {block_type}")
scl_output.append(f"// Block Name (Original): {block_name}") scl_output.append(f"// Block Name (Original): {block_name}")
if block_number: if block_number:
scl_output.append(f"// Block Number: {block_number}") scl_output.append(f"// Block Number: {block_number}")
if block_comment: if block_comment:
scl_output.append(f"// Block Comment: {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("")
scl_output.append(f'DATA_BLOCK "{scl_block_name}"') scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
scl_output.append("VERSION : 0.1") scl_output.append("VERSION : 0.1")
scl_output.append("") scl_output.append("")
interface_data = data.get("interface", {}) interface_data = data.get("interface", {})
# En DBs, la sección relevante suele ser 'Static'
static_vars = interface_data.get("Static", []) static_vars = interface_data.get("Static", [])
if static_vars: if static_vars:
scl_output.append("VAR") 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.extend(generate_scl_declarations(static_vars, indent_level=1))
scl_output.append("END_VAR") scl_output.append("END_VAR")
scl_output.append("") scl_output.append("")
@ -297,182 +446,288 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
print( print(
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB." "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("VAR")
scl_output.append("END_VAR") scl_output.append("END_VAR")
scl_output.append("") scl_output.append("")
scl_output.append("BEGIN") scl_output.append("BEGIN")
scl_output.append("") scl_output.append(
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
)
scl_output.append("END_DATA_BLOCK") scl_output.append("END_DATA_BLOCK")
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) --- # --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
else: else:
print("Modo de generación: FUNCTION_BLOCK / FUNCTION") # Determinar palabra clave SCL
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION" 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 # Cabecera del Bloque
scl_output.append(f"// Block Type: {block_type}") scl_output.append(f"// Block Type: {block_type}")
scl_output.append(f"// Block Name (Original): {block_name}") scl_output.append(f"// Block Name (Original): {block_name}")
if block_number: if block_number:
scl_output.append(f"// Block Number: {block_number}") scl_output.append(f"// Block Number: {block_number}")
scl_output.append(f"// Original Language: {block_lang_original}") # 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: if block_comment:
scl_output.append(f"// Block Comment: {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("") scl_output.append("")
# Manejar tipo de retorno para FUNCTION
# Manejar tipo de retorno para FUNCTION (FC)
return_type = "Void" # Default return_type = "Void" # Default
interface_data = data.get("interface", {}) interface_data = data.get("interface", {})
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"): if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
return_member = interface_data["Return"][ # Asumir un solo valor de retorno
0 return_member = interface_data["Return"][0]
] # Asumir un solo valor de retorno
return_type_raw = return_member.get("datatype", "Void") return_type_raw = return_member.get("datatype", "Void")
# Limpiar comillas si es UDT/String
return_type = ( return_type = (
return_type_raw.strip('"') return_type_raw[1:-1]
if return_type_raw.startswith('"') and return_type_raw.endswith('"') if isinstance(return_type_raw, str)
and return_type_raw.startswith('"')
and return_type_raw.endswith('"')
else return_type_raw else return_type_raw
) )
# Añadir comillas si es UDT # Añadir comillas si es UDT y no las tenía
if return_type != return_type_raw: if (
return_type != return_type_raw
and not return_type_raw.lower().startswith("array")
):
return_type = f'"{return_type}"' return_type = f'"{return_type}"'
else: # Mantener raw si es tipo básico o ya tenía comillas
return_type = return_type_raw
scl_output.append( # Línea de declaración del bloque
f'{scl_block_keyword} "{scl_block_name}" : {return_type}' if scl_block_keyword == "FUNCTION":
if scl_block_keyword == "FUNCTION" scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
else f'{scl_block_keyword} "{scl_block_name}"' else: # FB y OB
) scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
# Atributos y versión
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
scl_output.append("VERSION : 0.1") scl_output.append("VERSION : 0.1")
scl_output.append("") scl_output.append("")
# Declaraciones de Interfaz FC/FB # Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant)
section_order = [ # Orden estándar SCL
"Input", section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
"Output", declared_temps = set() # Para rastrear temps ya declaradas
"InOut", has_declarations = False
"Static",
"Temp",
"Constant",
] # Return ya está en cabecera
declared_temps = set()
for section_name in section_order: for section_name in section_order:
vars_in_section = interface_data.get(section_name, []) vars_in_section = interface_data.get(section_name, [])
if vars_in_section: 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()}" scl_section_keyword = f"VAR_{section_name.upper()}"
if section_name == "Static": if section_name == "Static":
scl_section_keyword = "VAR_STAT" scl_section_keyword = "VAR_STAT" # Para FBs
if section_name == "Temp": if section_name == "Temp":
scl_section_keyword = "VAR_TEMP" scl_section_keyword = "VAR_TEMP"
if section_name == "Constant": if section_name == "Constant":
scl_section_keyword = "CONSTANT" scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_
scl_output.append(scl_section_keyword) scl_output.append(scl_section_keyword)
# Usar la función recursiva para generar declaraciones
scl_output.extend( scl_output.extend(
generate_scl_declarations(vars_in_section, indent_level=1) 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": if section_name == "Temp":
declared_temps.update( declared_temps.update(
format_variable_name(v.get("name")) format_variable_name(v.get("name"))
for v in vars_in_section for v in vars_in_section
if v.get("name") if v.get("name")
) )
scl_output.append("END_VAR") # Declaraciones VAR_TEMP adicionales (auto-detectadas)
scl_output.append("") # Buscar variables que empiecen con #_temp_ en el SCL generado
temp_vars_detected = set()
# Declaraciones VAR_TEMP adicionales detectadas # Patrón para encontrar #variable o "#variable"
temp_vars = set()
temp_pattern = re.compile( temp_pattern = re.compile(
r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?' r'"?(#\w+)"?'
) ) # Busca # seguido de caracteres alfanuméricos
for network in data.get("networks", []): for network in data.get("networks", []):
for instruction in network.get("logic", []): 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", "") scl_code = instruction.get("scl", "")
edge_update_code = instruction.get("_edge_mem_update_scl", "") edge_update_code = instruction.get(
"_edge_mem_update_scl", ""
) # Para flancos
code_to_scan = ( code_to_scan = (
(scl_code if scl_code else "") (scl_code if scl_code else "")
+ "\n" + "\n"
+ (edge_update_code if edge_update_code else "") + (edge_update_code if edge_update_code else "")
) )
if code_to_scan: if code_to_scan:
# Usar findall para encontrar todas las ocurrencias
found_temps = temp_pattern.findall(code_to_scan) found_temps = temp_pattern.findall(code_to_scan)
for temp_tuple in found_temps: for temp_name in found_temps:
temp_name = next((t for t in temp_tuple if t), None) # findall devuelve el grupo capturado (#...)
if temp_name: if temp_name:
temp_vars.add( temp_vars_detected.add(temp_name)
"#" + temp_name
if not temp_name.startswith("#") # Filtrar las que ya estaban declaradas
else temp_name additional_temps = sorted(list(temp_vars_detected - declared_temps))
)
additional_temps = sorted(list(temp_vars - declared_temps))
if additional_temps: if additional_temps:
if not interface_data.get("Temp"): 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") scl_output.append("VAR_TEMP")
for var_name in additional_temps:
scl_name = format_variable_name(var_name) for temp_name in additional_temps:
inferred_type = "Bool" # Asumir Bool # 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( scl_output.append(
f" {scl_name} : {inferred_type}; // Auto-generated temporary" f" {scl_name} : {inferred_type}; // Auto-generated temporary"
) )
if not interface_data.get("Temp"):
# 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("END_VAR")
scl_output.append("") scl_output.append("")
# Cuerpo del Bloque FC/FB # --- Cuerpo del Bloque (BEGIN...END) ---
scl_output.append("BEGIN") scl_output.append("BEGIN")
scl_output.append("") scl_output.append("")
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown) # Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
for i, network in enumerate(data.get("networks", [])): for i, network in enumerate(data.get("networks", [])):
network_title = network.get("title", f'Network {network.get("id")}') network_title = network.get(
"title", f'Network {network.get("id", i+1)}'
) # Usar i+1 si falta ID
network_comment = network.get("comment", "") network_comment = network.get("comment", "")
network_lang = network.get("language", "LAD") network_lang = network.get("language", "LAD") # Lenguaje original de la red
scl_output.append( scl_output.append(
f" // Network {i+1}: {network_title} (Original Language: {network_lang})" f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
) )
if network_comment: if network_comment:
# Indentar comentarios de red
for line in network_comment.splitlines(): for line in network_comment.splitlines():
scl_output.append(f" // {line}") scl_output.append(f" // {line}")
scl_output.append("") scl_output.append("") # Línea en blanco antes del código de red
network_has_code = False 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": 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 network_has_code = True
if ( raw_stl_code = logic_in_network[0].get(
network.get("logic")
and network["logic"][0].get("type") == "RAW_STL_CHUNK"
):
raw_stl_code = network["logic"][0].get(
"stl", "// ERROR: STL code missing" "stl", "// ERROR: STL code missing"
) )
scl_output.append(f" {'//'} ```STL") # 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(): for stl_line in raw_stl_code.splitlines():
scl_output.append(f" {stl_line}") scl_output.append(f" // {stl_line}")
scl_output.append(f" {'//'} ```") scl_output.append(f" // --- END STL Network {i+1} ---")
scl_output.append("") # Línea en blanco después
else: else:
scl_output.append(" // ERROR: Contenido STL inesperado.") scl_output.append(
else: # LAD, FBD, SCL, etc. f" // ERROR: Contenido STL inesperado en Network {i+1}."
for instruction in network.get("logic", []): )
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", "") instruction_type = instruction.get("type", "")
scl_code = instruction.get("scl", "") scl_code = instruction.get("scl", "")
is_grouped = instruction.get("grouped", False) is_grouped = instruction.get("grouped", False)
# Saltar instrucciones agrupadas (su lógica está en el IF)
if is_grouped: if is_grouped:
continue continue
# Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder
if ( if (
instruction_type.endswith(SCL_SUFFIX) instruction_type.endswith(SCL_SUFFIX)
or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"] or instruction_type
in [
"RAW_SCL_CHUNK",
"UNSUPPORTED_LANG",
"UNSUPPORTED_CONTENT",
"PARSING_ERROR",
]
or "_error" in instruction_type # Incluir errores comentados
) and scl_code: ) and scl_code:
# Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF)
is_only_comment = all( is_only_comment = all(
line.strip().startswith("//") line.strip().startswith("//")
for line in scl_code.splitlines() for line in scl_code.splitlines()
if line.strip() if line.strip()
) )
is_if_block = scl_code.strip().startswith("IF") is_if_block = scl_code.strip().startswith("IF")
if not is_only_comment or is_if_block:
# 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 network_has_code = True
for line in scl_code.splitlines(): for line in scl_code.splitlines():
scl_output.append(f" {line}") scl_output.append(f" {line}") # Indentar código
if network_has_code: # Añadir línea en blanco después de cada bloque SCL para legibilidad
scl_output.append("") scl_output.append("")
else:
scl_output.append(f" // Network did not produce printable SCL code.") # 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("") scl_output.append("")
# Fin del bloque FC/FB
scl_output.append(f"END_{scl_block_keyword}") # Fin del bloque FC/FB/OB
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
# --- Escritura del Archivo SCL (Común) --- # --- Escritura del Archivo SCL (Común) ---
print(f"Escribiendo archivo SCL en: {output_scl_filepath}") print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
@ -492,7 +747,7 @@ if __name__ == "__main__":
import argparse import argparse
import os import os
import sys import sys
import traceback # Asegurarse que traceback está importado si se usa en generate_scl import traceback # Asegurarse que traceback está importado
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria # Configurar ArgumentParser para recibir la ruta del XML original obligatoria
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -511,7 +766,6 @@ if __name__ == "__main__":
print( print(
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado." f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
) )
# No salir necesariamente.
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL) # Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
@ -521,8 +775,9 @@ if __name__ == "__main__":
input_json_file = os.path.join( input_json_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.json" base_dir, f"{xml_filename_base}_simplified_processed.json"
) )
# Cambiar extensión de salida a .scl
output_scl_file = os.path.join( output_scl_file = os.path.join(
base_dir, f"{xml_filename_base}_simplified_processed.scl" base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
) )
print( print(
@ -540,13 +795,13 @@ if __name__ == "__main__":
sys.exit(1) # Salir si el archivo necesario no está sys.exit(1) # Salir si el archivo necesario no está
else: else:
# Llamar a la función principal de generación SCL del script # Llamar a la función principal de generación SCL del script
# Asumiendo que tu función principal se llama generate_scl(input_json_path, output_scl_path)
try: try:
generate_scl(input_json_file, output_scl_file) generate_scl(input_json_file, output_scl_file)
sys.exit(0) # Salir con éxito explícitamente
except Exception as e: except Exception as e:
print( print(
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}" f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
) )
# traceback ya debería estar importado si generate_scl lo necesita # traceback ya debería estar importado
traceback.print_exc() traceback.print_exc()
sys.exit(1) # Salir con error si la función principal falla sys.exit(1) # Salir con error si la función principal falla