diff --git a/ToUpload/generators/__init__.py b/ToUpload/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ToUpload/generators/generate_md_tag_table.py b/ToUpload/generators/generate_md_tag_table.py new file mode 100644 index 0000000..5106d21 --- /dev/null +++ b/ToUpload/generators/generate_md_tag_table.py @@ -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 \ No newline at end of file diff --git a/ToUpload/generators/generate_md_udt.py b/ToUpload/generators/generate_md_udt.py new file mode 100644 index 0000000..55c1a88 --- /dev/null +++ b/ToUpload/generators/generate_md_udt.py @@ -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 \ No newline at end of file diff --git a/ToUpload/generators/generate_scl_code_block.py b/ToUpload/generators/generate_scl_code_block.py new file mode 100644 index 0000000..a942c99 --- /dev/null +++ b/ToUpload/generators/generate_scl_code_block.py @@ -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 \ No newline at end of file diff --git a/ToUpload/generators/generate_scl_db.py b/ToUpload/generators/generate_scl_db.py new file mode 100644 index 0000000..0f9aca0 --- /dev/null +++ b/ToUpload/generators/generate_scl_db.py @@ -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 \ No newline at end of file diff --git a/ToUpload/generators/generator_utils.py b/ToUpload/generators/generator_utils.py new file mode 100644 index 0000000..7355f1c --- /dev/null +++ b/ToUpload/generators/generator_utils.py @@ -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 \ No newline at end of file diff --git a/ToUpload/parsers/parser_utils.py b/ToUpload/parsers/parser_utils.py index 64d748d..88b2fb0 100644 --- a/ToUpload/parsers/parser_utils.py +++ b/ToUpload/parsers/parser_utils.py @@ -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 .""" - # 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 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 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 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 . ¿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 mismo + call_scope = call_element.get("Scope") if call_scope == "LocalVariable": - # Si la llamada es local y no tiene , 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 .") - # 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 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 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 diff --git a/ToUpload/x0_main.py b/ToUpload/x0_main.py index d8f081b..322bd7a 100644 --- a/ToUpload/x0_main.py +++ b/ToUpload/x0_main.py @@ -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 diff --git a/ToUpload/x1_to_json.py b/ToUpload/x1_to_json.py index 0a52174..b909a01 100644 --- a/ToUpload/x1_to_json.py +++ b/ToUpload/x1_to_json.py @@ -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 (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_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 .""" + 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 ()." - ) - # 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 () 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 ." - ) - - if not result["interface"]: - print( - "Advertencia: Interface encontrada pero sin secciones procesables." - ) - else: - # Manejo especial para DB si no hay 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ó 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 .") + 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ó 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 \ No newline at end of file diff --git a/ToUpload/x2_process.py b/ToUpload/x2_process.py index 748b417..f3d5329 100644 --- a/ToUpload/x2_process.py +++ b/ToUpload/x2_process.py @@ -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 \ No newline at end of file + sys.exit(1) \ No newline at end of file diff --git a/ToUpload/x3_generate_scl.py b/ToUpload/x3_generate_scl.py index e20b9d2..4f5c2b1 100644 --- a/ToUpload/x3_generate_scl.py +++ b/ToUpload/x3_generate_scl.py @@ -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) \ No newline at end of file diff --git a/generators/__init__.py b/generators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/generators/generate_md_tag_table.py b/generators/generate_md_tag_table.py new file mode 100644 index 0000000..5106d21 --- /dev/null +++ b/generators/generate_md_tag_table.py @@ -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 \ No newline at end of file diff --git a/generators/generate_md_udt.py b/generators/generate_md_udt.py new file mode 100644 index 0000000..55c1a88 --- /dev/null +++ b/generators/generate_md_udt.py @@ -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 \ No newline at end of file diff --git a/generators/generate_scl_code_block.py b/generators/generate_scl_code_block.py new file mode 100644 index 0000000..a942c99 --- /dev/null +++ b/generators/generate_scl_code_block.py @@ -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 \ No newline at end of file diff --git a/generators/generate_scl_db.py b/generators/generate_scl_db.py new file mode 100644 index 0000000..0f9aca0 --- /dev/null +++ b/generators/generate_scl_db.py @@ -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 \ No newline at end of file diff --git a/generators/generator_utils.py b/generators/generator_utils.py new file mode 100644 index 0000000..7355f1c --- /dev/null +++ b/generators/generator_utils.py @@ -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 \ No newline at end of file diff --git a/x0_main.py b/x0_main.py index d8f081b..322bd7a 100644 --- a/x0_main.py +++ b/x0_main.py @@ -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 diff --git a/x1_to_json.py b/x1_to_json.py index 2e9f4ca..b909a01 100644 --- a/x1_to_json.py +++ b/x1_to_json.py @@ -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)}'") diff --git a/x2_process.py b/x2_process.py index 19d5202..f3d5329 100644 --- a/x2_process.py +++ b/x2_process.py @@ -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) \ No newline at end of file diff --git a/x3_generate_scl.py b/x3_generate_scl.py index 5c3290a..4f5c2b1 100644 --- a/x3_generate_scl.py +++ b/x3_generate_scl.py @@ -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) \ No newline at end of file