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