Funciona con UDT y Tags
This commit is contained in:
parent
0e68e32b8a
commit
546705f8ca
|
@ -0,0 +1,548 @@
|
|||
# ToUpload/parsers/parse_lad_fbd.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import (
|
||||
ns,
|
||||
parse_access,
|
||||
parse_part,
|
||||
parse_call,
|
||||
get_multilingual_text,
|
||||
)
|
||||
|
||||
# Sufijo usado en x2 para identificar instrucciones procesadas (útil para EN/ENO)
|
||||
SCL_SUFFIX = "_sympy_processed" # Asumimos que este es el sufijo de x2
|
||||
|
||||
|
||||
def parse_lad_fbd_network(network_element):
|
||||
"""
|
||||
Parsea una red LAD/FBD/GRAPH, extrae lógica y añade conexiones EN/ENO implícitas.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
if network_element is None:
|
||||
return {
|
||||
"id": "ERROR",
|
||||
"title": "Invalid Network Element",
|
||||
"logic": [],
|
||||
"error": "Input element was None",
|
||||
}
|
||||
|
||||
network_id = network_element.get("ID")
|
||||
# Usar get_multilingual_text de utils
|
||||
title_element = network_element.xpath(
|
||||
".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
|
||||
)
|
||||
network_title = (
|
||||
get_multilingual_text(title_element[0])
|
||||
if title_element
|
||||
else f"Network {network_id}"
|
||||
)
|
||||
comment_element = network_element.xpath(
|
||||
"./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # OJO: Path relativo a CompileUnit?
|
||||
if not comment_element: # Intentar path alternativo si el anterior falla
|
||||
comment_element = network_element.xpath(
|
||||
".//MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # Más genérico dentro de la red
|
||||
network_comment = (
|
||||
get_multilingual_text(comment_element[0]) if comment_element else ""
|
||||
)
|
||||
|
||||
# --- Determinar Lenguaje (ya que este parser maneja varios) ---
|
||||
network_lang = "Unknown"
|
||||
attr_list_net = network_element.xpath("./AttributeList")
|
||||
if attr_list_net:
|
||||
lang_node_net = attr_list_net[0].xpath("./ProgrammingLanguage/text()")
|
||||
if lang_node_net:
|
||||
network_lang = lang_node_net[0].strip()
|
||||
|
||||
# --- Buscar FlgNet ---
|
||||
# Buscar NetworkSource y luego FlgNet (ambos usan namespace flg)
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
flgnet = None
|
||||
if network_source_node:
|
||||
flgnet_list = network_source_node[0].xpath("./flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
else: # Intentar buscar FlgNet directamente si no hay NetworkSource
|
||||
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
|
||||
if flgnet is None:
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang,
|
||||
"logic": [],
|
||||
"error": "FlgNet not found inside NetworkSource or CompileUnit",
|
||||
}
|
||||
|
||||
# 1. Parse Access, Parts, Calls (usan utils)
|
||||
access_map = {}
|
||||
# Corregir XPath para buscar Access dentro de FlgNet/Parts
|
||||
for acc in flgnet.xpath(".//flg:Parts/flg:Access", namespaces=ns):
|
||||
acc_info = parse_access(acc)
|
||||
if acc_info and acc_info.get("uid") and "error" not in acc_info.get("type", ""):
|
||||
access_map[acc_info["uid"]] = acc_info
|
||||
elif acc_info:
|
||||
print(
|
||||
f"Advertencia: Ignorando Access inválido o con error UID={acc_info.get('uid')} en red {network_id}"
|
||||
)
|
||||
|
||||
parts_and_calls_map = {}
|
||||
# Corregir XPath para buscar Part y Call dentro de FlgNet/Parts
|
||||
instruction_elements = flgnet.xpath(
|
||||
".//flg:Parts/flg:Part | .//flg:Parts/flg:Call", namespaces=ns
|
||||
)
|
||||
for element in instruction_elements:
|
||||
parsed_info = None
|
||||
tag_name = etree.QName(element.tag).localname
|
||||
if tag_name == "Part":
|
||||
parsed_info = parse_part(element) # Usa utils
|
||||
elif tag_name == "Call":
|
||||
parsed_info = parse_call(element) # Usa utils
|
||||
|
||||
if (
|
||||
parsed_info
|
||||
and parsed_info.get("uid")
|
||||
and "error" not in parsed_info.get("type", "")
|
||||
):
|
||||
parts_and_calls_map[parsed_info["uid"]] = parsed_info
|
||||
elif parsed_info:
|
||||
# Si parse_call/parse_part devolvió error, lo guardamos para tener el UID
|
||||
print(
|
||||
f"Advertencia: {tag_name} con error UID={parsed_info.get('uid')} en red {network_id}. Error: {parsed_info.get('error')}"
|
||||
)
|
||||
parts_and_calls_map[parsed_info["uid"]] = (
|
||||
parsed_info # Guardar aunque tenga error
|
||||
)
|
||||
|
||||
# 2. Parse Wires (lógica compleja, mantener aquí)
|
||||
wire_connections = defaultdict(list) # destination -> [source1, source2]
|
||||
source_connections = defaultdict(list) # source -> [dest1, dest2]
|
||||
eno_outputs = defaultdict(list)
|
||||
qname_powerrail = etree.QName(ns["flg"], "Powerrail")
|
||||
qname_identcon = etree.QName(
|
||||
ns["flg"], "IdentCon"
|
||||
) # Conexión a/desde Access (variable/constante)
|
||||
qname_namecon = etree.QName(
|
||||
ns["flg"], "NameCon"
|
||||
) # Conexión a/desde Part/Call (pin con nombre)
|
||||
qname_openbranch = etree.QName(
|
||||
ns["flg"], "Openbranch"
|
||||
) # Rama abierta (normalmente ignorada o tratada como TRUE?)
|
||||
qname_opencon = etree.QName(
|
||||
ns["flg"], "OpenCon"
|
||||
) # Conexión abierta (pin no conectado)
|
||||
|
||||
# Corregir XPath para buscar Wire dentro de FlgNet/Wires
|
||||
for wire in flgnet.xpath(".//flg:Wires/flg:Wire", namespaces=ns):
|
||||
children = wire.getchildren()
|
||||
if len(children) < 2:
|
||||
continue # Necesita al menos origen y destino
|
||||
|
||||
source_elem = children[0]
|
||||
source_uid, source_pin = None, None
|
||||
|
||||
# Determinar origen
|
||||
if source_elem.tag == qname_powerrail:
|
||||
source_uid, source_pin = "POWERRAIL", "out"
|
||||
elif source_elem.tag == qname_identcon: # Origen es una variable/constante
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = "value" # Salida implícita de un Access
|
||||
elif source_elem.tag == qname_namecon: # Origen es pin de instrucción
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = source_elem.get("Name")
|
||||
elif source_elem.tag == qname_openbranch:
|
||||
# ¿Cómo manejar OpenBranch como fuente? Podría ser TRUE o una condición OR implícita
|
||||
source_uid = "OPENBRANCH_" + wire.get(
|
||||
"UId", "Unknown"
|
||||
) # UID único para la rama
|
||||
source_pin = "out"
|
||||
print(
|
||||
f"Advertencia: OpenBranch encontrado como fuente en Wire UID={wire.get('UId')} (Red {network_id}). Tratando como fuente especial."
|
||||
)
|
||||
# No lo añadimos a parts_and_calls_map, get_sympy_representation necesitará manejarlo
|
||||
# Ignorar OpenCon como fuente (no tiene sentido)
|
||||
if source_uid is None or source_pin is None:
|
||||
# print(f"Advertencia: Fuente de wire inválida o no soportada: {source_elem.tag} en Wire UID={wire.get('UId')}")
|
||||
continue
|
||||
|
||||
source_info = (source_uid, source_pin)
|
||||
|
||||
# Procesar destinos
|
||||
for dest_elem in children[1:]:
|
||||
dest_uid, dest_pin = None, None
|
||||
|
||||
if (
|
||||
dest_elem.tag == qname_identcon
|
||||
): # Destino es una variable/constante (asignación)
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = "value" # Entrada implícita de un Access
|
||||
elif dest_elem.tag == qname_namecon: # Destino es pin de instrucción
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = dest_elem.get("Name")
|
||||
# Ignorar Powerrail, OpenBranch, OpenCon como destinos válidos de conexión lógica principal
|
||||
|
||||
if dest_uid is not None and dest_pin is not None:
|
||||
dest_key = (dest_uid, dest_pin)
|
||||
if source_info not in wire_connections[dest_key]:
|
||||
wire_connections[dest_key].append(source_info)
|
||||
|
||||
# Mapa inverso: source -> list of destinations
|
||||
source_key = (source_uid, source_pin)
|
||||
dest_info = (dest_uid, dest_pin)
|
||||
if dest_info not in source_connections[source_key]:
|
||||
source_connections[source_key].append(dest_info)
|
||||
|
||||
# Trackear salidas ENO específicamente si la fuente es una instrucción
|
||||
if source_pin == "eno" and source_uid in parts_and_calls_map:
|
||||
if dest_info not in eno_outputs[source_uid]:
|
||||
eno_outputs[source_uid].append(dest_info)
|
||||
|
||||
# 3. Build Initial Logic Structure (incorporando errores)
|
||||
all_logic_steps = {}
|
||||
# Lista de tipos funcionales (usados para inferencia EN)
|
||||
# Estos son los tipos *originales* de las instrucciones
|
||||
functional_block_types = [
|
||||
"Move",
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Convert",
|
||||
"Call", # Call ya está aquí
|
||||
"TON",
|
||||
"TOF",
|
||||
"TP",
|
||||
"CTU",
|
||||
"CTD",
|
||||
"CTUD",
|
||||
"BLKMOV", # Añadidos
|
||||
"Se",
|
||||
"Sd", # Estos son tipos LAD que se mapearán a timers SCL
|
||||
]
|
||||
# Lista de generadores RLO (usados para inferencia EN)
|
||||
rlo_generators = [
|
||||
"Contact",
|
||||
"O",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
"And",
|
||||
"Xor",
|
||||
"PBox",
|
||||
"NBox",
|
||||
"Not",
|
||||
]
|
||||
|
||||
# Iterar sobre UIDs válidos (los que se pudieron parsear, aunque sea con error)
|
||||
valid_instruction_uids = list(parts_and_calls_map.keys())
|
||||
|
||||
for instruction_uid in valid_instruction_uids:
|
||||
instruction_info = parts_and_calls_map[instruction_uid]
|
||||
# Hacer copia profunda para no modificar el mapa original
|
||||
instruction_repr = copy.deepcopy(instruction_info)
|
||||
instruction_repr["instruction_uid"] = instruction_uid # Asegurar UID
|
||||
instruction_repr["inputs"] = {}
|
||||
instruction_repr["outputs"] = {}
|
||||
|
||||
# Si la instrucción ya tuvo un error de parseo, añadirlo aquí
|
||||
if "error" in instruction_info:
|
||||
instruction_repr["parsing_error"] = instruction_info["error"]
|
||||
# No intentar poblar inputs/outputs si el parseo base falló
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
continue
|
||||
|
||||
original_type = instruction_repr.get("type", "") # Tipo de la instrucción
|
||||
|
||||
# --- Poblar Entradas ---
|
||||
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
|
||||
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
|
||||
# Añadir pines dinámicamente basados en el tipo de instrucción
|
||||
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
|
||||
possible_input_pins.add("operand")
|
||||
elif original_type in [
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
]:
|
||||
possible_input_pins.update(["in1", "in2"])
|
||||
elif original_type in ["TON", "TOF", "TP"]:
|
||||
possible_input_pins.update(["IN", "PT"]) # Pines SCL
|
||||
elif original_type in ["Se", "Sd"]:
|
||||
possible_input_pins.update(["s", "tv", "timer"]) # Pines LAD
|
||||
elif original_type in ["CTU", "CTD", "CTUD"]:
|
||||
possible_input_pins.update(["CU", "CD", "R", "LD", "PV"]) # Pines SCL/LAD
|
||||
elif original_type in ["PBox", "NBox"]:
|
||||
possible_input_pins.update(
|
||||
["bit", "clk", "in"]
|
||||
) # PBox/NBox usa 'in' y 'bit'
|
||||
elif original_type == "BLKMOV":
|
||||
possible_input_pins.add("SRCBLK")
|
||||
elif original_type == "Move":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Convert":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Call":
|
||||
# Para Calls, los nombres de los parámetros reales se definen en el XML
|
||||
# El Xpath busca Parameter DENTRO de CallInfo, que está DENTRO de Call
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_xml_element = call_xml_element_list[0]
|
||||
call_info_node_list = call_xml_element.xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
call_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter/@Name", namespaces=ns
|
||||
)
|
||||
possible_input_pins.update(call_param_names)
|
||||
# print(f"DEBUG Call UID={instruction_uid}: Params={call_param_names}")
|
||||
else: # Fallback si no hay namespace (menos probable)
|
||||
call_info_node_list_no_ns = call_xml_element.xpath("./CallInfo")
|
||||
if call_info_node_list_no_ns:
|
||||
possible_input_pins.update(
|
||||
call_info_node_list_no_ns[0].xpath("./Parameter/@Name")
|
||||
)
|
||||
|
||||
# Iterar sobre pines posibles y buscar conexiones
|
||||
for pin_name in possible_input_pins:
|
||||
dest_key = (instruction_uid, pin_name)
|
||||
if dest_key in wire_connections:
|
||||
sources_list = wire_connections[dest_key]
|
||||
input_sources_repr = []
|
||||
for source_uid, source_pin in sources_list:
|
||||
source_repr = None
|
||||
if source_uid == "POWERRAIL":
|
||||
source_repr = {"type": "powerrail"}
|
||||
elif source_uid.startswith("OPENBRANCH_"):
|
||||
source_repr = {
|
||||
"type": "openbranch",
|
||||
"uid": source_uid,
|
||||
} # Fuente especial
|
||||
elif source_uid in access_map:
|
||||
source_repr = copy.deepcopy(access_map[source_uid])
|
||||
elif source_uid in parts_and_calls_map:
|
||||
source_instr_info = parts_and_calls_map[source_uid]
|
||||
source_repr = {
|
||||
"type": "connection",
|
||||
"source_instruction_type": source_instr_info.get(
|
||||
"type", "Unknown"
|
||||
), # Usar tipo base
|
||||
"source_instruction_uid": source_uid,
|
||||
"source_pin": source_pin,
|
||||
}
|
||||
else:
|
||||
# Fuente desconocida (ni Access, ni Part/Call válido)
|
||||
print(
|
||||
f"Advertencia: Fuente desconocida UID={source_uid} conectada a {instruction_uid}.{pin_name}"
|
||||
)
|
||||
source_repr = {"type": "unknown_source", "uid": source_uid}
|
||||
input_sources_repr.append(source_repr)
|
||||
|
||||
# Guardar la representación de la entrada (lista o dict)
|
||||
instruction_repr["inputs"][pin_name] = (
|
||||
input_sources_repr[0]
|
||||
if len(input_sources_repr) == 1
|
||||
else input_sources_repr
|
||||
)
|
||||
|
||||
# --- Poblar Salidas (simplificado: solo conexiones a Access) ---
|
||||
possible_output_pins = set(
|
||||
[
|
||||
"out",
|
||||
"out1",
|
||||
"Q",
|
||||
"q",
|
||||
"eno",
|
||||
"RET_VAL",
|
||||
"DSTBLK",
|
||||
"rt",
|
||||
"cv",
|
||||
"QU",
|
||||
"QD",
|
||||
"ET", # Añadir pines de salida estándar SCL
|
||||
]
|
||||
)
|
||||
if original_type == "BLKMOV":
|
||||
possible_output_pins.add("DSTBLK")
|
||||
if (
|
||||
original_type == "Call"
|
||||
): # Para Calls, las salidas dependen del bloque llamado
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_info_node_list = call_xml_element_list[0].xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
# Buscar parámetros con Section="Output" o "InOut" o "Return"
|
||||
output_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter[@Section='Output' or @Section='InOut' or @Section='Return']/@Name",
|
||||
namespaces=ns,
|
||||
)
|
||||
possible_output_pins.update(output_param_names)
|
||||
|
||||
for pin_name in possible_output_pins:
|
||||
source_key = (instruction_uid, pin_name)
|
||||
if source_key in source_connections:
|
||||
if pin_name not in instruction_repr["outputs"]:
|
||||
instruction_repr["outputs"][pin_name] = []
|
||||
for dest_uid, dest_pin in source_connections[source_key]:
|
||||
if (
|
||||
dest_uid in access_map
|
||||
): # Solo registrar si va a una variable/constante
|
||||
dest_operand_copy = copy.deepcopy(access_map[dest_uid])
|
||||
if (
|
||||
dest_operand_copy
|
||||
not in instruction_repr["outputs"][pin_name]
|
||||
):
|
||||
instruction_repr["outputs"][pin_name].append(
|
||||
dest_operand_copy
|
||||
)
|
||||
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
|
||||
# 4. Inferencia EN (modificado para usar tipos originales)
|
||||
processed_blocks_en_inference = set()
|
||||
try:
|
||||
# Ordenar UIDs numéricamente si es posible
|
||||
sorted_uids_for_en = sorted(
|
||||
all_logic_steps.keys(),
|
||||
key=lambda x: (
|
||||
int(x) if isinstance(x, str) and x.isdigit() else float("inf")
|
||||
),
|
||||
)
|
||||
except ValueError:
|
||||
sorted_uids_for_en = sorted(all_logic_steps.keys()) # Fallback sort
|
||||
|
||||
ordered_logic_list_for_en = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
for i, instruction in enumerate(ordered_logic_list_for_en):
|
||||
part_uid = instruction["instruction_uid"]
|
||||
# Usar el tipo original para la lógica de inferencia
|
||||
part_type_original = (
|
||||
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
|
||||
)
|
||||
|
||||
# Inferencia solo para tipos funcionales que no tengan EN explícito
|
||||
if (
|
||||
part_type_original in functional_block_types
|
||||
and "en" not in instruction.get("inputs", {})
|
||||
and part_uid not in processed_blocks_en_inference
|
||||
and "error" not in part_type_original
|
||||
): # No inferir para errores
|
||||
|
||||
inferred_en_source = None
|
||||
# Buscar hacia atrás en la lista ordenada
|
||||
if i > 0:
|
||||
for j in range(i - 1, -1, -1):
|
||||
prev_instr = ordered_logic_list_for_en[j]
|
||||
if "error" in prev_instr.get("type", ""):
|
||||
continue # Saltar errores previos
|
||||
|
||||
prev_uid = prev_instr["instruction_uid"]
|
||||
prev_type_original = (
|
||||
prev_instr.get("type", "")
|
||||
.replace(SCL_SUFFIX, "")
|
||||
.replace("_error", "")
|
||||
)
|
||||
|
||||
if prev_type_original in rlo_generators: # Fuente RLO encontrada
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "out",
|
||||
}
|
||||
break # Detener búsqueda
|
||||
elif (
|
||||
prev_type_original in functional_block_types
|
||||
): # Bloque funcional previo
|
||||
# Comprobar si este bloque tiene salida ENO conectada
|
||||
if (prev_uid, "eno") in source_connections:
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "eno",
|
||||
}
|
||||
# Si no tiene ENO conectado, el flujo RLO se detiene aquí
|
||||
break # Detener búsqueda
|
||||
elif prev_type_original in [
|
||||
"Coil",
|
||||
"SCoil",
|
||||
"RCoil",
|
||||
"SdCoil",
|
||||
"SetCoil",
|
||||
"ResetCoil",
|
||||
]:
|
||||
# Bobinas terminan el flujo RLO
|
||||
break # Detener búsqueda
|
||||
|
||||
# Si no se encontró fuente, conectar a PowerRail
|
||||
if inferred_en_source is None:
|
||||
inferred_en_source = {"type": "powerrail"}
|
||||
|
||||
# Actualizar la instrucción EN el diccionario principal
|
||||
if part_uid in all_logic_steps:
|
||||
# Asegurar que inputs exista
|
||||
if "inputs" not in all_logic_steps[part_uid]:
|
||||
all_logic_steps[part_uid]["inputs"] = {}
|
||||
all_logic_steps[part_uid]["inputs"]["en"] = inferred_en_source
|
||||
processed_blocks_en_inference.add(part_uid)
|
||||
|
||||
# 5. Lógica ENO (añadir destinos ENO si existen)
|
||||
for source_instr_uid, eno_destinations in eno_outputs.items():
|
||||
if source_instr_uid in all_logic_steps and "error" not in all_logic_steps[
|
||||
source_instr_uid
|
||||
].get("type", ""):
|
||||
all_logic_steps[source_instr_uid]["eno_destinations"] = eno_destinations
|
||||
|
||||
# 6. Ordenar y Devolver
|
||||
final_logic_list = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang, # Lenguaje original de la red
|
||||
"logic": final_logic_list,
|
||||
# No añadir 'error' aquí a menos que el parseo completo falle
|
||||
}
|
||||
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
# Este parser maneja LAD, FBD y GRAPH
|
||||
return {
|
||||
"language": ["LAD", "FBD", "GRAPH"], # Lista de lenguajes soportados
|
||||
"parser_func": parse_lad_fbd_network, # Función a llamar
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
# ToUpload/parsers/parse_scl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import ns, get_multilingual_text
|
||||
|
||||
def reconstruct_scl_from_tokens(st_node):
|
||||
"""
|
||||
Reconstruye SCL desde <StructuredText>, mejorando el manejo de
|
||||
variables, constantes literales, tokens básicos, espacios y saltos de línea.
|
||||
"""
|
||||
if st_node is None:
|
||||
return "// Error: StructuredText node not found.\n"
|
||||
|
||||
scl_parts = []
|
||||
# Usar st:* para obtener todos los elementos hijos dentro del namespace st
|
||||
children = st_node.xpath("./st:*", namespaces=ns)
|
||||
|
||||
for elem in children:
|
||||
tag = etree.QName(elem.tag).localname
|
||||
|
||||
if tag == "Token":
|
||||
scl_parts.append(elem.get("Text", ""))
|
||||
elif tag == "Blank":
|
||||
# Añadir espacios solo si es necesario o más de uno
|
||||
num_spaces = int(elem.get("Num", 1))
|
||||
if not scl_parts or not scl_parts[-1].endswith(" "):
|
||||
scl_parts.append(" " * num_spaces)
|
||||
elif num_spaces > 1:
|
||||
scl_parts.append(" " * (num_spaces -1))
|
||||
|
||||
elif tag == "NewLine":
|
||||
# Quitar espacios finales antes del salto de línea
|
||||
if scl_parts:
|
||||
scl_parts[-1] = scl_parts[-1].rstrip()
|
||||
scl_parts.append("\n")
|
||||
elif tag == "Access":
|
||||
scope = elem.get("Scope")
|
||||
access_str = f"/*_ERR_Scope_{scope}_*/" # Placeholder
|
||||
|
||||
# --- Variables ---
|
||||
if scope in [
|
||||
"GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable",
|
||||
"InputVariable", "OutputVariable", "ConstantVariable",
|
||||
"GlobalConstant", "LocalConstant" # Añadir constantes simbólicas
|
||||
]:
|
||||
symbol_elem = elem.xpath("./st:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./st:Component", namespaces=ns)
|
||||
symbol_text_parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
if i > 0: symbol_text_parts.append(".")
|
||||
|
||||
# Check for HasQuotes attribute (adjust namespace if needed)
|
||||
# El atributo está en el Component o en el Access padre? Probar ambos
|
||||
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true" # Check directly on Component
|
||||
has_quotes_access = False
|
||||
access_parent = comp.xpath("ancestor::st:Access[1]", namespaces=ns) # Get immediate Access parent
|
||||
if access_parent:
|
||||
has_quotes_attr = access_parent[0].xpath("./st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns)
|
||||
has_quotes_access = has_quotes_attr and has_quotes_attr[0].lower() == 'true'
|
||||
|
||||
has_quotes = has_quotes_comp or has_quotes_access
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
# Apply quotes based on HasQuotes or if it's the first component and not temp
|
||||
if has_quotes or (i == 0 and not is_temp and '"' not in name): # Avoid double quotes
|
||||
symbol_text_parts.append(f'"{name}"')
|
||||
else:
|
||||
symbol_text_parts.append(name)
|
||||
|
||||
# --- Array Index Access ---
|
||||
index_access_nodes = comp.xpath("./st:Access", namespaces=ns)
|
||||
if index_access_nodes:
|
||||
# Llamada recursiva para cada índice
|
||||
indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access_nodes]
|
||||
# Limpiar saltos de línea dentro de los corchetes
|
||||
indices_cleaned = [idx.replace('\n', '').strip() for idx in indices_text]
|
||||
symbol_text_parts.append(f"[{','.join(indices_cleaned)}]")
|
||||
|
||||
access_str = "".join(symbol_text_parts)
|
||||
else:
|
||||
access_str = f"/*_ERR_NO_SYMBOL_IN_{scope}_*/"
|
||||
|
||||
# --- Constantes Literales ---
|
||||
elif scope == "LiteralConstant":
|
||||
constant_elem = elem.xpath("./st:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns)
|
||||
const_type = type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else ""
|
||||
const_val = val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONSTVAL_"
|
||||
|
||||
# Formatear según tipo
|
||||
if const_type == "bool": access_str = const_val.upper()
|
||||
elif const_type.lower() == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type.lower() == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WSTRING#'{replaced_val}'"
|
||||
elif const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WCHAR#'{replaced_val}'"
|
||||
elif const_type == "time": access_str = f"T#{const_val}"
|
||||
elif const_type == "ltime": access_str = f"LT#{const_val}"
|
||||
elif const_type == "s5time": access_str = f"S5T#{const_val}"
|
||||
elif const_type == "date": access_str = f"D#{const_val}"
|
||||
elif const_type == "dtl": access_str = f"DTL#{const_val}"
|
||||
elif const_type == "dt": access_str = f"DT#{const_val}"
|
||||
elif const_type == "tod": access_str = f"TOD#{const_val}"
|
||||
elif const_type in ["int", "dint", "sint", "usint", "uint", "udint", "real", "lreal", "word", "dword", "byte"]:
|
||||
# Añadir .0 para reales si no tienen decimal
|
||||
if const_type in ["real", "lreal"] and '.' not in const_val and 'e' not in const_val.lower():
|
||||
access_str = f"{const_val}.0"
|
||||
else:
|
||||
access_str = const_val
|
||||
else: # Otros tipos (LWORD, etc.) o desconocidos
|
||||
access_str = const_val
|
||||
else:
|
||||
access_str = "/*_ERR_NOCONST_*/"
|
||||
|
||||
# --- Llamadas a Funciones/Bloques (Scope=Call) ---
|
||||
elif scope == "Call":
|
||||
call_info_node = elem.xpath("./st:CallInfo", namespaces=ns)
|
||||
if call_info_node:
|
||||
ci = call_info_node[0]
|
||||
call_name = ci.get("Name", "_ERR_CALLNAME_")
|
||||
call_type = ci.get("BlockType") # FB, FC, etc.
|
||||
|
||||
# Parámetros (están como Access o Token dentro de CallInfo/Parameter)
|
||||
params = ci.xpath("./st:Parameter", namespaces=ns)
|
||||
param_parts = []
|
||||
for p in params:
|
||||
p_name = p.get("Name", "_ERR_PARAMNAME_")
|
||||
# El valor del parámetro está dentro del nodo Parameter
|
||||
p_value_node = p.xpath("./st:Access | ./st:Token", namespaces=ns) # Buscar Access o Token
|
||||
p_value_scl = ""
|
||||
if p_value_node:
|
||||
p_value_scl = reconstruct_scl_from_tokens(p) # Parsear el contenido del parámetro
|
||||
p_value_scl = p_value_scl.replace('\n', '').strip() # Limpiar SCL resultante
|
||||
param_parts.append(f"{p_name} := {p_value_scl}")
|
||||
|
||||
# Manejar FB vs FC
|
||||
if call_type == "FB":
|
||||
instance_node = ci.xpath("./st:Instance/st:Component/@Name", namespaces=ns)
|
||||
if instance_node:
|
||||
instance_name = f'"{instance_node[0]}"'
|
||||
access_str = f"{instance_name}({', '.join(param_parts)})"
|
||||
else: # FB sin instancia? Podría ser STAT
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* FB sin instancia explícita? *)'
|
||||
elif call_type == "FC":
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)})'
|
||||
else: # Otros tipos de llamada
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* Tipo: {call_type} *)'
|
||||
else:
|
||||
access_str = "/*_ERR_NO_CALLINFO_*/"
|
||||
|
||||
# Añadir más scopes si son necesarios (e.g., Address, Label, Reference)
|
||||
|
||||
scl_parts.append(access_str)
|
||||
|
||||
elif tag == "Comment" or tag == "LineComment":
|
||||
# Usar get_multilingual_text del parser_utils
|
||||
comment_text = get_multilingual_text(elem)
|
||||
if tag == "Comment":
|
||||
scl_parts.append(f"(* {comment_text} *)")
|
||||
else:
|
||||
scl_parts.append(f"// {comment_text}")
|
||||
# Ignorar otros tipos de nodos si no son relevantes para el SCL
|
||||
|
||||
full_scl = "".join(scl_parts)
|
||||
|
||||
# --- Re-indentación Simple ---
|
||||
output_lines = []
|
||||
indent_level = 0
|
||||
indent_str = " " # Dos espacios
|
||||
for line in full_scl.splitlines():
|
||||
trimmed_line = line.strip()
|
||||
if not trimmed_line:
|
||||
# Mantener líneas vacías? Opcional.
|
||||
# output_lines.append("")
|
||||
continue
|
||||
|
||||
# Reducir indentación ANTES de imprimir para END, ELSE, etc.
|
||||
if trimmed_line.upper().startswith(("END_", "UNTIL", "}")) or \
|
||||
trimmed_line.upper() in ["ELSE", "ELSIF"]:
|
||||
indent_level = max(0, indent_level - 1)
|
||||
|
||||
output_lines.append(indent_str * indent_level + trimmed_line)
|
||||
|
||||
# Aumentar indentación DESPUÉS de imprimir para IF, FOR, etc.
|
||||
# Ser más específico con las palabras clave que aumentan indentación
|
||||
# Usar .upper() para ignorar mayúsculas/minúsculas
|
||||
line_upper = trimmed_line.upper()
|
||||
if line_upper.endswith(("THEN", "DO", "OF", "{")) or \
|
||||
line_upper.startswith(("IF ", "FOR ", "WHILE ", "CASE ", "REPEAT", "STRUCT")) or \
|
||||
line_upper == "ELSE":
|
||||
# Excepción: No indentar después de ELSE IF
|
||||
if not (line_upper == "ELSE" and "IF" in output_lines[-1].upper()):
|
||||
indent_level += 1
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def parse_scl_network(network_element):
|
||||
"""
|
||||
Parsea una red SCL extrayendo el código fuente reconstruido.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSCL_ID")
|
||||
network_lang = "SCL" # Sabemos que es SCL
|
||||
|
||||
# Buscar NetworkSource y luego StructuredText
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
structured_text_node = None
|
||||
if network_source_node:
|
||||
structured_text_node_list = network_source_node[0].xpath("./st:StructuredText", namespaces=ns)
|
||||
if structured_text_node_list:
|
||||
structured_text_node = structured_text_node_list[0]
|
||||
|
||||
reconstructed_scl = "// SCL extraction failed: StructuredText node not found.\n"
|
||||
if structured_text_node is not None:
|
||||
reconstructed_scl = reconstruct_scl_from_tokens(structured_text_node)
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"logic": [ # SCL se guarda como un único bloque lógico
|
||||
{
|
||||
"instruction_uid": f"SCL_{network_id}", # UID sintético
|
||||
"type": "RAW_SCL_CHUNK", # Tipo especial para SCL crudo
|
||||
"scl": reconstructed_scl, # El código SCL reconstruido
|
||||
}
|
||||
],
|
||||
# No añadimos error aquí, reconstruct_scl_from_tokens ya incluye comentarios de error
|
||||
}
|
||||
return parsed_network_data
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {
|
||||
'language': ['SCL'], # Lista de lenguajes soportados
|
||||
'parser_func': parse_scl_network # Función a llamar
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
# ToUpload/parsers/parse_stl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import ns # Solo necesitamos los namespaces aquí
|
||||
|
||||
# --- Funciones Auxiliares de Reconstrucción STL (Adaptadas de x1) ---
|
||||
|
||||
def get_access_text_stl(access_element):
|
||||
"""Reconstruye una representación textual simple de un Access en STL."""
|
||||
if access_element is None: return "_ERR_ACCESS_"
|
||||
scope = access_element.get("Scope")
|
||||
|
||||
# Símbolo (Variable, Constante Simbólica)
|
||||
symbol_elem = access_element.xpath("./stl:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./stl:Component", namespaces=ns)
|
||||
parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
# Comprobar HasQuotes (en Access padre?)
|
||||
has_quotes_elem = comp.xpath("ancestor::stl:Access/stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns)
|
||||
has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true"
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
if i > 0: parts.append(".")
|
||||
# Aplicar comillas
|
||||
if has_quotes or (i == 0 and not is_temp and '"' not in name):
|
||||
parts.append(f'"{name}"')
|
||||
else:
|
||||
parts.append(name)
|
||||
# Índices de Array
|
||||
index_access = comp.xpath("./stl:Access", namespaces=ns)
|
||||
if index_access:
|
||||
indices = [get_access_text_stl(ia) for ia in index_access]
|
||||
parts.append(f"[{','.join(indices)}]")
|
||||
return "".join(parts)
|
||||
|
||||
# Constante Literal
|
||||
constant_elem = access_element.xpath("./stl:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns)
|
||||
const_type = (type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else "")
|
||||
const_val = (val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONST_")
|
||||
|
||||
# Añadir prefijos estándar STL
|
||||
if const_type == "time": return f"T#{const_val}"
|
||||
if const_type == "s5time": return f"S5T#{const_val}"
|
||||
if const_type == "date": return f"D#{const_val}"
|
||||
if const_type == "dt": return f"DT#{const_val}"
|
||||
if const_type == "time_of_day" or const_type=="tod": return f"TOD#{const_val}"
|
||||
if const_type.lower() == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
if const_type.lower() == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
if const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WSTRING#'{replaced_val}'"
|
||||
if const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WCHAR#'{replaced_val}'" # Añadir más si es necesario (WSTRING#, BYTE#, WORD#...)
|
||||
if const_type == "byte" and const_val.startswith("16#"): return f"B#{const_val}" # Formato B#16#FF
|
||||
if const_type == "word" and const_val.startswith("16#"): return f"W#{const_val}"
|
||||
if const_type == "dword" and const_val.startswith("16#"): return f"DW#{const_val}"
|
||||
# Real con punto decimal
|
||||
if const_type == "real" and '.' not in const_val and 'e' not in const_val.lower(): return f"{const_val}.0"
|
||||
return const_val # Valor por defecto
|
||||
|
||||
# Etiqueta
|
||||
label_elem = access_element.xpath("./stl:Label", namespaces=ns)
|
||||
if label_elem:
|
||||
return label_elem[0].get("Name", "_ERR_LABEL_")
|
||||
|
||||
# Acceso Indirecto (Punteros)
|
||||
indirect_elem = access_element.xpath("./stl:Indirect", namespaces=ns)
|
||||
if indirect_elem:
|
||||
reg = indirect_elem[0].get("Register", "AR?") # AR1, AR2
|
||||
offset_str = indirect_elem[0].get("BitOffset", "0")
|
||||
area = indirect_elem[0].get("Area", "DB") # DB, DI, L, etc.
|
||||
width = indirect_elem[0].get("Width", "X") # Bit, Byte, Word, Double
|
||||
try:
|
||||
bit_offset = int(offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
p_format_offset = f"P#{byte_offset}.{bit_in_byte}"
|
||||
except ValueError:
|
||||
p_format_offset = "P#?.?"
|
||||
width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D", "Long": "D"}
|
||||
width_char = width_map.get(width, width[0] if width else "?")
|
||||
return f"{area}{width_char}[{reg},{p_format_offset}]"
|
||||
|
||||
# Dirección Absoluta (I, Q, M, PI, PQ, T, C, DBX, DIX, L)
|
||||
address_elem = access_element.xpath("./stl:Address", namespaces=ns)
|
||||
if address_elem:
|
||||
area = address_elem[0].get("Area", "??") # Input, Output, Memory, DB, DI, Local, Timer, Counter...
|
||||
bit_offset_str = address_elem[0].get("BitOffset", "0")
|
||||
addr_type_str = address_elem[0].get("Type", "Bool") # Bool, Byte, Word, DWord, Int, DInt, Real...
|
||||
try:
|
||||
bit_offset = int(bit_offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
# Determinar ancho (X, B, W, D)
|
||||
addr_width = "X" # Default bit
|
||||
if addr_type_str in ["Byte", "SInt", "USInt"]: addr_width = "B"
|
||||
elif addr_type_str in ["Word", "Int", "UInt"]: addr_width = "W"
|
||||
elif addr_type_str in ["DWord", "DInt", "UDInt", "Real", "Time", "DT", "TOD"]: addr_width = "D"
|
||||
elif addr_type_str in ["LReal", "LTime", "LWord", "LInt", "ULInt"]: addr_width = "D" # L se maneja como D en direccionamiento base? O usar L? Chequear estándar. STL clásico no tenía L.
|
||||
# Mapear Área XML a Área STL
|
||||
area_map = {"Input": "I", "Output": "Q", "Memory": "M",
|
||||
"PeripheryInput": "PI", "PeripheryOutput": "PQ",
|
||||
"DB": "DB", "DI": "DI", "Local": "L",
|
||||
"Timer": "T", "Counter": "C"}
|
||||
stl_area = area_map.get(area, area)
|
||||
|
||||
if stl_area in ["DB", "DI"]:
|
||||
block_num = address_elem[0].get("BlockNumber") # Para DB10.DBX0.0
|
||||
if block_num:
|
||||
return f"{stl_area}{block_num}.{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
else: # Para acceso con registro DB/DI (DBX, DIW, etc.)
|
||||
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
elif stl_area in ["T", "C"]:
|
||||
return f"{stl_area}{byte_offset}" # T 5, C 10 (offset es el número)
|
||||
else: # I, Q, M, L, PI, PQ
|
||||
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # M10.1, IW0, QB5, etc.
|
||||
|
||||
except ValueError:
|
||||
return f"{area}?{bit_offset_str}?"
|
||||
|
||||
# CallInfo (para CALL FC10, CALL FB20, DB10)
|
||||
call_info_elem = access_element.xpath("./stl:CallInfo", namespaces=ns)
|
||||
if call_info_elem:
|
||||
name = call_info_elem[0].get("Name", "_ERR_CALL_")
|
||||
btype = call_info_elem[0].get("BlockType", "FC") # FC, FB, DB
|
||||
instance_node = call_info_elem[0].xpath("./stl:Instance/stl:Component/@Name", namespaces=ns)
|
||||
if btype == "FB" and instance_node:
|
||||
# Para CALL FB, el operando es el DB de instancia
|
||||
db_name_raw = instance_node[0]
|
||||
return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw
|
||||
elif btype == "DB":
|
||||
return f'DB "{name}"' # O solo DB name? ej. DB10
|
||||
else: # FC
|
||||
return f'{btype} "{name}"' # FC "Nombre"
|
||||
|
||||
return f"_{scope}_?" # Fallback
|
||||
|
||||
|
||||
def get_comment_text_stl(comment_element):
|
||||
"""Extrae texto de un LineComment o Comment para STL."""
|
||||
if comment_element is None: return ""
|
||||
# STL Comments suelen tener <Text> directamente
|
||||
text_nodes = comment_element.xpath("./stl:Text/text()", namespaces=ns)
|
||||
if text_nodes:
|
||||
return text_nodes[0].strip()
|
||||
return "" # Vacío si no hay <Text>
|
||||
|
||||
def reconstruct_stl_from_statementlist(statement_list_node):
|
||||
"""Reconstruye el código STL como una cadena de texto desde <StatementList>."""
|
||||
if statement_list_node is None:
|
||||
return "// Error: StatementList node not found.\n"
|
||||
stl_lines = []
|
||||
statements = statement_list_node.xpath("./stl:StlStatement", namespaces=ns)
|
||||
|
||||
for stmt in statements:
|
||||
line_parts = []
|
||||
inline_comment = "" # Comentarios en la misma línea
|
||||
|
||||
# 1. Comentarios iniciales (línea completa //)
|
||||
initial_comments = stmt.xpath("child::stl:Comment[not(@Inserted='true')] | child::stl:LineComment[not(@Inserted='true')]", namespaces=ns)
|
||||
for comm in initial_comments:
|
||||
comment_text = get_comment_text_stl(comm)
|
||||
if comment_text:
|
||||
for comment_line in comment_text.splitlines():
|
||||
stl_lines.append(f"// {comment_line}")
|
||||
|
||||
# 2. Etiqueta (Label)
|
||||
label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns)
|
||||
label_str = ""
|
||||
if label_decl:
|
||||
label_name = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns)
|
||||
if label_name:
|
||||
label_str = f"{label_name[0]}:"
|
||||
# Comentarios después de la etiqueta (inline)
|
||||
label_comments = label_decl[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for lcomm in label_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(lcomm)}"
|
||||
if label_str:
|
||||
line_parts.append(label_str)
|
||||
|
||||
# 3. Instrucción (StlToken)
|
||||
instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns)
|
||||
instruction_str = ""
|
||||
if instruction_token:
|
||||
token_text = instruction_token[0].get("Text", "_ERR_TOKEN_")
|
||||
if token_text == "EMPTY_LINE":
|
||||
stl_lines.append("") # Línea vacía
|
||||
continue # Saltar resto del statement
|
||||
elif token_text == "COMMENT": # Marcador de línea de comentario completo
|
||||
# Ya manejado por initial_comments? Verificar XML. Si no, extraer comentario aquí.
|
||||
pass # Asumir manejado antes
|
||||
else:
|
||||
instruction_str = token_text
|
||||
# Comentarios asociados al token (inline)
|
||||
token_comments = instruction_token[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for tcomm in token_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(tcomm)}"
|
||||
if instruction_str:
|
||||
# Añadir tabulación si hay etiqueta
|
||||
line_parts.append("\t" + instruction_str if label_str else instruction_str)
|
||||
|
||||
# 4. Operando (Access)
|
||||
access_elem = stmt.xpath("./stl:Access", namespaces=ns)
|
||||
access_str = ""
|
||||
if access_elem:
|
||||
access_text = get_access_text_stl(access_elem[0])
|
||||
access_str = access_text
|
||||
# Comentarios dentro del Access (inline)
|
||||
access_comments = access_elem[0].xpath("child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for acc_comm in access_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(acc_comm)}"
|
||||
if access_str:
|
||||
line_parts.append(access_str)
|
||||
|
||||
# Construir línea final
|
||||
current_line = " ".join(lp for lp in line_parts if lp) # Unir partes con espacio
|
||||
if inline_comment:
|
||||
current_line += f"\t{inline_comment.strip()}" # Añadir comentario con tab
|
||||
|
||||
if current_line.strip(): # Añadir solo si no está vacía después de todo
|
||||
stl_lines.append(current_line.rstrip()) # Quitar espacios finales
|
||||
|
||||
return "\n".join(stl_lines)
|
||||
|
||||
|
||||
def parse_stl_network(network_element):
|
||||
"""
|
||||
Parsea una red STL extrayendo el código fuente reconstruido.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSTL_ID")
|
||||
network_lang = "STL"
|
||||
|
||||
# Buscar NetworkSource y luego StatementList
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
statement_list_node = None
|
||||
if network_source_node:
|
||||
statement_list_node_list = network_source_node[0].xpath("./stl:StatementList", namespaces=ns)
|
||||
if statement_list_node_list:
|
||||
statement_list_node = statement_list_node_list[0]
|
||||
|
||||
reconstructed_stl = "// STL extraction failed: StatementList node not found.\n"
|
||||
if statement_list_node is not None:
|
||||
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node)
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"logic": [ # STL se guarda como un único bloque lógico
|
||||
{
|
||||
"instruction_uid": f"STL_{network_id}", # UID sintético
|
||||
"type": "RAW_STL_CHUNK", # Tipo especial para STL crudo
|
||||
"stl": reconstructed_stl, # El código STL reconstruido
|
||||
}
|
||||
],
|
||||
}
|
||||
return parsed_network_data
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {
|
||||
'language': ['STL'], # Lenguaje soportado
|
||||
'parser_func': parse_stl_network # Función a llamar
|
||||
}
|
|
@ -0,0 +1,478 @@
|
|||
# ToUpload/parsers/parser_utils.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import traceback
|
||||
|
||||
# --- Namespaces (Común para muchos parsers) ---
|
||||
ns = {
|
||||
"iface": "http://www.siemens.com/automation/Openness/SW/Interface/v5",
|
||||
"flg": "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4",
|
||||
"st": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StructuredText/v3",
|
||||
"stl": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StatementList/v4",
|
||||
}
|
||||
|
||||
# --- Funciones Comunes de Extracción de Texto y Nodos ---
|
||||
|
||||
|
||||
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
|
||||
"""Extrae texto multilingüe de un elemento XML."""
|
||||
if element is None:
|
||||
return ""
|
||||
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)
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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 ""
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Error extrayendo MultilingualText: {e}")
|
||||
# traceback.print_exc() # Descomentar para más detalles del error
|
||||
return ""
|
||||
|
||||
|
||||
def get_symbol_name(symbol_element):
|
||||
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
||||
# Adaptado para usar namespace flg
|
||||
if symbol_element is None:
|
||||
return None
|
||||
try:
|
||||
# Asume que Component está dentro de Symbol y ambos están en el namespace flg
|
||||
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
||||
# Formatear correctamente con comillas dobles si es necesario (ej. DBs)
|
||||
return (
|
||||
".".join(
|
||||
f'"{c}"' if not c.startswith("#") and '"' not in c else c
|
||||
for c in components
|
||||
)
|
||||
if components
|
||||
else None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Excepción en get_symbol_name: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_access(access_element):
|
||||
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
|
||||
# Adaptado para usar namespace flg
|
||||
if access_element is None:
|
||||
return None
|
||||
uid = access_element.get("UId")
|
||||
scope = access_element.get("Scope")
|
||||
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
||||
|
||||
# Buscar Symbol o Constant usando el namespace flg
|
||||
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
||||
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
||||
|
||||
if symbol:
|
||||
info["type"] = "variable"
|
||||
# Llamar a get_symbol_name que ahora espera flg:Symbol
|
||||
info["name"] = get_symbol_name(symbol[0])
|
||||
if info["name"] is None:
|
||||
info["type"] = "error_parsing_symbol"
|
||||
print(f"Error: No se pudo parsear nombre símbolo Access UID={uid}")
|
||||
# Intentar extraer texto directamente como fallback muy básico
|
||||
raw_text = "".join(symbol[0].xpath(".//text()")).strip()
|
||||
info["name"] = (
|
||||
f'"_ERR_PARSING_{raw_text[:20]}"'
|
||||
if raw_text
|
||||
else f'"_ERR_PARSING_EMPTY_SYMBOL_ACCESS_{uid}"'
|
||||
)
|
||||
# return info # Podríamos devolver el error aquí
|
||||
elif constant:
|
||||
info["type"] = "constant"
|
||||
# Buscar ConstantType y ConstantValue usando el namespace flg
|
||||
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
||||
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
||||
|
||||
# Extraer texto
|
||||
info["datatype"] = (
|
||||
const_type_elem[0].text.strip()
|
||||
if const_type_elem and const_type_elem[0].text is not None
|
||||
else "Unknown"
|
||||
)
|
||||
value_str = (
|
||||
const_val_elem[0].text.strip()
|
||||
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"]:
|
||||
info["datatype"] = "Bool"
|
||||
elif value_str.isdigit() or (
|
||||
value_str.startswith("-") and value_str[1:].isdigit()
|
||||
):
|
||||
info["datatype"] = "Int" # O DInt? Int es más seguro
|
||||
elif "." in value_str:
|
||||
try:
|
||||
float(value_str)
|
||||
info["datatype"] = "Real" # O LReal? Real es más seguro
|
||||
except ValueError:
|
||||
pass # Podría ser string con punto
|
||||
elif "#" in value_str:
|
||||
# Inferir tipo desde prefijo (T#, DT#, '...', etc.)
|
||||
parts = value_str.split("#", 1)
|
||||
prefix = parts[0].upper()
|
||||
if prefix == "T":
|
||||
info["datatype"] = "Time"
|
||||
elif prefix == "LT":
|
||||
info["datatype"] = "LTime"
|
||||
elif prefix == "S5T":
|
||||
info["datatype"] = "S5Time"
|
||||
elif prefix == "D":
|
||||
info["datatype"] = "Date"
|
||||
elif prefix == "DT":
|
||||
info["datatype"] = "DT"
|
||||
elif prefix == "DTL":
|
||||
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
|
||||
else:
|
||||
info["datatype"] = (
|
||||
"TypedConstant" # Genérico si no se reconoce prefijo
|
||||
)
|
||||
|
||||
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)
|
||||
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:
|
||||
val_str_processed = value_str.split("#", 1)[-1]
|
||||
if (
|
||||
val_str_processed.startswith("'")
|
||||
and val_str_processed.endswith("'")
|
||||
and len(val_str_processed) > 1
|
||||
):
|
||||
val_str_processed = val_str_processed[1:-1]
|
||||
try:
|
||||
if dtype_lower in [
|
||||
"int",
|
||||
"dint",
|
||||
"udint",
|
||||
"sint",
|
||||
"usint",
|
||||
"lint",
|
||||
"ulint",
|
||||
"word",
|
||||
"dword",
|
||||
"lword",
|
||||
"byte",
|
||||
]:
|
||||
info["value"] = int(val_str_processed)
|
||||
elif dtype_lower == "bool":
|
||||
info["value"] = (
|
||||
val_str_processed.lower() == "true" or val_str_processed == "1"
|
||||
)
|
||||
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
|
||||
|
||||
else:
|
||||
info["type"] = "unknown_structure"
|
||||
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
||||
# return info
|
||||
|
||||
# Verificar nombre faltante después de intentar parsear
|
||||
if info["type"] == "variable" and info.get("name") is None:
|
||||
print(f"Error Interno: parse_access var sin nombre UID {uid}.")
|
||||
info["type"] = "error_no_name"
|
||||
# return info
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def parse_part(part_element):
|
||||
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
||||
# Asume que Part está en namespace flg
|
||||
if part_element is None:
|
||||
return None
|
||||
uid = part_element.get("UId")
|
||||
name = part_element.get("Name")
|
||||
if not uid or not name:
|
||||
print(
|
||||
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
||||
)
|
||||
return None
|
||||
|
||||
template_values = {}
|
||||
try:
|
||||
# TemplateValue parece NO tener namespace flg
|
||||
for tv in part_element.xpath("./TemplateValue"):
|
||||
tv_name = tv.get("Name")
|
||||
tv_type = tv.get("Type")
|
||||
if tv_name and tv_type:
|
||||
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')
|
||||
"template_values": template_values,
|
||||
"negated_pins": negated_pins,
|
||||
}
|
||||
|
||||
|
||||
def parse_call(call_element):
|
||||
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
||||
# Asume que Call está en namespace flg
|
||||
if call_element is None:
|
||||
return None
|
||||
uid = call_element.get("UId")
|
||||
if not uid:
|
||||
print(
|
||||
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
|
||||
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
|
||||
|
||||
block_name = call_info.get("Name")
|
||||
block_type = call_info.get("BlockType") # FC, FB
|
||||
if not block_name or not block_type:
|
||||
print(f"Error: CallInfo para UID {uid} sin Name o BlockType.")
|
||||
return {
|
||||
"uid": uid,
|
||||
"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
|
||||
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
|
||||
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('"')
|
||||
else db_name_raw
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: <flg:Component> en <flg:Instance> FB Call UID {uid} sin 'Name'."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: No se encontró <flg:Component> en <flg:Instance> FB Call UID {uid}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
|
||||
)
|
||||
# Aquí podríamos intentar buscar si el scope del Call es LocalVariable para inferir STAT
|
||||
call_scope = call_element.get("Scope") # Scope del <Call> mismo
|
||||
if call_scope == "LocalVariable":
|
||||
# Si la llamada es local y no tiene <Instance>, probablemente es una multi-instancia STAT
|
||||
instance_name = f'"{block_name}"' # Usar el nombre del bloque como nombre de instancia STAT (convención común)
|
||||
instance_scope = "Static" # Marcar como estático
|
||||
print(
|
||||
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
||||
)
|
||||
# else: # Error si es Global y no tiene Instance? Depende de la semántica deseada.
|
||||
# print(f"Error: FB Call '{block_name}' UID {uid} no es STAT y no tiene <flg:Instance>.")
|
||||
# return {"uid": uid, "type": "Call_error", "error": "FB Call sin datos de instancia"}
|
||||
|
||||
# El 'type' aquí es genérico 'Call', la distinción FC/FB se hace con block_type
|
||||
call_data = {
|
||||
"uid": uid,
|
||||
"type": "Call",
|
||||
"block_name": block_name,
|
||||
"block_type": block_type, # FC o FB
|
||||
}
|
||||
if instance_name:
|
||||
call_data["instance_db"] = instance_name # Nombre formateado SCL
|
||||
if instance_scope:
|
||||
call_data["instance_scope"] = instance_scope # Static, GlobalDB, etc.
|
||||
|
||||
return call_data
|
||||
|
||||
|
||||
def parse_interface_members(member_elements):
|
||||
"""
|
||||
Parsea recursivamente una lista de elementos <Member> de una interfaz o estructura.
|
||||
Maneja miembros simples, structs anidados y arrays con valores iniciales.
|
||||
Usa el namespace 'iface'.
|
||||
"""
|
||||
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_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."
|
||||
)
|
||||
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)
|
||||
"remanence": member_remanence,
|
||||
"accessibility": member_accessibility,
|
||||
"start_value": None,
|
||||
"comment": None,
|
||||
"children": [], # Para Structs
|
||||
"array_elements": {}, # Para Arrays
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
if constant_name
|
||||
else (
|
||||
start_value_node[0].text
|
||||
if start_value_node[0].text is not None
|
||||
else ""
|
||||
)
|
||||
)
|
||||
# No intentar convertir aquí, se hará en x3 según el tipo de dato
|
||||
|
||||
# --- Structs Anidados ---
|
||||
# Los miembros de un struct están dentro de Sections/Section/Member
|
||||
nested_sections = member.xpath(
|
||||
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
||||
) # Sección sin nombre específico
|
||||
if nested_sections:
|
||||
# Llamada recursiva
|
||||
member_info["children"] = parse_interface_members(nested_sections)
|
||||
|
||||
# --- Arrays ---
|
||||
# Buscar elementos <Subelement> para valores iniciales de array
|
||||
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
|
||||
subelements = member.xpath("./iface:Subelement", namespaces=ns)
|
||||
for sub in subelements:
|
||||
path = sub.get("Path") # Path es el índice: '0', '1', '0,0', etc.
|
||||
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
|
||||
if path and sub_start_value_node:
|
||||
constant_name = sub_start_value_node[0].get("ConstantName")
|
||||
value = (
|
||||
constant_name
|
||||
if constant_name
|
||||
else (
|
||||
sub_start_value_node[0].text
|
||||
if sub_start_value_node[0].text is not None
|
||||
else ""
|
||||
)
|
||||
)
|
||||
member_info["array_elements"][path] = value
|
||||
# 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,
|
||||
}
|
||||
|
||||
members_data.append(member_info)
|
||||
return members_data
|
|
@ -11,17 +11,14 @@ SCL_SUFFIX = "_sympy_processed"
|
|||
|
||||
def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
# Get original type before potential suffix/error was added by x1 or previous passes
|
||||
# This requires storing the original type perhaps, or removing known suffixes
|
||||
# Let's assume 'block_type' (FC/FB) and 'block_name' are correct from x1
|
||||
instr_type_original = instruction.get("type", "") # Tipo antes de añadir sufijo
|
||||
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||
return False
|
||||
|
||||
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
|
||||
block_type = instruction.get("block_type") # FC, FB
|
||||
instance_db = instruction.get("instance_db") # Nombre del DB de instancia (para FB)
|
||||
|
||||
# Check if already processed
|
||||
if instruction.get("type", "").endswith(SCL_SUFFIX) or "_error" in instruction.get("type", ""):
|
||||
return False
|
||||
|
||||
# Formatear nombres SCL (para la llamada final)
|
||||
block_name_scl = format_variable_name(block_name)
|
||||
instance_db_scl = format_variable_name(instance_db) if instance_db else None
|
||||
|
@ -36,140 +33,91 @@ def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManag
|
|||
|
||||
# --- Procesar Parámetros de Entrada ---
|
||||
scl_call_params = []
|
||||
processed_inputs = {"en"} # Track processed pins to avoid duplicates if 'en' is also listed elsewhere
|
||||
processed_inputs = {"en"}
|
||||
dependencies_resolved = True
|
||||
|
||||
# Iterar sobre las entradas que x1 debería haber poblado
|
||||
# Ordenar por nombre de pin para consistencia en la llamada SCL
|
||||
# Ordenar para consistencia
|
||||
input_pin_names = sorted(instruction.get("inputs", {}).keys())
|
||||
|
||||
for pin_name in input_pin_names:
|
||||
if pin_name not in processed_inputs: # Skip 'en' if already handled
|
||||
if pin_name not in processed_inputs:
|
||||
source_info = instruction["inputs"][pin_name]
|
||||
|
||||
# Get the representation of the source (SymPy, constant, or SCL string)
|
||||
# Obtener la representación de la fuente (puede ser SymPy o Constante/String)
|
||||
source_sympy_or_const = get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
if source_sympy_or_const is None:
|
||||
# print(f"DEBUG Call {instr_uid}: Input param '{pin_name}' dependency not ready.")
|
||||
dependencies_resolved = False
|
||||
break # Exit if one dependency is not ready
|
||||
break # Salir si una dependencia no está lista
|
||||
|
||||
# Convert the expression/constant to SCL for the call
|
||||
# Simplification of inputs is generally not needed here, convert directly
|
||||
# Convertir la expresión/constante a SCL para la llamada
|
||||
# Simplificar ANTES de convertir? Probablemente no necesario para parámetros de entrada
|
||||
# a menos que queramos optimizar el valor pasado. Por ahora, convertir directo.
|
||||
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
|
||||
|
||||
# Parameter pin name needs formatting for SCL
|
||||
# El nombre del pin SÍ necesita formateo
|
||||
pin_name_scl = format_variable_name(pin_name)
|
||||
|
||||
# Special check for DB_ANY or ANY_POINTER - pass name directly without :=
|
||||
# We need the original parameter type info for this, which is not in the simplified JSON.
|
||||
# WORKAROUND: Check if param_scl_value looks like a DB name ("DB_NAME")
|
||||
# This is heuristic and might be wrong. Ideally, x1 should pass type info.
|
||||
# For now, we assume standard 'Param := Value' syntax.
|
||||
# if param_scl_value.startswith('"') and param_scl_value.endswith('"') and block_type == "FC": # Heuristic for DB_ANY?
|
||||
# scl_call_params.append(f"{pin_name_scl} := {param_scl_value}") # Still use := for clarity? TIA might infer
|
||||
# else:
|
||||
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
|
||||
|
||||
processed_inputs.add(pin_name)
|
||||
|
||||
if not dependencies_resolved:
|
||||
return False
|
||||
|
||||
# --- Construcción de la Llamada SCL (con parámetros) ---
|
||||
# --- Construcción de la Llamada SCL (similar a antes) ---
|
||||
scl_call_body = ""
|
||||
param_string = ", ".join(scl_call_params) # Join parameters with commas
|
||||
param_string = ", ".join(scl_call_params)
|
||||
|
||||
if block_type == "FB":
|
||||
if not instance_db_scl:
|
||||
print(f"Error: Call FB '{block_name_scl}' (UID {instr_uid}) sin instancia.")
|
||||
instruction["scl"] = f"// ERROR: FB Call {block_name_scl} sin instancia"
|
||||
instruction["type"] = f"Call_FB_error" # Mark with error
|
||||
return True # Processed (with error)
|
||||
# FB Call: InstanceName(Param1 := Value1, Param2 := Value2);
|
||||
instruction["type"] = f"Call_FB_error"
|
||||
return True
|
||||
scl_call_body = f"{instance_db_scl}({param_string});"
|
||||
elif block_type == "FC":
|
||||
# FC Call: BlockName(Param1 := Value1, Param2 := Value2);
|
||||
scl_call_body = f"{block_name_scl}({param_string});"
|
||||
else:
|
||||
print(f"Advertencia: Tipo de bloque no soportado para Call UID {instr_uid}: {block_type}")
|
||||
scl_call_body = f"// ERROR: Call a bloque tipo '{block_type}' no soportado: {block_name_scl}"
|
||||
# Mark instruction type with error
|
||||
instruction["type"] = f"Call_{block_type or 'Unknown'}_error" # Add specific type if known
|
||||
|
||||
instruction["type"] = f"Call_{block_type}_error" # Marcar como error
|
||||
|
||||
# --- Aplicar Condición EN (usando la expresión SymPy EN) ---
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
# Simplify the EN condition before converting to SCL
|
||||
# Simplificar la condición EN ANTES de convertirla a SCL
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for Call {instr_uid} ({block_name_scl}): {e}")
|
||||
print(f"Error simplifying EN for Call {instr_uid}: {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
# Avoid IF TRUE/FALSE blocks
|
||||
if en_condition_scl == "TRUE":
|
||||
scl_final = scl_call_body
|
||||
elif en_condition_scl == "FALSE":
|
||||
scl_final = f"// Call {block_name_scl} (UID {instr_uid}) condition simplified to FALSE."
|
||||
# Also update type to avoid further processing?
|
||||
# instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}_Optimized"
|
||||
else:
|
||||
# Indent the call body within the IF block
|
||||
indented_call = "\n".join([f" {line}" for line in scl_call_body.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_call}\nEND_IF;"
|
||||
indented_call = "\n".join([f" {line}" for line in scl_call_body.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_call}\nEND_IF;"
|
||||
else:
|
||||
# No IF needed if EN is always TRUE
|
||||
scl_final = scl_call_body
|
||||
|
||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
||||
instruction["scl"] = scl_final # Guardar el SCL final generado
|
||||
instruction["type"] = (f"Call_{block_type}{SCL_SUFFIX}" if "_error" not in instruction["type"] else instruction["type"])
|
||||
|
||||
# Update instruction type to mark as processed (unless already marked as error)
|
||||
if "_error" not in instruction.get("type", ""):
|
||||
instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}"
|
||||
|
||||
# Propagar el estado ENO (es la expresión SymPy de EN)
|
||||
# Actualizar sympy_map con el estado ENO (es la expresión SymPy de EN)
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||
|
||||
# --- Propagar Valores de Salida (Importante pero complejo) ---
|
||||
# Esto requiere conocer la interfaz del bloque llamado (que no tenemos aquí directamente)
|
||||
# O asumir convenciones estándar (ej. FCs tienen Ret_Val, FBs tienen outputs en su instancia)
|
||||
|
||||
# Heurística simple: Si es un FC, intentar propagar Ret_Val si existe en outputs
|
||||
# Si es un FB, las salidas se acceden a través de la instancia (e.g., "MyInstance".Output1)
|
||||
# Por ahora, dejaremos la propagación de salidas más avanzada para una mejora futura
|
||||
# o requerirá pasar información de la interfaz del bloque llamado.
|
||||
|
||||
# Ejemplo básico (necesita mejorar):
|
||||
# Propagar valores de salida (requiere info de interfaz o heurística)
|
||||
# Si se sabe que hay una salida 'MyOutput', se podría añadir su SCL al mapa
|
||||
# Ejemplo MUY simplificado:
|
||||
# for pin_name, dest_list in instruction.get("outputs", {}).items():
|
||||
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
||||
# map_key_out = (network_id, instr_uid, pin_name)
|
||||
# pin_name_scl = format_variable_name(pin_name)
|
||||
# if block_type == "FB" and instance_db_scl:
|
||||
# # Salida de FB: "Instancia".NombrePin
|
||||
# output_scl_access = f"{instance_db_scl}.{pin_name_scl}"
|
||||
# # Podríamos guardar el string SCL o crear/obtener un Symbol
|
||||
# sympy_out_symbol = symbol_manager.get_symbol(output_scl_access)
|
||||
# sympy_map[map_key_out] = sympy_out_symbol if sympy_out_symbol else output_scl_access # Prefiere Symbol
|
||||
# elif block_type == "FC":
|
||||
# # Salida de FC: Requiere asignar a una variable (temporal o de interfaz)
|
||||
# # Esto se complica porque el destino está en 'dest_list'
|
||||
# if len(dest_list) == 1 and dest_list[0].get("type") == "variable":
|
||||
# target_var_name = format_variable_name(dest_list[0].get("name"))
|
||||
# # Guardar el nombre del destino SCL que contendrá el valor
|
||||
# sympy_map[map_key_out] = target_var_name
|
||||
# # Necesitaríamos modificar scl_final para incluir la asignación:
|
||||
# # target_var_name := FC_Call(...); (requiere reestructurar la generación SCL)
|
||||
# else:
|
||||
# # Múltiples destinos o destino no variable es complejo para FC outputs
|
||||
# sympy_map[map_key_out] = f"/* TODO: Assign FC output {pin_name_scl} */"
|
||||
|
||||
# sympy_map[map_key_out] = f"{instance_db_scl}.{format_variable_name(pin_name)}" # Guardar el *string* de acceso SCL
|
||||
# # Para FCs es más complejo, necesitaría asignación explícita a temp
|
||||
# # else: # FC output -> necesita temp var
|
||||
# # temp_var = generate_temp_var_name(...)
|
||||
# # sympy_map[map_key_out] = temp_var
|
||||
|
||||
return True
|
||||
|
||||
|
@ -177,8 +125,7 @@ def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManag
|
|||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para las llamadas a FC y FB."""
|
||||
# Asegurarse que los type_name coincidan con los usados en x1 y x2
|
||||
return [
|
||||
{'type_name': 'call_fc', 'processor_func': process_call, 'priority': 6}, # Prioridad alta
|
||||
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6} # Prioridad alta
|
||||
{'type_name': 'call_fc', 'processor_func': process_call, 'priority': 6},
|
||||
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6}
|
||||
]
|
|
@ -3,255 +3,163 @@ import subprocess
|
|||
import os
|
||||
import sys
|
||||
import locale
|
||||
import glob # <--- Importar glob para buscar archivos
|
||||
import glob
|
||||
|
||||
|
||||
# (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior)
|
||||
# (Función get_console_encoding y variable CONSOLE_ENCODING como antes)
|
||||
def get_console_encoding():
|
||||
"""Obtiene la codificación preferida de la consola, con fallback."""
|
||||
try:
|
||||
return locale.getpreferredencoding(False)
|
||||
except Exception:
|
||||
return "cp1252"
|
||||
|
||||
# Fallback común en Windows si falla getpreferredencoding
|
||||
return "cp1252" # O prueba con 'utf-8' si cp1252 da problemas
|
||||
|
||||
CONSOLE_ENCODING = get_console_encoding()
|
||||
# Descomenta la siguiente línea si quieres ver la codificación detectada:
|
||||
# print(f"Detected console encoding: {CONSOLE_ENCODING}")
|
||||
|
||||
|
||||
# (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING)
|
||||
# (Función run_script como antes, usando CONSOLE_ENCODING)
|
||||
def run_script(script_name, xml_arg):
|
||||
"""Runs a given script with the specified XML file argument."""
|
||||
script_path = os.path.join(os.path.dirname(__file__), script_name)
|
||||
command = [sys.executable, script_path, xml_arg]
|
||||
# Asegurarse que la ruta al script sea absoluta o relativa al script actual
|
||||
script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name)
|
||||
# Usar la ruta absoluta al ejecutable de Python actual
|
||||
python_executable = sys.executable
|
||||
command = [python_executable, script_path, xml_arg] # Usar la ruta absoluta de python
|
||||
print(f"\n--- Running {script_name} with argument: {xml_arg} ---")
|
||||
try:
|
||||
# Ejecutar el proceso hijo
|
||||
result = subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding=CONSOLE_ENCODING,
|
||||
errors="replace",
|
||||
) # 'replace' para evitar errores
|
||||
check=True, # Lanza excepción si el script falla (return code != 0)
|
||||
capture_output=True,# Captura stdout y stderr
|
||||
text=True, # Decodifica stdout/stderr como texto
|
||||
encoding=CONSOLE_ENCODING, # Usa la codificación detectada
|
||||
errors='replace' # Reemplaza caracteres no decodificables
|
||||
)
|
||||
|
||||
# Imprimir stdout y stderr si no están vacíos
|
||||
stdout_clean = result.stdout.strip() if result.stdout else ""
|
||||
stderr_clean = result.stderr.strip() if result.stderr else ""
|
||||
|
||||
# Imprimir stdout y stderr
|
||||
# Eliminar saltos de línea extra al final si existen
|
||||
stdout_clean = result.stdout.strip()
|
||||
stderr_clean = result.stderr.strip()
|
||||
if stdout_clean:
|
||||
print(stdout_clean)
|
||||
if stderr_clean:
|
||||
print("--- Stderr ---")
|
||||
print(stderr_clean)
|
||||
print("--------------")
|
||||
# Imprimir stderr claramente para errores del script hijo
|
||||
print(f"--- Stderr ({script_name}) ---", file=sys.stderr) # Imprimir en stderr
|
||||
print(stderr_clean, file=sys.stderr)
|
||||
print("--------------------------", file=sys.stderr)
|
||||
|
||||
print(f"--- {script_name} finished successfully ---")
|
||||
return True
|
||||
return True # Indicar éxito
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Script '{script_path}' not found.")
|
||||
# Error si el script python o el ejecutable no se encuentran
|
||||
print(f"Error: Script '{script_path}' or Python executable '{python_executable}' not found.", file=sys.stderr)
|
||||
return False
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running {script_name}:")
|
||||
print(f"Return code: {e.returncode}")
|
||||
stdout_decoded = (
|
||||
e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip()
|
||||
if isinstance(e.stdout, bytes)
|
||||
else (e.stdout or "").strip()
|
||||
)
|
||||
stderr_decoded = (
|
||||
e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip()
|
||||
if isinstance(e.stderr, bytes)
|
||||
else (e.stderr or "").strip()
|
||||
)
|
||||
# Error si el script hijo devuelve un código de error (ej., sys.exit(1))
|
||||
print(f"Error running {script_name}: Script returned non-zero exit code {e.returncode}.", file=sys.stderr)
|
||||
|
||||
# Decodificar e imprimir stdout/stderr del proceso fallido
|
||||
stdout_decoded = e.stdout.strip() if e.stdout else ""
|
||||
stderr_decoded = e.stderr.strip() if e.stderr else ""
|
||||
|
||||
if stdout_decoded:
|
||||
print("--- Stdout ---")
|
||||
print(stdout_decoded)
|
||||
print(f"--- Stdout ({script_name}) ---", file=sys.stderr)
|
||||
print(stdout_decoded, file=sys.stderr)
|
||||
if stderr_decoded:
|
||||
print("--- Stderr ---")
|
||||
print(stderr_decoded)
|
||||
print("--------------")
|
||||
return False
|
||||
print(f"--- Stderr ({script_name}) ---", file=sys.stderr)
|
||||
print(stderr_decoded, file=sys.stderr)
|
||||
print("--------------------------", file=sys.stderr)
|
||||
return False # Indicar fallo
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred while running {script_name}: {e}")
|
||||
return False
|
||||
# Otros errores inesperados
|
||||
print(f"An unexpected error occurred while running {script_name}: {e}", file=sys.stderr)
|
||||
# Imprimir traceback para depuración
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
return False # Indicar fallo
|
||||
|
||||
|
||||
# --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO ---
|
||||
def select_xml_file():
|
||||
"""Busca archivos .xml, los lista y pide al usuario que elija uno."""
|
||||
print("No XML file specified. Searching for XML files in current directory...")
|
||||
# Buscar archivos .xml en el directorio actual (.)
|
||||
xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético
|
||||
|
||||
if not xml_files:
|
||||
print("Error: No .xml files found in the current directory.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\nAvailable XML files:")
|
||||
for i, filename in enumerate(xml_files, start=1):
|
||||
print(f" {i}: {filename}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input(
|
||||
f"Enter the number of the file to process (1-{len(xml_files)}): "
|
||||
)
|
||||
choice_num = int(choice)
|
||||
if 1 <= choice_num <= len(xml_files):
|
||||
selected_file = xml_files[choice_num - 1]
|
||||
print(f"Selected: {selected_file}")
|
||||
return selected_file
|
||||
else:
|
||||
print("Invalid choice. Please enter a number from the list.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
except EOFError: # Manejar si la entrada se cierra inesperadamente
|
||||
print("\nSelection cancelled.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# --- FIN NUEVA FUNCIÓN ---
|
||||
|
||||
# --- NO SE NECESITA select_xml_file() si procesamos todos ---
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Imports necesarios para esta sección
|
||||
import os
|
||||
import sys
|
||||
import glob # Asegúrate de que glob esté importado al principio del archivo
|
||||
|
||||
# --- PARTE 1: BUSCAR ARCHIVOS ---
|
||||
# Directorio base donde buscar los archivos XML (relativo al script)
|
||||
base_search_dir = "XML Project"
|
||||
script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py
|
||||
# Obtener la ruta absoluta del directorio donde está x0_main.py
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
xml_project_dir = os.path.join(script_dir, base_search_dir)
|
||||
|
||||
print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'")
|
||||
|
||||
# Verificar si el directorio 'XML Project' existe
|
||||
if not os.path.isdir(xml_project_dir):
|
||||
print(
|
||||
f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio."
|
||||
)
|
||||
print(
|
||||
"Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro."
|
||||
)
|
||||
sys.exit(1)
|
||||
print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.", file=sys.stderr)
|
||||
print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.")
|
||||
sys.exit(1) # Salir con error
|
||||
|
||||
# Buscar todos los archivos .xml recursivamente dentro de xml_project_dir
|
||||
# Usamos os.path.join para construir la ruta de búsqueda correctamente
|
||||
# y '**/*.xml' para la recursividad con glob
|
||||
# Buscar todos los archivos .xml recursivamente
|
||||
search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
|
||||
xml_files_found = glob.glob(search_pattern, recursive=True)
|
||||
|
||||
if not xml_files_found:
|
||||
print(
|
||||
f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios."
|
||||
)
|
||||
sys.exit(0) # Salir limpiamente si no hay archivos
|
||||
print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.")
|
||||
sys.exit(0) # Salir limpiamente si no hay archivos
|
||||
|
||||
print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:")
|
||||
# Ordenar para un procesamiento predecible (opcional)
|
||||
xml_files_found.sort()
|
||||
xml_files_found.sort() # Ordenar para consistencia
|
||||
for xml_file in xml_files_found:
|
||||
# Imprimir la ruta relativa desde el directorio del script para claridad
|
||||
print(f" - {os.path.relpath(xml_file, script_dir)}")
|
||||
|
||||
# Scripts a ejecutar en secuencia (asegúrate que los nombres son correctos)
|
||||
# --- PARTE 2: PROCESAR CADA ARCHIVO ---
|
||||
# Scripts a ejecutar en secuencia
|
||||
script1 = "x1_to_json.py"
|
||||
script2 = "x2_process.py"
|
||||
script3 = "x3_generate_scl.py"
|
||||
|
||||
# Procesar cada archivo encontrado
|
||||
processed_count = 0
|
||||
failed_count = 0
|
||||
for xml_filepath in xml_files_found:
|
||||
print(
|
||||
f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
||||
)
|
||||
|
||||
# Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio
|
||||
# Procesar cada archivo encontrado en el bucle
|
||||
for xml_filepath in xml_files_found:
|
||||
relative_path = os.path.relpath(xml_filepath, script_dir)
|
||||
print(f"\n--- Iniciando pipeline para: {relative_path} ---")
|
||||
|
||||
# Usar la ruta absoluta para los scripts hijos
|
||||
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
||||
|
||||
# Ejecutar los scripts en secuencia para el archivo actual
|
||||
# La función run_script ya está definida en tu script x0_main.py
|
||||
# Ejecutar los scripts en secuencia
|
||||
success = True
|
||||
if not run_script(script1, absolute_xml_filepath):
|
||||
print(
|
||||
f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
||||
)
|
||||
print(f"\nPipeline falló en el script '{script1}' para el archivo: {relative_path}", file=sys.stderr)
|
||||
success = False
|
||||
elif not run_script(script2, absolute_xml_filepath):
|
||||
print(
|
||||
f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
||||
)
|
||||
print(f"\nPipeline falló en el script '{script2}' para el archivo: {relative_path}", file=sys.stderr)
|
||||
success = False
|
||||
elif not run_script(script3, absolute_xml_filepath):
|
||||
print(
|
||||
f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
||||
)
|
||||
print(f"\nPipeline falló en el script '{script3}' para el archivo: {relative_path}", file=sys.stderr)
|
||||
success = False
|
||||
|
||||
# Actualizar contadores y mostrar estado
|
||||
if success:
|
||||
print(
|
||||
f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
||||
)
|
||||
print(f"--- Pipeline completado exitosamente para: {relative_path} ---")
|
||||
processed_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
print(
|
||||
f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
||||
)
|
||||
print(f"--- Pipeline falló para: {relative_path} ---", file=sys.stderr) # Indicar fallo
|
||||
|
||||
# --- PARTE 3: RESUMEN FINAL ---
|
||||
print("\n--- Resumen Final del Procesamiento ---")
|
||||
print(f"Total de archivos XML encontrados: {len(xml_files_found)}")
|
||||
print(
|
||||
f"Archivos procesados exitosamente por el pipeline completo: {processed_count}"
|
||||
)
|
||||
print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}")
|
||||
print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}")
|
||||
print("---------------------------------------")
|
||||
xml_filename = None
|
||||
|
||||
# Comprobar si se pasó un argumento de línea de comandos
|
||||
# sys.argv[0] es el nombre del script, sys.argv[1] sería el primer argumento
|
||||
if len(sys.argv) > 1:
|
||||
# Si hay argumentos, usar argparse para parsearlo (permite -h, etc.)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the Simatic XML processing pipeline."
|
||||
)
|
||||
parser.add_argument(
|
||||
"xml_file",
|
||||
# Ya no necesitamos nargs='?' ni default aquí porque sabemos que hay un argumento
|
||||
help="Path to the XML file to process.",
|
||||
)
|
||||
# Parsear solo los argumentos conocidos, ignorar extras si los hubiera
|
||||
args, unknown = parser.parse_known_args()
|
||||
xml_filename = args.xml_file
|
||||
print(f"XML file specified via argument: {xml_filename}")
|
||||
else:
|
||||
# Si no hay argumentos, llamar a la función interactiva
|
||||
xml_filename = select_xml_file()
|
||||
|
||||
# --- El resto del script continúa igual, usando xml_filename ---
|
||||
|
||||
# Verificar si el archivo XML de entrada (seleccionado o pasado) existe
|
||||
if not os.path.exists(xml_filename):
|
||||
print(f"Error: Selected or specified XML file not found: {xml_filename}")
|
||||
# Salir con código 0 si todo fue bien, 1 si hubo fallos
|
||||
if failed_count > 0:
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nStarting pipeline for: {xml_filename}")
|
||||
|
||||
# Run scripts sequentially (asegúrate que los nombres son correctos)
|
||||
script1 = "x1_to_json.py"
|
||||
script2 = "x2_process.py"
|
||||
script3 = "x3_generate_scl.py"
|
||||
|
||||
if run_script(script1, xml_filename):
|
||||
if run_script(script2, xml_filename):
|
||||
if run_script(script3, xml_filename):
|
||||
print("\nPipeline completed successfully.")
|
||||
else:
|
||||
print("\nPipeline failed at script:", script3)
|
||||
else:
|
||||
print("\nPipeline failed at script:", script2)
|
||||
else:
|
||||
print("\nPipeline failed at script:", script1)
|
||||
sys.exit(0)
|
||||
|
||||
# --- FIN: Se elimina la lógica redundante que venía después del bucle ---
|
File diff suppressed because it is too large
Load Diff
|
@ -18,17 +18,14 @@ from processors.processor_utils import (
|
|||
from processors.symbol_manager import SymbolManager # Import the manager
|
||||
|
||||
# --- Constantes y Configuración ---
|
||||
# SCL_SUFFIX = "_scl" # Old suffix
|
||||
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
|
||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
|
||||
|
||||
# Global data dictionary (consider passing 'data' as argument if needed elsewhere)
|
||||
# It's currently used by process_group_ifs implicitly via the outer scope,
|
||||
# which works but passing it explicitly might be cleaner.
|
||||
# Global data dictionary
|
||||
data = {}
|
||||
|
||||
|
||||
# --- (Incluye aquí las funciones process_group_ifs y load_processors SIN CAMBIOS) ---
|
||||
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||
"""
|
||||
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
||||
|
@ -112,6 +109,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
|||
# SCoil/RCoil might also be groupable if their SCL is final assignment
|
||||
"SCoil",
|
||||
"RCoil",
|
||||
"BLKMOV", # Added BLKMOV
|
||||
"TON", "TOF", "TP", "Se", "Sd", # Added timers
|
||||
"CTU", "CTD", "CTUD", # Added counters
|
||||
]
|
||||
|
||||
for consumer_instr in network_logic:
|
||||
|
@ -135,26 +135,26 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
|||
is_enabled_by_us = True
|
||||
|
||||
# Check if consumer is groupable AND has its final SCL generated
|
||||
# The suffix check needs adjustment based on how terminating processors set it.
|
||||
# Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix.
|
||||
if (
|
||||
is_enabled_by_us
|
||||
and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix
|
||||
and consumer_type.endswith(SCL_SUFFIX) # Check if processed
|
||||
and consumer_type_original in groupable_types
|
||||
):
|
||||
|
||||
consumer_scl = consumer_instr.get("scl", "")
|
||||
# Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now)
|
||||
# Extract core SCL
|
||||
core_scl = None
|
||||
if consumer_scl:
|
||||
# If consumer SCL itself is an IF generated by EN, take the body
|
||||
if consumer_scl.strip().startswith("IF"):
|
||||
match = re.search(
|
||||
r"THEN\s*(.*?)\s*END_IF;",
|
||||
r"IF\s+.*?THEN\s*(.*?)\s*END_IF;", # More robust regex
|
||||
consumer_scl,
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
core_scl = match.group(1).strip() if match else None
|
||||
# If body contains another IF, maybe don't group? (optional complexity)
|
||||
# if core_scl and core_scl.strip().startswith("IF"): core_scl = None
|
||||
elif not consumer_scl.strip().startswith(
|
||||
"//"
|
||||
): # Otherwise, take the whole line if not comment
|
||||
|
@ -300,8 +300,7 @@ def load_processors(processors_dir="processors"):
|
|||
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
||||
return processor_map, processor_list_sorted
|
||||
|
||||
|
||||
# --- Bucle Principal de Procesamiento (Modificado para STL) ---
|
||||
# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) ---
|
||||
def process_json_to_scl(json_filepath):
|
||||
"""
|
||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
||||
|
@ -321,15 +320,14 @@ def process_json_to_scl(json_filepath):
|
|||
traceback.print_exc()
|
||||
return
|
||||
|
||||
# --- Obtener lenguaje del bloque principal ---
|
||||
block_language = data.get("language", "Unknown")
|
||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
|
||||
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}")
|
||||
# --- 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')}")
|
||||
|
||||
# --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO ---
|
||||
if block_language == "DB":
|
||||
# --- 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 (DB). Saltando procesamiento lógico de x2."
|
||||
"INFO: El bloque es un Data Block (GlobalDB). Saltando procesamiento lógico de x2."
|
||||
)
|
||||
# Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo)
|
||||
output_filename = json_filepath.replace(
|
||||
|
@ -345,8 +343,8 @@ def process_json_to_scl(json_filepath):
|
|||
traceback.print_exc()
|
||||
return # <<< SALIR TEMPRANO PARA DBs
|
||||
|
||||
# --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) ---
|
||||
print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...")
|
||||
# --- 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
|
||||
|
||||
script_dir = os.path.dirname(__file__)
|
||||
processors_dir_path = os.path.join(script_dir, "processors")
|
||||
|
@ -391,7 +389,7 @@ def process_json_to_scl(json_filepath):
|
|||
passes = 0
|
||||
processing_complete = False
|
||||
|
||||
print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---")
|
||||
print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---") # <-- Mensaje actualizado
|
||||
while passes < max_passes and not processing_complete:
|
||||
passes += 1
|
||||
made_change_in_base_pass = False
|
||||
|
@ -408,34 +406,44 @@ def process_json_to_scl(json_filepath):
|
|||
func_to_call = processor_info["func"]
|
||||
for network in data.get("networks", []):
|
||||
network_id = network["id"]
|
||||
network_lang = network.get("language", "LAD")
|
||||
if network_lang == "STL":
|
||||
continue # Saltar STL
|
||||
network_lang = network.get("language", "LAD") # Lenguaje de la red
|
||||
if network_lang == "STL": # Saltar redes 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")
|
||||
instr_type_original = instruction.get("type", "Unknown")
|
||||
# Usar el tipo *actual* de la instrucción para el lookup
|
||||
instr_type_current = instruction.get("type", "Unknown")
|
||||
|
||||
# Saltar si ya está procesado, es error, agrupado, o tipo crudo
|
||||
if (
|
||||
instr_type_original.endswith(SCL_SUFFIX)
|
||||
or "_error" in instr_type_original
|
||||
instr_type_current.endswith(SCL_SUFFIX)
|
||||
or "_error" in instr_type_current
|
||||
or instruction.get("grouped", False)
|
||||
or instr_type_original
|
||||
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]
|
||||
or instr_type_current
|
||||
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]
|
||||
):
|
||||
continue
|
||||
lookup_key = instr_type_original.lower()
|
||||
effective_type_name = lookup_key
|
||||
if instr_type_original == "Call":
|
||||
block_type = instruction.get("block_type", "").upper()
|
||||
if block_type == "FC":
|
||||
effective_type_name = "call_fc"
|
||||
elif block_type == "FB":
|
||||
effective_type_name = "call_fb"
|
||||
|
||||
# El lookup usa el tipo actual (que aún no tiene el sufijo)
|
||||
lookup_key = instr_type_current.lower()
|
||||
effective_type_name = lookup_key
|
||||
|
||||
# Mapeo especial para llamadas FC/FB
|
||||
if instr_type_current == "Call":
|
||||
call_block_type = instruction.get("block_type", "").upper()
|
||||
if call_block_type == "FC":
|
||||
effective_type_name = "call_fc"
|
||||
elif call_block_type == "FB":
|
||||
effective_type_name = "call_fb"
|
||||
# Añadir otros tipos de llamada si es necesario
|
||||
|
||||
# Si el tipo efectivo coincide con el procesador actual
|
||||
if effective_type_name == current_type_name:
|
||||
try:
|
||||
# Pasar 'data' a la función del procesador
|
||||
changed = func_to_call(
|
||||
instruction, network_id, sympy_map, symbol_manager, data
|
||||
)
|
||||
|
@ -444,22 +452,24 @@ def process_json_to_scl(json_filepath):
|
|||
num_sympy_processed_this_pass += 1
|
||||
except Exception as e:
|
||||
print(
|
||||
f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}"
|
||||
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
|
||||
)
|
||||
traceback.print_exc()
|
||||
instruction["scl"] = (
|
||||
f"// ERROR en SymPy procesador base: {e}"
|
||||
)
|
||||
instruction["type"] = instr_type_original + "_error"
|
||||
made_change_in_base_pass = True
|
||||
# Añadir sufijo de error al tipo actual
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
# --- FASE 2: Agrupación IF (Ignorando STL) ---
|
||||
if (
|
||||
made_change_in_base_pass or passes == 1
|
||||
): # Ejecutar siempre en el primer pase
|
||||
): # Ejecutar siempre en el primer pase o si hubo cambios
|
||||
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
||||
num_grouped_this_pass = 0 # Resetear contador para el pase
|
||||
for network in data.get("networks", []):
|
||||
|
@ -468,19 +478,30 @@ def process_json_to_scl(json_filepath):
|
|||
if network_lang == "STL":
|
||||
continue # Saltar STL
|
||||
network_logic = network.get("logic", [])
|
||||
for instruction in network_logic:
|
||||
try:
|
||||
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}"
|
||||
)
|
||||
traceback.print_exc()
|
||||
# Iterar en orden por UID puede ser más estable para agrupación
|
||||
uids_in_network = sorted([instr.get("instruction_uid", "Z") for instr in network_logic if instr.get("instruction_uid")])
|
||||
for uid_to_process in uids_in_network:
|
||||
instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None)
|
||||
if not instruction: continue
|
||||
|
||||
# Saltar si ya está agrupada, es error, etc.
|
||||
if instruction.get("grouped") or "_error" in instruction.get("type", ""):
|
||||
continue
|
||||
# La agrupación sólo aplica a instrucciones que generan condiciones booleanas
|
||||
# y que ya fueron procesadas (tienen el sufijo)
|
||||
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
||||
try:
|
||||
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}"
|
||||
)
|
||||
traceback.print_exc()
|
||||
print(
|
||||
f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)."
|
||||
)
|
||||
|
@ -503,14 +524,16 @@ def process_json_to_scl(json_filepath):
|
|||
# --- FIN BUCLE ITERATIVO ---
|
||||
|
||||
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
|
||||
print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---")
|
||||
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
|
||||
unprocessed_count = 0
|
||||
unprocessed_details = []
|
||||
ignored_types = [
|
||||
"raw_scl_chunk",
|
||||
"unsupported_lang",
|
||||
"raw_stl_chunk",
|
||||
] # Añadido raw_stl_chunk
|
||||
"unsupported_content", # Añadido de x1
|
||||
"parsing_error", # Añadido de x1
|
||||
]
|
||||
for network in data.get("networks", []):
|
||||
network_id = network.get("id", "Unknown ID")
|
||||
network_title = network.get("title", f"Network {network_id}")
|
||||
|
@ -547,7 +570,7 @@ def process_json_to_scl(json_filepath):
|
|||
output_filename = json_filepath.replace(
|
||||
"_simplified.json", "_simplified_processed.json"
|
||||
)
|
||||
print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}")
|
||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
|
||||
try:
|
||||
with open(output_filename, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
@ -557,7 +580,7 @@ def process_json_to_scl(json_filepath):
|
|||
traceback.print_exc()
|
||||
|
||||
|
||||
# --- Ejecución (sin cambios) ---
|
||||
# --- Ejecución (sin cambios en esta parte) ---
|
||||
if __name__ == "__main__":
|
||||
# Imports necesarios solo para la ejecución como script principal
|
||||
import argparse
|
||||
|
@ -577,12 +600,10 @@ if __name__ == "__main__":
|
|||
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)
|
||||
# No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar
|
||||
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."
|
||||
)
|
||||
# No salir necesariamente, pero es bueno saberlo.
|
||||
|
||||
# Derivar nombre del archivo JSON de entrada (_simplified.json)
|
||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||
|
@ -610,14 +631,13 @@ if __name__ == "__main__":
|
|||
sys.exit(1) # Salir si el archivo necesario no está
|
||||
else:
|
||||
# Llamar a la función principal de procesamiento del script
|
||||
# Asumiendo que tu función principal se llama process_json_to_scl(input_json_path)
|
||||
try:
|
||||
process_json_to_scl(input_json_file)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
|
||||
)
|
||||
import traceback
|
||||
import traceback # Asegurar que traceback está importado
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1) # Salir con error si la función principal falla
|
||||
sys.exit(1) # Salir con error si la función principal falla
|
|
@ -46,24 +46,23 @@ except ImportError:
|
|||
# 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
|
||||
return None # Retornar None si no hay valor
|
||||
datatype_lower = datatype.lower() if datatype else ""
|
||||
value_str = str(value)
|
||||
|
||||
if "bool" in datatype_lower:
|
||||
return "TRUE" if value_str.lower() == "true" else "FALSE"
|
||||
elif "string" in datatype_lower:
|
||||
escaped_value = value_str.replace("'", "''")
|
||||
if escaped_value.startswith("'") and escaped_value.endswith("'"):
|
||||
escaped_value = escaped_value[1:-1]
|
||||
return f"'{escaped_value}'"
|
||||
elif "char" in datatype_lower: # Añadido Char
|
||||
escaped_value = value_str.replace("'", "''")
|
||||
if escaped_value.startswith("'") and escaped_value.endswith("'"):
|
||||
escaped_value = escaped_value[1:-1]
|
||||
return f"'{escaped_value}'"
|
||||
elif any(
|
||||
# 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",
|
||||
|
@ -79,72 +78,169 @@ def format_scl_start_value(value, datatype):
|
|||
"udint",
|
||||
"ulint",
|
||||
]
|
||||
): # Ampliado
|
||||
):
|
||||
try:
|
||||
return str(int(value_str))
|
||||
# Intentar convertir el valor (sin comillas) a entero
|
||||
return str(int(value_str_unquoted))
|
||||
except ValueError:
|
||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str):
|
||||
return value_str
|
||||
return f"'{value_str}'" # O como string si no es entero ni símbolo
|
||||
# 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:
|
||||
f_val = float(value_str)
|
||||
# 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:
|
||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str):
|
||||
return value_str
|
||||
return f"'{value_str}'"
|
||||
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime
|
||||
# Quitar T#, LT#, S5T# si existen
|
||||
# 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 = ""
|
||||
if value_str.upper().startswith("T#"):
|
||||
val_to_use = value_str_unquoted # Usar valor sin comillas
|
||||
if val_to_use.upper().startswith("T#"):
|
||||
prefix = "T#"
|
||||
value_str = value_str[2:]
|
||||
elif value_str.upper().startswith("LT#"):
|
||||
val_to_use = val_to_use[2:]
|
||||
elif val_to_use.upper().startswith("LT#"):
|
||||
prefix = "LT#"
|
||||
value_str = value_str[3:]
|
||||
elif value_str.upper().startswith("S5T#"):
|
||||
val_to_use = val_to_use[3:]
|
||||
elif val_to_use.upper().startswith("S5T#"):
|
||||
prefix = "S5T#"
|
||||
value_str = value_str[4:]
|
||||
# Devolver con el prefijo correcto o T# por defecto si no había
|
||||
if prefix:
|
||||
return f"{prefix}{value_str}"
|
||||
elif "s5time" in datatype_lower:
|
||||
return f"S5T#{value_str}"
|
||||
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#{value_str}"
|
||||
return f"LT#{val_to_use}"
|
||||
else:
|
||||
return f"T#{value_str}" # Default a TIME
|
||||
elif "date" in datatype_lower: # Añadido Date, DT, TOD
|
||||
if value_str.upper().startswith("D#"):
|
||||
return value_str
|
||||
elif "dt" in datatype_lower or "date_and_time" in datatype_lower:
|
||||
if value_str.upper().startswith("DT#"):
|
||||
return value_str
|
||||
else:
|
||||
return f"DT#{value_str}" # Añadir prefijo DT#
|
||||
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:
|
||||
if value_str.upper().startswith("TOD#"):
|
||||
return value_str
|
||||
else:
|
||||
return f"TOD#{value_str}" # Añadir prefijo TOD#
|
||||
else:
|
||||
return f"D#{value_str}" # Default a Date
|
||||
# Fallback genérico
|
||||
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 más caracteres en símbolos/tipos
|
||||
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas
|
||||
if value_str.startswith('"') and value_str.endswith('"'):
|
||||
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:
|
||||
escaped_value = value_str.replace("'", "''")
|
||||
return f"'{escaped_value}'"
|
||||
# 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) ---
|
||||
|
@ -155,87 +251,132 @@ def generate_scl_declarations(variables, indent_level=1):
|
|||
for var in variables:
|
||||
var_name_scl = format_variable_name(var.get("name"))
|
||||
var_dtype_raw = var.get("datatype", "VARIANT")
|
||||
# Limpiar comillas de tipos de datos UDT ("MyType" -> MyType)
|
||||
var_dtype = (
|
||||
var_dtype_raw.strip('"')
|
||||
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"')
|
||||
else var_dtype_raw
|
||||
)
|
||||
|
||||
var_comment = var.get("comment")
|
||||
start_value = var.get("start_value")
|
||||
children = var.get("children") # Para structs
|
||||
array_elements = var.get("array_elements") # Para arrays
|
||||
|
||||
# Manejar tipos de datos Array especiales
|
||||
array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE)
|
||||
base_type_for_init = var_dtype
|
||||
declaration_dtype = var_dtype
|
||||
if array_match:
|
||||
array_prefix = array_match.group(1)
|
||||
base_type_raw = array_match.group(2).strip()
|
||||
# Limpiar comillas del tipo base del array
|
||||
base_type_for_init = (
|
||||
base_type_raw.strip('"')
|
||||
if base_type_raw.startswith('"') and base_type_raw.endswith('"')
|
||||
else base_type_raw
|
||||
# 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
|
||||
)
|
||||
declaration_dtype = (
|
||||
f'{array_prefix}"{base_type_for_init}"'
|
||||
if '"' not in base_type_raw
|
||||
else f"{array_prefix}{base_type_raw}"
|
||||
) # Reconstruir con comillas si es UDT
|
||||
if array_match:
|
||||
var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
|
||||
|
||||
# Reconstruir declaración con comillas si es UDT y no array
|
||||
elif (
|
||||
not array_match and var_dtype != base_type_for_init
|
||||
): # Es un tipo que necesita comillas (UDT)
|
||||
declaration_dtype = f'"{var_dtype}"'
|
||||
# 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 = None
|
||||
init_value_scl = None
|
||||
|
||||
# ---- Arrays ----
|
||||
if array_elements:
|
||||
# Ordenar índices (asumiendo que son numéricos)
|
||||
# Ordenar índices (asumiendo que son numéricos '0', '1', ...)
|
||||
try:
|
||||
sorted_indices = sorted(array_elements.keys(), key=int)
|
||||
# Extraer números de los índices string
|
||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
||||
sorted_indices = sorted(indices_numeric.keys())
|
||||
# Mapear de nuevo a string para buscar valor
|
||||
sorted_indices_str = [str(k) for k in sorted_indices]
|
||||
except ValueError:
|
||||
sorted_indices = sorted(
|
||||
array_elements.keys()
|
||||
) # Fallback a orden alfabético
|
||||
# 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 = [
|
||||
format_scl_start_value(array_elements[idx], base_type_for_init)
|
||||
for idx in sorted_indices
|
||||
]
|
||||
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:
|
||||
init_value = f"[{', '.join(valid_inits)}]"
|
||||
# Si todos los valores son iguales y es un array grande, podríamos usar notación x(value)
|
||||
# Simplificación: por ahora, listar todos
|
||||
init_value_scl = f"[{', '.join(valid_inits)}]"
|
||||
elif array_elements: # Si había elementos pero todos formatearon a None
|
||||
print(
|
||||
f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos."
|
||||
)
|
||||
|
||||
# ---- Structs ----
|
||||
elif children:
|
||||
# No añadir comentario // Struct aquí, es redundante
|
||||
scl_lines.append(declaration_line) # Añadir línea de declaración base
|
||||
# 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:
|
||||
if var_comment: # Comentario después de END_STRUCT
|
||||
scl_lines.append(f"{indent}// {var_comment}")
|
||||
scl_lines.append("") # Línea extra
|
||||
continue # Saltar resto para Struct
|
||||
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:
|
||||
init_value = format_scl_start_value(start_value, var_dtype)
|
||||
try:
|
||||
init_value_scl = format_scl_start_value(
|
||||
start_value, base_type_for_init
|
||||
) # Usar tipo base
|
||||
except Exception as e_fmt_simple:
|
||||
print(
|
||||
f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"
|
||||
)
|
||||
init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder
|
||||
|
||||
# Añadir inicialización si existe y no es None
|
||||
if init_value_scl is not None:
|
||||
declaration_line += f" := {init_value_scl}"
|
||||
|
||||
# Añadir inicialización si existe
|
||||
if init_value:
|
||||
declaration_line += f" := {init_value}"
|
||||
declaration_line += ";"
|
||||
|
||||
# Añadir comentario si existe
|
||||
if var_comment:
|
||||
declaration_line += f" // {var_comment}"
|
||||
|
||||
scl_lines.append(declaration_line)
|
||||
|
||||
return scl_lines
|
||||
|
@ -243,7 +384,7 @@ def generate_scl_declarations(variables, indent_level=1):
|
|||
|
||||
# --- 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 o DB)."""
|
||||
"""Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
|
||||
|
||||
if not os.path.exists(processed_json_filepath):
|
||||
print(
|
||||
|
@ -263,33 +404,41 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
# --- Extracción de Información del Bloque (Común) ---
|
||||
block_name = data.get("block_name", "UnknownBlock")
|
||||
block_number = data.get("block_number")
|
||||
block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks
|
||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
|
||||
# block_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}, Lang: {block_lang_original})"
|
||||
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
|
||||
)
|
||||
scl_output = []
|
||||
|
||||
# --- GENERACIÓN PARA DATA BLOCK (DB) ---
|
||||
if block_lang_original == "DB":
|
||||
# --- 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:
|
||||
scl_output.append(f"// Block Comment: {block_comment}")
|
||||
# Dividir comentarios largos en múltiples líneas
|
||||
comment_lines = block_comment.splitlines()
|
||||
scl_output.append(f"// Block Comment:")
|
||||
for line in comment_lines:
|
||||
scl_output.append(f"// {line}")
|
||||
scl_output.append("")
|
||||
scl_output.append(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("")
|
||||
|
@ -297,182 +446,288 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
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("")
|
||||
scl_output.append(
|
||||
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
|
||||
)
|
||||
scl_output.append("END_DATA_BLOCK")
|
||||
|
||||
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) ---
|
||||
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
|
||||
else:
|
||||
print("Modo de generación: FUNCTION_BLOCK / FUNCTION")
|
||||
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION"
|
||||
# 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}")
|
||||
scl_output.append(f"// Original Language: {block_lang_original}")
|
||||
# Indicar lenguaje original de las redes si es relevante
|
||||
original_net_langs = set(
|
||||
n.get("language", "Unknown") for n in data.get("networks", [])
|
||||
)
|
||||
scl_output.append(
|
||||
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
|
||||
)
|
||||
if block_comment:
|
||||
scl_output.append(f"// Block Comment: {block_comment}")
|
||||
comment_lines = block_comment.splitlines()
|
||||
scl_output.append(f"// Block Comment:")
|
||||
for line in comment_lines:
|
||||
scl_output.append(f"// {line}")
|
||||
scl_output.append("")
|
||||
# Manejar tipo de retorno para FUNCTION
|
||||
|
||||
# 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"):
|
||||
return_member = interface_data["Return"][
|
||||
0
|
||||
] # Asumir un solo valor de retorno
|
||||
# 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.strip('"')
|
||||
if return_type_raw.startswith('"') and return_type_raw.endswith('"')
|
||||
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
|
||||
if return_type != 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
|
||||
|
||||
scl_output.append(
|
||||
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
|
||||
if scl_block_keyword == "FUNCTION"
|
||||
else f'{scl_block_keyword} "{scl_block_name}"'
|
||||
)
|
||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||
# 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 FC/FB
|
||||
section_order = [
|
||||
"Input",
|
||||
"Output",
|
||||
"InOut",
|
||||
"Static",
|
||||
"Temp",
|
||||
"Constant",
|
||||
] # Return ya está en cabecera
|
||||
declared_temps = set()
|
||||
# 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"
|
||||
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_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")
|
||||
)
|
||||
scl_output.append("END_VAR")
|
||||
scl_output.append("")
|
||||
|
||||
# Declaraciones VAR_TEMP adicionales detectadas
|
||||
temp_vars = set()
|
||||
# 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'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?'
|
||||
)
|
||||
r'"?(#\w+)"?'
|
||||
) # Busca # seguido de caracteres alfanuméricos
|
||||
|
||||
for network in data.get("networks", []):
|
||||
for 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", "")
|
||||
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_tuple in found_temps:
|
||||
temp_name = next((t for t in temp_tuple if t), None)
|
||||
for temp_name in found_temps:
|
||||
# findall devuelve el grupo capturado (#...)
|
||||
if temp_name:
|
||||
temp_vars.add(
|
||||
"#" + temp_name
|
||||
if not temp_name.startswith("#")
|
||||
else temp_name
|
||||
)
|
||||
additional_temps = sorted(list(temp_vars - declared_temps))
|
||||
temp_vars_detected.add(temp_name)
|
||||
|
||||
# Filtrar las que ya estaban declaradas
|
||||
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||
|
||||
if additional_temps:
|
||||
if not interface_data.get("Temp"):
|
||||
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
||||
# Si no se declaró la sección Temp antes, añadirla ahora
|
||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
||||
scl_output.append("VAR_TEMP")
|
||||
for var_name in additional_temps:
|
||||
scl_name = format_variable_name(var_name)
|
||||
inferred_type = "Bool" # Asumir Bool
|
||||
|
||||
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"
|
||||
)
|
||||
if not interface_data.get("Temp"):
|
||||
|
||||
# Si abrimos la sección aquí, cerrarla
|
||||
if "Temp" not in interface_data or not interface_data["Temp"]:
|
||||
scl_output.append("END_VAR")
|
||||
scl_output.append("")
|
||||
|
||||
# Cuerpo del Bloque FC/FB
|
||||
# --- Cuerpo del Bloque (BEGIN...END) ---
|
||||
scl_output.append("BEGIN")
|
||||
scl_output.append("")
|
||||
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown)
|
||||
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
|
||||
for i, network in enumerate(data.get("networks", [])):
|
||||
network_title = network.get("title", f'Network {network.get("id")}')
|
||||
network_title = network.get(
|
||||
"title", f'Network {network.get("id", i+1)}'
|
||||
) # Usar i+1 si falta ID
|
||||
network_comment = network.get("comment", "")
|
||||
network_lang = network.get("language", "LAD")
|
||||
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("")
|
||||
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":
|
||||
network_has_code = True
|
||||
if (
|
||||
network.get("logic")
|
||||
and network["logic"][0].get("type") == "RAW_STL_CHUNK"
|
||||
):
|
||||
raw_stl_code = network["logic"][0].get(
|
||||
# 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"
|
||||
)
|
||||
scl_output.append(f" {'//'} ```STL")
|
||||
# Incrustar STL como comentario multi-línea o delimitado
|
||||
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
|
||||
# Comentar cada línea STL
|
||||
for stl_line in raw_stl_code.splitlines():
|
||||
scl_output.append(f" {stl_line}")
|
||||
scl_output.append(f" {'//'} ```")
|
||||
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(" // ERROR: Contenido STL inesperado.")
|
||||
else: # LAD, FBD, SCL, etc.
|
||||
for instruction in network.get("logic", []):
|
||||
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"]
|
||||
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")
|
||||
if not is_only_comment or is_if_block:
|
||||
|
||||
# Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error)
|
||||
if (
|
||||
not is_only_comment
|
||||
or is_if_block
|
||||
or "_error" in instruction_type
|
||||
or instruction_type
|
||||
in [
|
||||
"UNSUPPORTED_LANG",
|
||||
"UNSUPPORTED_CONTENT",
|
||||
"PARSING_ERROR",
|
||||
]
|
||||
):
|
||||
network_has_code = True
|
||||
for line in scl_code.splitlines():
|
||||
scl_output.append(f" {line}")
|
||||
if network_has_code:
|
||||
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("")
|
||||
else:
|
||||
scl_output.append(f" // Network did not produce printable SCL code.")
|
||||
scl_output.append("")
|
||||
# Fin del bloque FC/FB
|
||||
scl_output.append(f"END_{scl_block_keyword}")
|
||||
|
||||
# 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}")
|
||||
|
@ -492,7 +747,7 @@ if __name__ == "__main__":
|
|||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import traceback # Asegurarse que traceback está importado si se usa en generate_scl
|
||||
import traceback # Asegurarse que traceback está importado
|
||||
|
||||
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
|
||||
parser = argparse.ArgumentParser(
|
||||
|
@ -511,7 +766,6 @@ if __name__ == "__main__":
|
|||
print(
|
||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
||||
)
|
||||
# No salir necesariamente.
|
||||
|
||||
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||
|
@ -521,8 +775,9 @@ if __name__ == "__main__":
|
|||
input_json_file = os.path.join(
|
||||
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}_simplified_processed.scl"
|
||||
base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
|
||||
)
|
||||
|
||||
print(
|
||||
|
@ -540,13 +795,13 @@ if __name__ == "__main__":
|
|||
sys.exit(1) # Salir si el archivo necesario no está
|
||||
else:
|
||||
# Llamar a la función principal de generación SCL del script
|
||||
# Asumiendo que tu función principal se llama generate_scl(input_json_path, output_scl_path)
|
||||
try:
|
||||
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 si generate_scl lo necesita
|
||||
# traceback ya debería estar importado
|
||||
traceback.print_exc()
|
||||
sys.exit(1) # Salir con error si la función principal falla
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
|
||||
# Directorio donde se crearán los archivos de procesador
|
||||
PROCESSORS_DIR = "processors"
|
||||
|
||||
# Cabecera estándar para añadir a cada nuevo archivo
|
||||
FILE_HEADER = """# -*- coding: utf-8 -*-
|
||||
|
||||
# TODO: Import necessary functions from processor_utils
|
||||
# Example: from .processor_utils import get_scl_representation, format_variable_name
|
||||
# Or: import processors.processor_utils as utils
|
||||
|
||||
# TODO: Define constants if needed (e.g., SCL_SUFFIX) or import them
|
||||
SCL_SUFFIX = "_scl"
|
||||
|
||||
# --- Function code starts ---
|
||||
"""
|
||||
|
||||
# Pie de página estándar con la función get_processor_info de plantilla
|
||||
def get_file_footer(func_name):
|
||||
"""Generates the standard footer with a placeholder get_processor_info."""
|
||||
type_name_guess = func_name.replace('process_', '')
|
||||
return f"""
|
||||
# --- Function code ends ---
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
\"\"\"Returns the type name and processing function for this module.\"\"\"
|
||||
# TODO: Adjust the type_name if needed (e.g., call, edge_detector, comparison, math).
|
||||
# TODO: Return a list if this module handles multiple types (e.g., PBox/NBox, FC/FB).
|
||||
type_name = "{type_name_guess}" # Basic guess
|
||||
return {{'type_name': type_name, 'processor_func': {func_name}}}
|
||||
"""
|
||||
|
||||
def extract_and_create_processors(source_py_file):
|
||||
"""
|
||||
Extracts top-level functions starting with 'process_' from the source file
|
||||
and creates individual processor files in the PROCESSORS_DIR, copying
|
||||
the entire function body until the next top-level definition.
|
||||
"""
|
||||
if not os.path.exists(source_py_file):
|
||||
print(f"Error: Source file not found: '{source_py_file}'")
|
||||
return
|
||||
|
||||
print(f"Reading source file: '{source_py_file}'")
|
||||
try:
|
||||
with open(source_py_file, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
except Exception as e:
|
||||
print(f"Error reading source file: {e}")
|
||||
return
|
||||
|
||||
os.makedirs(PROCESSORS_DIR, exist_ok=True)
|
||||
print(f"Ensuring '{PROCESSORS_DIR}' directory exists.")
|
||||
print("Searching for processor functions (def process_...):")
|
||||
|
||||
processor_functions = [] # Store tuples of (name, start_line_index, end_line_index)
|
||||
current_func_start = -1
|
||||
current_func_name = None
|
||||
|
||||
# Pattern to find ANY top-level function definition
|
||||
any_func_def_pattern = re.compile(r"^def\s+(\w+)\s*\(")
|
||||
# Pattern specific to processor functions
|
||||
process_func_def_pattern = re.compile(r"^def\s+(process_\w+)\s*\(")
|
||||
|
||||
# First pass: Identify start and end lines of all top-level functions
|
||||
for i, line in enumerate(lines):
|
||||
match = any_func_def_pattern.match(line)
|
||||
if match:
|
||||
# Found a new top-level function definition
|
||||
if current_func_name is not None:
|
||||
# Mark the end of the *previous* function
|
||||
# Only add if it was a 'process_' function
|
||||
if current_func_name.startswith("process_"):
|
||||
processor_functions.append((current_func_name, current_func_start, i))
|
||||
|
||||
# Start tracking the new function
|
||||
current_func_name = match.group(1)
|
||||
current_func_start = i
|
||||
|
||||
# Add the last function found in the file (if it was a process_ function)
|
||||
if current_func_name is not None and current_func_name.startswith("process_"):
|
||||
processor_functions.append((current_func_name, current_func_start, len(lines)))
|
||||
|
||||
# Second pass: Create files using the identified line ranges
|
||||
processor_count = 0
|
||||
if not processor_functions:
|
||||
print("\nWarning: No functions starting with 'process_' found at the top level.")
|
||||
return
|
||||
|
||||
print(f"Found {len(processor_functions)} potential processor functions.")
|
||||
|
||||
for func_name, start_idx, end_idx in processor_functions:
|
||||
print(f" - Processing: {func_name} (lines {start_idx+1}-{end_idx})")
|
||||
func_lines = lines[start_idx:end_idx] # Extract lines for this function
|
||||
# Remove trailing blank lines from the extracted block, often happens before next def
|
||||
while func_lines and func_lines[-1].strip() == "":
|
||||
func_lines.pop()
|
||||
|
||||
create_processor_file(func_name, func_lines)
|
||||
processor_count += 1
|
||||
|
||||
print(f"\nFinished processing. Attempted to create/check {processor_count} processor files in '{PROCESSORS_DIR}'.")
|
||||
|
||||
|
||||
def create_processor_file(func_name, func_lines):
|
||||
"""Creates the individual processor file if it doesn't exist."""
|
||||
target_filename = f"{func_name}.py"
|
||||
target_filepath = os.path.join(PROCESSORS_DIR, target_filename)
|
||||
|
||||
if os.path.exists(target_filepath):
|
||||
print(f" * Skipping: '{target_filename}' already exists.")
|
||||
return
|
||||
|
||||
print(f" * Creating: '{target_filename}'...")
|
||||
try:
|
||||
with open(target_filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(FILE_HEADER)
|
||||
# Write the function lines, ensuring consistent newline endings
|
||||
for line in func_lines:
|
||||
f.write(line.rstrip() + '\n')
|
||||
f.write(get_file_footer(func_name))
|
||||
except Exception as e:
|
||||
print(f" Error writing file '{target_filename}': {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extracts 'process_*' functions from a source Python file "
|
||||
"and creates individual processor files."
|
||||
)
|
||||
parser.add_argument(
|
||||
"source_file",
|
||||
default="x2_process.py", # Valor por defecto
|
||||
nargs='?', # Hacerlo opcional para que use el default
|
||||
help="Path to the source Python file (default: x2_process.py)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
extract_and_create_processors(args.source_file)
|
|
@ -0,0 +1,548 @@
|
|||
# ToUpload/parsers/parse_lad_fbd.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import (
|
||||
ns,
|
||||
parse_access,
|
||||
parse_part,
|
||||
parse_call,
|
||||
get_multilingual_text,
|
||||
)
|
||||
|
||||
# Sufijo usado en x2 para identificar instrucciones procesadas (útil para EN/ENO)
|
||||
SCL_SUFFIX = "_sympy_processed" # Asumimos que este es el sufijo de x2
|
||||
|
||||
|
||||
def parse_lad_fbd_network(network_element):
|
||||
"""
|
||||
Parsea una red LAD/FBD/GRAPH, extrae lógica y añade conexiones EN/ENO implícitas.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
if network_element is None:
|
||||
return {
|
||||
"id": "ERROR",
|
||||
"title": "Invalid Network Element",
|
||||
"logic": [],
|
||||
"error": "Input element was None",
|
||||
}
|
||||
|
||||
network_id = network_element.get("ID")
|
||||
# Usar get_multilingual_text de utils
|
||||
title_element = network_element.xpath(
|
||||
".//iface:MultilingualText[@CompositionName='Title']", namespaces=ns
|
||||
)
|
||||
network_title = (
|
||||
get_multilingual_text(title_element[0])
|
||||
if title_element
|
||||
else f"Network {network_id}"
|
||||
)
|
||||
comment_element = network_element.xpath(
|
||||
"./ObjectList/MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # OJO: Path relativo a CompileUnit?
|
||||
if not comment_element: # Intentar path alternativo si el anterior falla
|
||||
comment_element = network_element.xpath(
|
||||
".//MultilingualText[@CompositionName='Comment']", namespaces=ns
|
||||
) # Más genérico dentro de la red
|
||||
network_comment = (
|
||||
get_multilingual_text(comment_element[0]) if comment_element else ""
|
||||
)
|
||||
|
||||
# --- Determinar Lenguaje (ya que este parser maneja varios) ---
|
||||
network_lang = "Unknown"
|
||||
attr_list_net = network_element.xpath("./AttributeList")
|
||||
if attr_list_net:
|
||||
lang_node_net = attr_list_net[0].xpath("./ProgrammingLanguage/text()")
|
||||
if lang_node_net:
|
||||
network_lang = lang_node_net[0].strip()
|
||||
|
||||
# --- Buscar FlgNet ---
|
||||
# Buscar NetworkSource y luego FlgNet (ambos usan namespace flg)
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
flgnet = None
|
||||
if network_source_node:
|
||||
flgnet_list = network_source_node[0].xpath("./flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
else: # Intentar buscar FlgNet directamente si no hay NetworkSource
|
||||
flgnet_list = network_element.xpath(".//flg:FlgNet", namespaces=ns)
|
||||
if flgnet_list:
|
||||
flgnet = flgnet_list[0]
|
||||
|
||||
if flgnet is None:
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang,
|
||||
"logic": [],
|
||||
"error": "FlgNet not found inside NetworkSource or CompileUnit",
|
||||
}
|
||||
|
||||
# 1. Parse Access, Parts, Calls (usan utils)
|
||||
access_map = {}
|
||||
# Corregir XPath para buscar Access dentro de FlgNet/Parts
|
||||
for acc in flgnet.xpath(".//flg:Parts/flg:Access", namespaces=ns):
|
||||
acc_info = parse_access(acc)
|
||||
if acc_info and acc_info.get("uid") and "error" not in acc_info.get("type", ""):
|
||||
access_map[acc_info["uid"]] = acc_info
|
||||
elif acc_info:
|
||||
print(
|
||||
f"Advertencia: Ignorando Access inválido o con error UID={acc_info.get('uid')} en red {network_id}"
|
||||
)
|
||||
|
||||
parts_and_calls_map = {}
|
||||
# Corregir XPath para buscar Part y Call dentro de FlgNet/Parts
|
||||
instruction_elements = flgnet.xpath(
|
||||
".//flg:Parts/flg:Part | .//flg:Parts/flg:Call", namespaces=ns
|
||||
)
|
||||
for element in instruction_elements:
|
||||
parsed_info = None
|
||||
tag_name = etree.QName(element.tag).localname
|
||||
if tag_name == "Part":
|
||||
parsed_info = parse_part(element) # Usa utils
|
||||
elif tag_name == "Call":
|
||||
parsed_info = parse_call(element) # Usa utils
|
||||
|
||||
if (
|
||||
parsed_info
|
||||
and parsed_info.get("uid")
|
||||
and "error" not in parsed_info.get("type", "")
|
||||
):
|
||||
parts_and_calls_map[parsed_info["uid"]] = parsed_info
|
||||
elif parsed_info:
|
||||
# Si parse_call/parse_part devolvió error, lo guardamos para tener el UID
|
||||
print(
|
||||
f"Advertencia: {tag_name} con error UID={parsed_info.get('uid')} en red {network_id}. Error: {parsed_info.get('error')}"
|
||||
)
|
||||
parts_and_calls_map[parsed_info["uid"]] = (
|
||||
parsed_info # Guardar aunque tenga error
|
||||
)
|
||||
|
||||
# 2. Parse Wires (lógica compleja, mantener aquí)
|
||||
wire_connections = defaultdict(list) # destination -> [source1, source2]
|
||||
source_connections = defaultdict(list) # source -> [dest1, dest2]
|
||||
eno_outputs = defaultdict(list)
|
||||
qname_powerrail = etree.QName(ns["flg"], "Powerrail")
|
||||
qname_identcon = etree.QName(
|
||||
ns["flg"], "IdentCon"
|
||||
) # Conexión a/desde Access (variable/constante)
|
||||
qname_namecon = etree.QName(
|
||||
ns["flg"], "NameCon"
|
||||
) # Conexión a/desde Part/Call (pin con nombre)
|
||||
qname_openbranch = etree.QName(
|
||||
ns["flg"], "Openbranch"
|
||||
) # Rama abierta (normalmente ignorada o tratada como TRUE?)
|
||||
qname_opencon = etree.QName(
|
||||
ns["flg"], "OpenCon"
|
||||
) # Conexión abierta (pin no conectado)
|
||||
|
||||
# Corregir XPath para buscar Wire dentro de FlgNet/Wires
|
||||
for wire in flgnet.xpath(".//flg:Wires/flg:Wire", namespaces=ns):
|
||||
children = wire.getchildren()
|
||||
if len(children) < 2:
|
||||
continue # Necesita al menos origen y destino
|
||||
|
||||
source_elem = children[0]
|
||||
source_uid, source_pin = None, None
|
||||
|
||||
# Determinar origen
|
||||
if source_elem.tag == qname_powerrail:
|
||||
source_uid, source_pin = "POWERRAIL", "out"
|
||||
elif source_elem.tag == qname_identcon: # Origen es una variable/constante
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = "value" # Salida implícita de un Access
|
||||
elif source_elem.tag == qname_namecon: # Origen es pin de instrucción
|
||||
source_uid = source_elem.get("UId")
|
||||
source_pin = source_elem.get("Name")
|
||||
elif source_elem.tag == qname_openbranch:
|
||||
# ¿Cómo manejar OpenBranch como fuente? Podría ser TRUE o una condición OR implícita
|
||||
source_uid = "OPENBRANCH_" + wire.get(
|
||||
"UId", "Unknown"
|
||||
) # UID único para la rama
|
||||
source_pin = "out"
|
||||
print(
|
||||
f"Advertencia: OpenBranch encontrado como fuente en Wire UID={wire.get('UId')} (Red {network_id}). Tratando como fuente especial."
|
||||
)
|
||||
# No lo añadimos a parts_and_calls_map, get_sympy_representation necesitará manejarlo
|
||||
# Ignorar OpenCon como fuente (no tiene sentido)
|
||||
if source_uid is None or source_pin is None:
|
||||
# print(f"Advertencia: Fuente de wire inválida o no soportada: {source_elem.tag} en Wire UID={wire.get('UId')}")
|
||||
continue
|
||||
|
||||
source_info = (source_uid, source_pin)
|
||||
|
||||
# Procesar destinos
|
||||
for dest_elem in children[1:]:
|
||||
dest_uid, dest_pin = None, None
|
||||
|
||||
if (
|
||||
dest_elem.tag == qname_identcon
|
||||
): # Destino es una variable/constante (asignación)
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = "value" # Entrada implícita de un Access
|
||||
elif dest_elem.tag == qname_namecon: # Destino es pin de instrucción
|
||||
dest_uid = dest_elem.get("UId")
|
||||
dest_pin = dest_elem.get("Name")
|
||||
# Ignorar Powerrail, OpenBranch, OpenCon como destinos válidos de conexión lógica principal
|
||||
|
||||
if dest_uid is not None and dest_pin is not None:
|
||||
dest_key = (dest_uid, dest_pin)
|
||||
if source_info not in wire_connections[dest_key]:
|
||||
wire_connections[dest_key].append(source_info)
|
||||
|
||||
# Mapa inverso: source -> list of destinations
|
||||
source_key = (source_uid, source_pin)
|
||||
dest_info = (dest_uid, dest_pin)
|
||||
if dest_info not in source_connections[source_key]:
|
||||
source_connections[source_key].append(dest_info)
|
||||
|
||||
# Trackear salidas ENO específicamente si la fuente es una instrucción
|
||||
if source_pin == "eno" and source_uid in parts_and_calls_map:
|
||||
if dest_info not in eno_outputs[source_uid]:
|
||||
eno_outputs[source_uid].append(dest_info)
|
||||
|
||||
# 3. Build Initial Logic Structure (incorporando errores)
|
||||
all_logic_steps = {}
|
||||
# Lista de tipos funcionales (usados para inferencia EN)
|
||||
# Estos son los tipos *originales* de las instrucciones
|
||||
functional_block_types = [
|
||||
"Move",
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Convert",
|
||||
"Call", # Call ya está aquí
|
||||
"TON",
|
||||
"TOF",
|
||||
"TP",
|
||||
"CTU",
|
||||
"CTD",
|
||||
"CTUD",
|
||||
"BLKMOV", # Añadidos
|
||||
"Se",
|
||||
"Sd", # Estos son tipos LAD que se mapearán a timers SCL
|
||||
]
|
||||
# Lista de generadores RLO (usados para inferencia EN)
|
||||
rlo_generators = [
|
||||
"Contact",
|
||||
"O",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
"And",
|
||||
"Xor",
|
||||
"PBox",
|
||||
"NBox",
|
||||
"Not",
|
||||
]
|
||||
|
||||
# Iterar sobre UIDs válidos (los que se pudieron parsear, aunque sea con error)
|
||||
valid_instruction_uids = list(parts_and_calls_map.keys())
|
||||
|
||||
for instruction_uid in valid_instruction_uids:
|
||||
instruction_info = parts_and_calls_map[instruction_uid]
|
||||
# Hacer copia profunda para no modificar el mapa original
|
||||
instruction_repr = copy.deepcopy(instruction_info)
|
||||
instruction_repr["instruction_uid"] = instruction_uid # Asegurar UID
|
||||
instruction_repr["inputs"] = {}
|
||||
instruction_repr["outputs"] = {}
|
||||
|
||||
# Si la instrucción ya tuvo un error de parseo, añadirlo aquí
|
||||
if "error" in instruction_info:
|
||||
instruction_repr["parsing_error"] = instruction_info["error"]
|
||||
# No intentar poblar inputs/outputs si el parseo base falló
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
continue
|
||||
|
||||
original_type = instruction_repr.get("type", "") # Tipo de la instrucción
|
||||
|
||||
# --- Poblar Entradas ---
|
||||
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
|
||||
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
|
||||
# Añadir pines dinámicamente basados en el tipo de instrucción
|
||||
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
|
||||
possible_input_pins.add("operand")
|
||||
elif original_type in [
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Mod",
|
||||
"Eq",
|
||||
"Ne",
|
||||
"Gt",
|
||||
"Lt",
|
||||
"Ge",
|
||||
"Le",
|
||||
]:
|
||||
possible_input_pins.update(["in1", "in2"])
|
||||
elif original_type in ["TON", "TOF", "TP"]:
|
||||
possible_input_pins.update(["IN", "PT"]) # Pines SCL
|
||||
elif original_type in ["Se", "Sd"]:
|
||||
possible_input_pins.update(["s", "tv", "timer"]) # Pines LAD
|
||||
elif original_type in ["CTU", "CTD", "CTUD"]:
|
||||
possible_input_pins.update(["CU", "CD", "R", "LD", "PV"]) # Pines SCL/LAD
|
||||
elif original_type in ["PBox", "NBox"]:
|
||||
possible_input_pins.update(
|
||||
["bit", "clk", "in"]
|
||||
) # PBox/NBox usa 'in' y 'bit'
|
||||
elif original_type == "BLKMOV":
|
||||
possible_input_pins.add("SRCBLK")
|
||||
elif original_type == "Move":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Convert":
|
||||
possible_input_pins.add("in")
|
||||
elif original_type == "Call":
|
||||
# Para Calls, los nombres de los parámetros reales se definen en el XML
|
||||
# El Xpath busca Parameter DENTRO de CallInfo, que está DENTRO de Call
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_xml_element = call_xml_element_list[0]
|
||||
call_info_node_list = call_xml_element.xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
call_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter/@Name", namespaces=ns
|
||||
)
|
||||
possible_input_pins.update(call_param_names)
|
||||
# print(f"DEBUG Call UID={instruction_uid}: Params={call_param_names}")
|
||||
else: # Fallback si no hay namespace (menos probable)
|
||||
call_info_node_list_no_ns = call_xml_element.xpath("./CallInfo")
|
||||
if call_info_node_list_no_ns:
|
||||
possible_input_pins.update(
|
||||
call_info_node_list_no_ns[0].xpath("./Parameter/@Name")
|
||||
)
|
||||
|
||||
# Iterar sobre pines posibles y buscar conexiones
|
||||
for pin_name in possible_input_pins:
|
||||
dest_key = (instruction_uid, pin_name)
|
||||
if dest_key in wire_connections:
|
||||
sources_list = wire_connections[dest_key]
|
||||
input_sources_repr = []
|
||||
for source_uid, source_pin in sources_list:
|
||||
source_repr = None
|
||||
if source_uid == "POWERRAIL":
|
||||
source_repr = {"type": "powerrail"}
|
||||
elif source_uid.startswith("OPENBRANCH_"):
|
||||
source_repr = {
|
||||
"type": "openbranch",
|
||||
"uid": source_uid,
|
||||
} # Fuente especial
|
||||
elif source_uid in access_map:
|
||||
source_repr = copy.deepcopy(access_map[source_uid])
|
||||
elif source_uid in parts_and_calls_map:
|
||||
source_instr_info = parts_and_calls_map[source_uid]
|
||||
source_repr = {
|
||||
"type": "connection",
|
||||
"source_instruction_type": source_instr_info.get(
|
||||
"type", "Unknown"
|
||||
), # Usar tipo base
|
||||
"source_instruction_uid": source_uid,
|
||||
"source_pin": source_pin,
|
||||
}
|
||||
else:
|
||||
# Fuente desconocida (ni Access, ni Part/Call válido)
|
||||
print(
|
||||
f"Advertencia: Fuente desconocida UID={source_uid} conectada a {instruction_uid}.{pin_name}"
|
||||
)
|
||||
source_repr = {"type": "unknown_source", "uid": source_uid}
|
||||
input_sources_repr.append(source_repr)
|
||||
|
||||
# Guardar la representación de la entrada (lista o dict)
|
||||
instruction_repr["inputs"][pin_name] = (
|
||||
input_sources_repr[0]
|
||||
if len(input_sources_repr) == 1
|
||||
else input_sources_repr
|
||||
)
|
||||
|
||||
# --- Poblar Salidas (simplificado: solo conexiones a Access) ---
|
||||
possible_output_pins = set(
|
||||
[
|
||||
"out",
|
||||
"out1",
|
||||
"Q",
|
||||
"q",
|
||||
"eno",
|
||||
"RET_VAL",
|
||||
"DSTBLK",
|
||||
"rt",
|
||||
"cv",
|
||||
"QU",
|
||||
"QD",
|
||||
"ET", # Añadir pines de salida estándar SCL
|
||||
]
|
||||
)
|
||||
if original_type == "BLKMOV":
|
||||
possible_output_pins.add("DSTBLK")
|
||||
if (
|
||||
original_type == "Call"
|
||||
): # Para Calls, las salidas dependen del bloque llamado
|
||||
call_xml_element_list = flgnet.xpath(
|
||||
f".//flg:Parts/flg:Call[@UId='{instruction_uid}']", namespaces=ns
|
||||
)
|
||||
if call_xml_element_list:
|
||||
call_info_node_list = call_xml_element_list[0].xpath(
|
||||
"./flg:CallInfo", namespaces=ns
|
||||
)
|
||||
if call_info_node_list:
|
||||
# Buscar parámetros con Section="Output" o "InOut" o "Return"
|
||||
output_param_names = call_info_node_list[0].xpath(
|
||||
"./flg:Parameter[@Section='Output' or @Section='InOut' or @Section='Return']/@Name",
|
||||
namespaces=ns,
|
||||
)
|
||||
possible_output_pins.update(output_param_names)
|
||||
|
||||
for pin_name in possible_output_pins:
|
||||
source_key = (instruction_uid, pin_name)
|
||||
if source_key in source_connections:
|
||||
if pin_name not in instruction_repr["outputs"]:
|
||||
instruction_repr["outputs"][pin_name] = []
|
||||
for dest_uid, dest_pin in source_connections[source_key]:
|
||||
if (
|
||||
dest_uid in access_map
|
||||
): # Solo registrar si va a una variable/constante
|
||||
dest_operand_copy = copy.deepcopy(access_map[dest_uid])
|
||||
if (
|
||||
dest_operand_copy
|
||||
not in instruction_repr["outputs"][pin_name]
|
||||
):
|
||||
instruction_repr["outputs"][pin_name].append(
|
||||
dest_operand_copy
|
||||
)
|
||||
|
||||
all_logic_steps[instruction_uid] = instruction_repr
|
||||
|
||||
# 4. Inferencia EN (modificado para usar tipos originales)
|
||||
processed_blocks_en_inference = set()
|
||||
try:
|
||||
# Ordenar UIDs numéricamente si es posible
|
||||
sorted_uids_for_en = sorted(
|
||||
all_logic_steps.keys(),
|
||||
key=lambda x: (
|
||||
int(x) if isinstance(x, str) and x.isdigit() else float("inf")
|
||||
),
|
||||
)
|
||||
except ValueError:
|
||||
sorted_uids_for_en = sorted(all_logic_steps.keys()) # Fallback sort
|
||||
|
||||
ordered_logic_list_for_en = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
for i, instruction in enumerate(ordered_logic_list_for_en):
|
||||
part_uid = instruction["instruction_uid"]
|
||||
# Usar el tipo original para la lógica de inferencia
|
||||
part_type_original = (
|
||||
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
|
||||
)
|
||||
|
||||
# Inferencia solo para tipos funcionales que no tengan EN explícito
|
||||
if (
|
||||
part_type_original in functional_block_types
|
||||
and "en" not in instruction.get("inputs", {})
|
||||
and part_uid not in processed_blocks_en_inference
|
||||
and "error" not in part_type_original
|
||||
): # No inferir para errores
|
||||
|
||||
inferred_en_source = None
|
||||
# Buscar hacia atrás en la lista ordenada
|
||||
if i > 0:
|
||||
for j in range(i - 1, -1, -1):
|
||||
prev_instr = ordered_logic_list_for_en[j]
|
||||
if "error" in prev_instr.get("type", ""):
|
||||
continue # Saltar errores previos
|
||||
|
||||
prev_uid = prev_instr["instruction_uid"]
|
||||
prev_type_original = (
|
||||
prev_instr.get("type", "")
|
||||
.replace(SCL_SUFFIX, "")
|
||||
.replace("_error", "")
|
||||
)
|
||||
|
||||
if prev_type_original in rlo_generators: # Fuente RLO encontrada
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "out",
|
||||
}
|
||||
break # Detener búsqueda
|
||||
elif (
|
||||
prev_type_original in functional_block_types
|
||||
): # Bloque funcional previo
|
||||
# Comprobar si este bloque tiene salida ENO conectada
|
||||
if (prev_uid, "eno") in source_connections:
|
||||
inferred_en_source = {
|
||||
"type": "connection",
|
||||
"source_instruction_uid": prev_uid,
|
||||
"source_instruction_type": prev_type_original, # Tipo original
|
||||
"source_pin": "eno",
|
||||
}
|
||||
# Si no tiene ENO conectado, el flujo RLO se detiene aquí
|
||||
break # Detener búsqueda
|
||||
elif prev_type_original in [
|
||||
"Coil",
|
||||
"SCoil",
|
||||
"RCoil",
|
||||
"SdCoil",
|
||||
"SetCoil",
|
||||
"ResetCoil",
|
||||
]:
|
||||
# Bobinas terminan el flujo RLO
|
||||
break # Detener búsqueda
|
||||
|
||||
# Si no se encontró fuente, conectar a PowerRail
|
||||
if inferred_en_source is None:
|
||||
inferred_en_source = {"type": "powerrail"}
|
||||
|
||||
# Actualizar la instrucción EN el diccionario principal
|
||||
if part_uid in all_logic_steps:
|
||||
# Asegurar que inputs exista
|
||||
if "inputs" not in all_logic_steps[part_uid]:
|
||||
all_logic_steps[part_uid]["inputs"] = {}
|
||||
all_logic_steps[part_uid]["inputs"]["en"] = inferred_en_source
|
||||
processed_blocks_en_inference.add(part_uid)
|
||||
|
||||
# 5. Lógica ENO (añadir destinos ENO si existen)
|
||||
for source_instr_uid, eno_destinations in eno_outputs.items():
|
||||
if source_instr_uid in all_logic_steps and "error" not in all_logic_steps[
|
||||
source_instr_uid
|
||||
].get("type", ""):
|
||||
all_logic_steps[source_instr_uid]["eno_destinations"] = eno_destinations
|
||||
|
||||
# 6. Ordenar y Devolver
|
||||
final_logic_list = [
|
||||
all_logic_steps[uid] for uid in sorted_uids_for_en if uid in all_logic_steps
|
||||
]
|
||||
|
||||
return {
|
||||
"id": network_id,
|
||||
"title": network_title,
|
||||
"comment": network_comment,
|
||||
"language": network_lang, # Lenguaje original de la red
|
||||
"logic": final_logic_list,
|
||||
# No añadir 'error' aquí a menos que el parseo completo falle
|
||||
}
|
||||
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
# Este parser maneja LAD, FBD y GRAPH
|
||||
return {
|
||||
"language": ["LAD", "FBD", "GRAPH"], # Lista de lenguajes soportados
|
||||
"parser_func": parse_lad_fbd_network, # Función a llamar
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
# ToUpload/parsers/parse_scl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import ns, get_multilingual_text
|
||||
|
||||
def reconstruct_scl_from_tokens(st_node):
|
||||
"""
|
||||
Reconstruye SCL desde <StructuredText>, mejorando el manejo de
|
||||
variables, constantes literales, tokens básicos, espacios y saltos de línea.
|
||||
"""
|
||||
if st_node is None:
|
||||
return "// Error: StructuredText node not found.\n"
|
||||
|
||||
scl_parts = []
|
||||
# Usar st:* para obtener todos los elementos hijos dentro del namespace st
|
||||
children = st_node.xpath("./st:*", namespaces=ns)
|
||||
|
||||
for elem in children:
|
||||
tag = etree.QName(elem.tag).localname
|
||||
|
||||
if tag == "Token":
|
||||
scl_parts.append(elem.get("Text", ""))
|
||||
elif tag == "Blank":
|
||||
# Añadir espacios solo si es necesario o más de uno
|
||||
num_spaces = int(elem.get("Num", 1))
|
||||
if not scl_parts or not scl_parts[-1].endswith(" "):
|
||||
scl_parts.append(" " * num_spaces)
|
||||
elif num_spaces > 1:
|
||||
scl_parts.append(" " * (num_spaces -1))
|
||||
|
||||
elif tag == "NewLine":
|
||||
# Quitar espacios finales antes del salto de línea
|
||||
if scl_parts:
|
||||
scl_parts[-1] = scl_parts[-1].rstrip()
|
||||
scl_parts.append("\n")
|
||||
elif tag == "Access":
|
||||
scope = elem.get("Scope")
|
||||
access_str = f"/*_ERR_Scope_{scope}_*/" # Placeholder
|
||||
|
||||
# --- Variables ---
|
||||
if scope in [
|
||||
"GlobalVariable", "LocalVariable", "TempVariable", "InOutVariable",
|
||||
"InputVariable", "OutputVariable", "ConstantVariable",
|
||||
"GlobalConstant", "LocalConstant" # Añadir constantes simbólicas
|
||||
]:
|
||||
symbol_elem = elem.xpath("./st:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./st:Component", namespaces=ns)
|
||||
symbol_text_parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
if i > 0: symbol_text_parts.append(".")
|
||||
|
||||
# Check for HasQuotes attribute (adjust namespace if needed)
|
||||
# El atributo está en el Component o en el Access padre? Probar ambos
|
||||
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true" # Check directly on Component
|
||||
has_quotes_access = False
|
||||
access_parent = comp.xpath("ancestor::st:Access[1]", namespaces=ns) # Get immediate Access parent
|
||||
if access_parent:
|
||||
has_quotes_attr = access_parent[0].xpath("./st:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns)
|
||||
has_quotes_access = has_quotes_attr and has_quotes_attr[0].lower() == 'true'
|
||||
|
||||
has_quotes = has_quotes_comp or has_quotes_access
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
# Apply quotes based on HasQuotes or if it's the first component and not temp
|
||||
if has_quotes or (i == 0 and not is_temp and '"' not in name): # Avoid double quotes
|
||||
symbol_text_parts.append(f'"{name}"')
|
||||
else:
|
||||
symbol_text_parts.append(name)
|
||||
|
||||
# --- Array Index Access ---
|
||||
index_access_nodes = comp.xpath("./st:Access", namespaces=ns)
|
||||
if index_access_nodes:
|
||||
# Llamada recursiva para cada índice
|
||||
indices_text = [reconstruct_scl_from_tokens(idx_node) for idx_node in index_access_nodes]
|
||||
# Limpiar saltos de línea dentro de los corchetes
|
||||
indices_cleaned = [idx.replace('\n', '').strip() for idx in indices_text]
|
||||
symbol_text_parts.append(f"[{','.join(indices_cleaned)}]")
|
||||
|
||||
access_str = "".join(symbol_text_parts)
|
||||
else:
|
||||
access_str = f"/*_ERR_NO_SYMBOL_IN_{scope}_*/"
|
||||
|
||||
# --- Constantes Literales ---
|
||||
elif scope == "LiteralConstant":
|
||||
constant_elem = elem.xpath("./st:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
val_elem = constant_elem[0].xpath("./st:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./st:ConstantType/text()", namespaces=ns)
|
||||
const_type = type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else ""
|
||||
const_val = val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONSTVAL_"
|
||||
|
||||
# Formatear según tipo
|
||||
if const_type == "bool": access_str = const_val.upper()
|
||||
elif const_type.lower() == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type.lower() == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
elif const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WSTRING#'{replaced_val}'"
|
||||
elif const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WCHAR#'{replaced_val}'"
|
||||
elif const_type == "time": access_str = f"T#{const_val}"
|
||||
elif const_type == "ltime": access_str = f"LT#{const_val}"
|
||||
elif const_type == "s5time": access_str = f"S5T#{const_val}"
|
||||
elif const_type == "date": access_str = f"D#{const_val}"
|
||||
elif const_type == "dtl": access_str = f"DTL#{const_val}"
|
||||
elif const_type == "dt": access_str = f"DT#{const_val}"
|
||||
elif const_type == "tod": access_str = f"TOD#{const_val}"
|
||||
elif const_type in ["int", "dint", "sint", "usint", "uint", "udint", "real", "lreal", "word", "dword", "byte"]:
|
||||
# Añadir .0 para reales si no tienen decimal
|
||||
if const_type in ["real", "lreal"] and '.' not in const_val and 'e' not in const_val.lower():
|
||||
access_str = f"{const_val}.0"
|
||||
else:
|
||||
access_str = const_val
|
||||
else: # Otros tipos (LWORD, etc.) o desconocidos
|
||||
access_str = const_val
|
||||
else:
|
||||
access_str = "/*_ERR_NOCONST_*/"
|
||||
|
||||
# --- Llamadas a Funciones/Bloques (Scope=Call) ---
|
||||
elif scope == "Call":
|
||||
call_info_node = elem.xpath("./st:CallInfo", namespaces=ns)
|
||||
if call_info_node:
|
||||
ci = call_info_node[0]
|
||||
call_name = ci.get("Name", "_ERR_CALLNAME_")
|
||||
call_type = ci.get("BlockType") # FB, FC, etc.
|
||||
|
||||
# Parámetros (están como Access o Token dentro de CallInfo/Parameter)
|
||||
params = ci.xpath("./st:Parameter", namespaces=ns)
|
||||
param_parts = []
|
||||
for p in params:
|
||||
p_name = p.get("Name", "_ERR_PARAMNAME_")
|
||||
# El valor del parámetro está dentro del nodo Parameter
|
||||
p_value_node = p.xpath("./st:Access | ./st:Token", namespaces=ns) # Buscar Access o Token
|
||||
p_value_scl = ""
|
||||
if p_value_node:
|
||||
p_value_scl = reconstruct_scl_from_tokens(p) # Parsear el contenido del parámetro
|
||||
p_value_scl = p_value_scl.replace('\n', '').strip() # Limpiar SCL resultante
|
||||
param_parts.append(f"{p_name} := {p_value_scl}")
|
||||
|
||||
# Manejar FB vs FC
|
||||
if call_type == "FB":
|
||||
instance_node = ci.xpath("./st:Instance/st:Component/@Name", namespaces=ns)
|
||||
if instance_node:
|
||||
instance_name = f'"{instance_node[0]}"'
|
||||
access_str = f"{instance_name}({', '.join(param_parts)})"
|
||||
else: # FB sin instancia? Podría ser STAT
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* FB sin instancia explícita? *)'
|
||||
elif call_type == "FC":
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)})'
|
||||
else: # Otros tipos de llamada
|
||||
access_str = f'"{call_name}"({", ".join(param_parts)}) (* Tipo: {call_type} *)'
|
||||
else:
|
||||
access_str = "/*_ERR_NO_CALLINFO_*/"
|
||||
|
||||
# Añadir más scopes si son necesarios (e.g., Address, Label, Reference)
|
||||
|
||||
scl_parts.append(access_str)
|
||||
|
||||
elif tag == "Comment" or tag == "LineComment":
|
||||
# Usar get_multilingual_text del parser_utils
|
||||
comment_text = get_multilingual_text(elem)
|
||||
if tag == "Comment":
|
||||
scl_parts.append(f"(* {comment_text} *)")
|
||||
else:
|
||||
scl_parts.append(f"// {comment_text}")
|
||||
# Ignorar otros tipos de nodos si no son relevantes para el SCL
|
||||
|
||||
full_scl = "".join(scl_parts)
|
||||
|
||||
# --- Re-indentación Simple ---
|
||||
output_lines = []
|
||||
indent_level = 0
|
||||
indent_str = " " # Dos espacios
|
||||
for line in full_scl.splitlines():
|
||||
trimmed_line = line.strip()
|
||||
if not trimmed_line:
|
||||
# Mantener líneas vacías? Opcional.
|
||||
# output_lines.append("")
|
||||
continue
|
||||
|
||||
# Reducir indentación ANTES de imprimir para END, ELSE, etc.
|
||||
if trimmed_line.upper().startswith(("END_", "UNTIL", "}")) or \
|
||||
trimmed_line.upper() in ["ELSE", "ELSIF"]:
|
||||
indent_level = max(0, indent_level - 1)
|
||||
|
||||
output_lines.append(indent_str * indent_level + trimmed_line)
|
||||
|
||||
# Aumentar indentación DESPUÉS de imprimir para IF, FOR, etc.
|
||||
# Ser más específico con las palabras clave que aumentan indentación
|
||||
# Usar .upper() para ignorar mayúsculas/minúsculas
|
||||
line_upper = trimmed_line.upper()
|
||||
if line_upper.endswith(("THEN", "DO", "OF", "{")) or \
|
||||
line_upper.startswith(("IF ", "FOR ", "WHILE ", "CASE ", "REPEAT", "STRUCT")) or \
|
||||
line_upper == "ELSE":
|
||||
# Excepción: No indentar después de ELSE IF
|
||||
if not (line_upper == "ELSE" and "IF" in output_lines[-1].upper()):
|
||||
indent_level += 1
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def parse_scl_network(network_element):
|
||||
"""
|
||||
Parsea una red SCL extrayendo el código fuente reconstruido.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSCL_ID")
|
||||
network_lang = "SCL" # Sabemos que es SCL
|
||||
|
||||
# Buscar NetworkSource y luego StructuredText
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
structured_text_node = None
|
||||
if network_source_node:
|
||||
structured_text_node_list = network_source_node[0].xpath("./st:StructuredText", namespaces=ns)
|
||||
if structured_text_node_list:
|
||||
structured_text_node = structured_text_node_list[0]
|
||||
|
||||
reconstructed_scl = "// SCL extraction failed: StructuredText node not found.\n"
|
||||
if structured_text_node is not None:
|
||||
reconstructed_scl = reconstruct_scl_from_tokens(structured_text_node)
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"logic": [ # SCL se guarda como un único bloque lógico
|
||||
{
|
||||
"instruction_uid": f"SCL_{network_id}", # UID sintético
|
||||
"type": "RAW_SCL_CHUNK", # Tipo especial para SCL crudo
|
||||
"scl": reconstructed_scl, # El código SCL reconstruido
|
||||
}
|
||||
],
|
||||
# No añadimos error aquí, reconstruct_scl_from_tokens ya incluye comentarios de error
|
||||
}
|
||||
return parsed_network_data
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {
|
||||
'language': ['SCL'], # Lista de lenguajes soportados
|
||||
'parser_func': parse_scl_network # Función a llamar
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
# ToUpload/parsers/parse_stl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
|
||||
# Importar desde las utilidades del parser
|
||||
from .parser_utils import ns # Solo necesitamos los namespaces aquí
|
||||
|
||||
# --- Funciones Auxiliares de Reconstrucción STL (Adaptadas de x1) ---
|
||||
|
||||
def get_access_text_stl(access_element):
|
||||
"""Reconstruye una representación textual simple de un Access en STL."""
|
||||
if access_element is None: return "_ERR_ACCESS_"
|
||||
scope = access_element.get("Scope")
|
||||
|
||||
# Símbolo (Variable, Constante Simbólica)
|
||||
symbol_elem = access_element.xpath("./stl:Symbol", namespaces=ns)
|
||||
if symbol_elem:
|
||||
components = symbol_elem[0].xpath("./stl:Component", namespaces=ns)
|
||||
parts = []
|
||||
for i, comp in enumerate(components):
|
||||
name = comp.get("Name", "_ERR_COMP_")
|
||||
# Comprobar HasQuotes (en Access padre?)
|
||||
has_quotes_elem = comp.xpath("ancestor::stl:Access/stl:BooleanAttribute[@Name='HasQuotes']/text()", namespaces=ns)
|
||||
has_quotes = has_quotes_elem and has_quotes_elem[0].lower() == "true"
|
||||
is_temp = name.startswith("#")
|
||||
|
||||
if i > 0: parts.append(".")
|
||||
# Aplicar comillas
|
||||
if has_quotes or (i == 0 and not is_temp and '"' not in name):
|
||||
parts.append(f'"{name}"')
|
||||
else:
|
||||
parts.append(name)
|
||||
# Índices de Array
|
||||
index_access = comp.xpath("./stl:Access", namespaces=ns)
|
||||
if index_access:
|
||||
indices = [get_access_text_stl(ia) for ia in index_access]
|
||||
parts.append(f"[{','.join(indices)}]")
|
||||
return "".join(parts)
|
||||
|
||||
# Constante Literal
|
||||
constant_elem = access_element.xpath("./stl:Constant", namespaces=ns)
|
||||
if constant_elem:
|
||||
val_elem = constant_elem[0].xpath("./stl:ConstantValue/text()", namespaces=ns)
|
||||
type_elem = constant_elem[0].xpath("./stl:ConstantType/text()", namespaces=ns)
|
||||
const_type = (type_elem[0].strip().lower() if type_elem and type_elem[0] is not None else "")
|
||||
const_val = (val_elem[0].strip() if val_elem and val_elem[0] is not None else "_ERR_CONST_")
|
||||
|
||||
# Añadir prefijos estándar STL
|
||||
if const_type == "time": return f"T#{const_val}"
|
||||
if const_type == "s5time": return f"S5T#{const_val}"
|
||||
if const_type == "date": return f"D#{const_val}"
|
||||
if const_type == "dt": return f"DT#{const_val}"
|
||||
if const_type == "time_of_day" or const_type=="tod": return f"TOD#{const_val}"
|
||||
if const_type.lower() == "string":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
if const_type.lower() == "char":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"'{replaced_val}'"
|
||||
if const_type == "wstring":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WSTRING#'{replaced_val}'"
|
||||
if const_type == "wchar":
|
||||
replaced_val = const_val.replace("'", "''")
|
||||
access_str = f"WCHAR#'{replaced_val}'" # Añadir más si es necesario (WSTRING#, BYTE#, WORD#...)
|
||||
if const_type == "byte" and const_val.startswith("16#"): return f"B#{const_val}" # Formato B#16#FF
|
||||
if const_type == "word" and const_val.startswith("16#"): return f"W#{const_val}"
|
||||
if const_type == "dword" and const_val.startswith("16#"): return f"DW#{const_val}"
|
||||
# Real con punto decimal
|
||||
if const_type == "real" and '.' not in const_val and 'e' not in const_val.lower(): return f"{const_val}.0"
|
||||
return const_val # Valor por defecto
|
||||
|
||||
# Etiqueta
|
||||
label_elem = access_element.xpath("./stl:Label", namespaces=ns)
|
||||
if label_elem:
|
||||
return label_elem[0].get("Name", "_ERR_LABEL_")
|
||||
|
||||
# Acceso Indirecto (Punteros)
|
||||
indirect_elem = access_element.xpath("./stl:Indirect", namespaces=ns)
|
||||
if indirect_elem:
|
||||
reg = indirect_elem[0].get("Register", "AR?") # AR1, AR2
|
||||
offset_str = indirect_elem[0].get("BitOffset", "0")
|
||||
area = indirect_elem[0].get("Area", "DB") # DB, DI, L, etc.
|
||||
width = indirect_elem[0].get("Width", "X") # Bit, Byte, Word, Double
|
||||
try:
|
||||
bit_offset = int(offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
p_format_offset = f"P#{byte_offset}.{bit_in_byte}"
|
||||
except ValueError:
|
||||
p_format_offset = "P#?.?"
|
||||
width_map = {"Bit": "X", "Byte": "B", "Word": "W", "Double": "D", "Long": "D"}
|
||||
width_char = width_map.get(width, width[0] if width else "?")
|
||||
return f"{area}{width_char}[{reg},{p_format_offset}]"
|
||||
|
||||
# Dirección Absoluta (I, Q, M, PI, PQ, T, C, DBX, DIX, L)
|
||||
address_elem = access_element.xpath("./stl:Address", namespaces=ns)
|
||||
if address_elem:
|
||||
area = address_elem[0].get("Area", "??") # Input, Output, Memory, DB, DI, Local, Timer, Counter...
|
||||
bit_offset_str = address_elem[0].get("BitOffset", "0")
|
||||
addr_type_str = address_elem[0].get("Type", "Bool") # Bool, Byte, Word, DWord, Int, DInt, Real...
|
||||
try:
|
||||
bit_offset = int(bit_offset_str)
|
||||
byte_offset = bit_offset // 8
|
||||
bit_in_byte = bit_offset % 8
|
||||
# Determinar ancho (X, B, W, D)
|
||||
addr_width = "X" # Default bit
|
||||
if addr_type_str in ["Byte", "SInt", "USInt"]: addr_width = "B"
|
||||
elif addr_type_str in ["Word", "Int", "UInt"]: addr_width = "W"
|
||||
elif addr_type_str in ["DWord", "DInt", "UDInt", "Real", "Time", "DT", "TOD"]: addr_width = "D"
|
||||
elif addr_type_str in ["LReal", "LTime", "LWord", "LInt", "ULInt"]: addr_width = "D" # L se maneja como D en direccionamiento base? O usar L? Chequear estándar. STL clásico no tenía L.
|
||||
# Mapear Área XML a Área STL
|
||||
area_map = {"Input": "I", "Output": "Q", "Memory": "M",
|
||||
"PeripheryInput": "PI", "PeripheryOutput": "PQ",
|
||||
"DB": "DB", "DI": "DI", "Local": "L",
|
||||
"Timer": "T", "Counter": "C"}
|
||||
stl_area = area_map.get(area, area)
|
||||
|
||||
if stl_area in ["DB", "DI"]:
|
||||
block_num = address_elem[0].get("BlockNumber") # Para DB10.DBX0.0
|
||||
if block_num:
|
||||
return f"{stl_area}{block_num}.{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
else: # Para acceso con registro DB/DI (DBX, DIW, etc.)
|
||||
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
|
||||
elif stl_area in ["T", "C"]:
|
||||
return f"{stl_area}{byte_offset}" # T 5, C 10 (offset es el número)
|
||||
else: # I, Q, M, L, PI, PQ
|
||||
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}" # M10.1, IW0, QB5, etc.
|
||||
|
||||
except ValueError:
|
||||
return f"{area}?{bit_offset_str}?"
|
||||
|
||||
# CallInfo (para CALL FC10, CALL FB20, DB10)
|
||||
call_info_elem = access_element.xpath("./stl:CallInfo", namespaces=ns)
|
||||
if call_info_elem:
|
||||
name = call_info_elem[0].get("Name", "_ERR_CALL_")
|
||||
btype = call_info_elem[0].get("BlockType", "FC") # FC, FB, DB
|
||||
instance_node = call_info_elem[0].xpath("./stl:Instance/stl:Component/@Name", namespaces=ns)
|
||||
if btype == "FB" and instance_node:
|
||||
# Para CALL FB, el operando es el DB de instancia
|
||||
db_name_raw = instance_node[0]
|
||||
return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw
|
||||
elif btype == "DB":
|
||||
return f'DB "{name}"' # O solo DB name? ej. DB10
|
||||
else: # FC
|
||||
return f'{btype} "{name}"' # FC "Nombre"
|
||||
|
||||
return f"_{scope}_?" # Fallback
|
||||
|
||||
|
||||
def get_comment_text_stl(comment_element):
|
||||
"""Extrae texto de un LineComment o Comment para STL."""
|
||||
if comment_element is None: return ""
|
||||
# STL Comments suelen tener <Text> directamente
|
||||
text_nodes = comment_element.xpath("./stl:Text/text()", namespaces=ns)
|
||||
if text_nodes:
|
||||
return text_nodes[0].strip()
|
||||
return "" # Vacío si no hay <Text>
|
||||
|
||||
def reconstruct_stl_from_statementlist(statement_list_node):
|
||||
"""Reconstruye el código STL como una cadena de texto desde <StatementList>."""
|
||||
if statement_list_node is None:
|
||||
return "// Error: StatementList node not found.\n"
|
||||
stl_lines = []
|
||||
statements = statement_list_node.xpath("./stl:StlStatement", namespaces=ns)
|
||||
|
||||
for stmt in statements:
|
||||
line_parts = []
|
||||
inline_comment = "" # Comentarios en la misma línea
|
||||
|
||||
# 1. Comentarios iniciales (línea completa //)
|
||||
initial_comments = stmt.xpath("child::stl:Comment[not(@Inserted='true')] | child::stl:LineComment[not(@Inserted='true')]", namespaces=ns)
|
||||
for comm in initial_comments:
|
||||
comment_text = get_comment_text_stl(comm)
|
||||
if comment_text:
|
||||
for comment_line in comment_text.splitlines():
|
||||
stl_lines.append(f"// {comment_line}")
|
||||
|
||||
# 2. Etiqueta (Label)
|
||||
label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns)
|
||||
label_str = ""
|
||||
if label_decl:
|
||||
label_name = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns)
|
||||
if label_name:
|
||||
label_str = f"{label_name[0]}:"
|
||||
# Comentarios después de la etiqueta (inline)
|
||||
label_comments = label_decl[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for lcomm in label_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(lcomm)}"
|
||||
if label_str:
|
||||
line_parts.append(label_str)
|
||||
|
||||
# 3. Instrucción (StlToken)
|
||||
instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns)
|
||||
instruction_str = ""
|
||||
if instruction_token:
|
||||
token_text = instruction_token[0].get("Text", "_ERR_TOKEN_")
|
||||
if token_text == "EMPTY_LINE":
|
||||
stl_lines.append("") # Línea vacía
|
||||
continue # Saltar resto del statement
|
||||
elif token_text == "COMMENT": # Marcador de línea de comentario completo
|
||||
# Ya manejado por initial_comments? Verificar XML. Si no, extraer comentario aquí.
|
||||
pass # Asumir manejado antes
|
||||
else:
|
||||
instruction_str = token_text
|
||||
# Comentarios asociados al token (inline)
|
||||
token_comments = instruction_token[0].xpath("./stl:Comment[@Inserted='true'] | ./stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for tcomm in token_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(tcomm)}"
|
||||
if instruction_str:
|
||||
# Añadir tabulación si hay etiqueta
|
||||
line_parts.append("\t" + instruction_str if label_str else instruction_str)
|
||||
|
||||
# 4. Operando (Access)
|
||||
access_elem = stmt.xpath("./stl:Access", namespaces=ns)
|
||||
access_str = ""
|
||||
if access_elem:
|
||||
access_text = get_access_text_stl(access_elem[0])
|
||||
access_str = access_text
|
||||
# Comentarios dentro del Access (inline)
|
||||
access_comments = access_elem[0].xpath("child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']", namespaces=ns)
|
||||
for acc_comm in access_comments:
|
||||
inline_comment += f" // {get_comment_text_stl(acc_comm)}"
|
||||
if access_str:
|
||||
line_parts.append(access_str)
|
||||
|
||||
# Construir línea final
|
||||
current_line = " ".join(lp for lp in line_parts if lp) # Unir partes con espacio
|
||||
if inline_comment:
|
||||
current_line += f"\t{inline_comment.strip()}" # Añadir comentario con tab
|
||||
|
||||
if current_line.strip(): # Añadir solo si no está vacía después de todo
|
||||
stl_lines.append(current_line.rstrip()) # Quitar espacios finales
|
||||
|
||||
return "\n".join(stl_lines)
|
||||
|
||||
|
||||
def parse_stl_network(network_element):
|
||||
"""
|
||||
Parsea una red STL extrayendo el código fuente reconstruido.
|
||||
Devuelve un diccionario representando la red para el JSON.
|
||||
"""
|
||||
network_id = network_element.get("ID", "UnknownSTL_ID")
|
||||
network_lang = "STL"
|
||||
|
||||
# Buscar NetworkSource y luego StatementList
|
||||
network_source_node = network_element.xpath(".//flg:NetworkSource", namespaces=ns)
|
||||
statement_list_node = None
|
||||
if network_source_node:
|
||||
statement_list_node_list = network_source_node[0].xpath("./stl:StatementList", namespaces=ns)
|
||||
if statement_list_node_list:
|
||||
statement_list_node = statement_list_node_list[0]
|
||||
|
||||
reconstructed_stl = "// STL extraction failed: StatementList node not found.\n"
|
||||
if statement_list_node is not None:
|
||||
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node)
|
||||
|
||||
# Crear la estructura de datos para la red
|
||||
parsed_network_data = {
|
||||
"id": network_id,
|
||||
"language": network_lang,
|
||||
"logic": [ # STL se guarda como un único bloque lógico
|
||||
{
|
||||
"instruction_uid": f"STL_{network_id}", # UID sintético
|
||||
"type": "RAW_STL_CHUNK", # Tipo especial para STL crudo
|
||||
"stl": reconstructed_stl, # El código STL reconstruido
|
||||
}
|
||||
],
|
||||
}
|
||||
return parsed_network_data
|
||||
|
||||
# --- Función de Información del Parser ---
|
||||
def get_parser_info():
|
||||
"""Devuelve la información para este parser."""
|
||||
return {
|
||||
'language': ['STL'], # Lenguaje soportado
|
||||
'parser_func': parse_stl_network # Función a llamar
|
||||
}
|
|
@ -0,0 +1,387 @@
|
|||
# ToUpload/parsers/parser_utils.py
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree
|
||||
import traceback
|
||||
|
||||
# --- Namespaces (Común para muchos parsers) ---
|
||||
ns = {
|
||||
"iface": "http://www.siemens.com/automation/Openness/SW/Interface/v5",
|
||||
"flg": "http://www.siemens.com/automation/Openness/SW/NetworkSource/FlgNet/v4",
|
||||
"st": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StructuredText/v3",
|
||||
"stl": "http://www.siemens.com/automation/Openness/SW/NetworkSource/StatementList/v4",
|
||||
}
|
||||
|
||||
# --- Funciones Comunes de Extracción de Texto y Nodos ---
|
||||
|
||||
|
||||
def get_multilingual_text(element, default_lang="en-US", fallback_lang="it-IT"):
|
||||
"""Extrae texto multilingüe de un elemento XML, asegurando devolver siempre string."""
|
||||
if element is None:
|
||||
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()
|
||||
# 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 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 "" # Devolver cadena vacía en caso de excepción
|
||||
|
||||
|
||||
def get_symbol_name(symbol_element):
|
||||
"""Obtiene el nombre completo de un símbolo desde un elemento <flg:Symbol>."""
|
||||
if symbol_element is None:
|
||||
return None
|
||||
try:
|
||||
components = symbol_element.xpath("./flg:Component/@Name", namespaces=ns)
|
||||
return (
|
||||
".".join(
|
||||
f'"{c}"' if not c.startswith("#") and '"' not in c else c
|
||||
for c in components
|
||||
)
|
||||
if components
|
||||
else None
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Excepción en get_symbol_name: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_access(access_element):
|
||||
"""Parsea un nodo <flg:Access> devolviendo un diccionario con su información."""
|
||||
if access_element is None:
|
||||
return None
|
||||
uid = access_element.get("UId")
|
||||
scope = access_element.get("Scope")
|
||||
info = {"uid": uid, "scope": scope, "type": "unknown"}
|
||||
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
|
||||
constant = access_element.xpath("./flg:Constant", namespaces=ns)
|
||||
|
||||
if symbol:
|
||||
info["type"] = "variable"
|
||||
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}")
|
||||
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}"'
|
||||
)
|
||||
elif constant:
|
||||
info["type"] = "constant"
|
||||
const_type_elem = constant[0].xpath("./flg:ConstantType", namespaces=ns)
|
||||
const_val_elem = constant[0].xpath("./flg:ConstantValue", namespaces=ns)
|
||||
info["datatype"] = (
|
||||
const_type_elem[0].text.strip()
|
||||
if const_type_elem and const_type_elem[0].text is not None
|
||||
else "Unknown"
|
||||
)
|
||||
value_str = (
|
||||
const_val_elem[0].text.strip()
|
||||
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}")
|
||||
if info["datatype"] == "Unknown" and value_str:
|
||||
val_lower = value_str.lower()
|
||||
if val_lower in ["true", "false"]:
|
||||
info["datatype"] = "Bool"
|
||||
elif value_str.isdigit() or (
|
||||
value_str.startswith("-") and value_str[1:].isdigit()
|
||||
):
|
||||
info["datatype"] = "Int"
|
||||
elif "." in value_str:
|
||||
try:
|
||||
float(value_str)
|
||||
info["datatype"] = "Real"
|
||||
except ValueError:
|
||||
pass
|
||||
elif "#" in value_str:
|
||||
parts = value_str.split("#", 1)
|
||||
prefix = parts[0].upper()
|
||||
if prefix == "T":
|
||||
info["datatype"] = "Time"
|
||||
elif prefix == "LT":
|
||||
info["datatype"] = "LTime"
|
||||
elif prefix == "S5T":
|
||||
info["datatype"] = "S5Time"
|
||||
elif prefix == "D":
|
||||
info["datatype"] = "Date"
|
||||
elif prefix == "DT":
|
||||
info["datatype"] = "DT"
|
||||
elif prefix == "DTL":
|
||||
info["datatype"] = "DTL"
|
||||
elif prefix == "TOD":
|
||||
info["datatype"] = "Time_Of_Day"
|
||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||
info["datatype"] = "String"
|
||||
else:
|
||||
info["datatype"] = "TypedConstant"
|
||||
elif value_str.startswith("'") and value_str.endswith("'"):
|
||||
info["datatype"] = "String"
|
||||
info["value"] = value_str
|
||||
dtype_lower = info["datatype"].lower()
|
||||
val_str_processed = value_str
|
||||
if isinstance(value_str, str):
|
||||
if "#" in value_str:
|
||||
val_str_processed = value_str.split("#", 1)[-1]
|
||||
if (
|
||||
val_str_processed.startswith("'")
|
||||
and val_str_processed.endswith("'")
|
||||
and len(val_str_processed) > 1
|
||||
):
|
||||
val_str_processed = val_str_processed[1:-1]
|
||||
try:
|
||||
if dtype_lower in [
|
||||
"int",
|
||||
"dint",
|
||||
"udint",
|
||||
"sint",
|
||||
"usint",
|
||||
"lint",
|
||||
"ulint",
|
||||
"word",
|
||||
"dword",
|
||||
"lword",
|
||||
"byte",
|
||||
]:
|
||||
info["value"] = int(val_str_processed)
|
||||
elif dtype_lower == "bool":
|
||||
info["value"] = (
|
||||
val_str_processed.lower() == "true" or val_str_processed == "1"
|
||||
)
|
||||
elif dtype_lower in ["real", "lreal"]:
|
||||
info["value"] = float(val_str_processed)
|
||||
except (ValueError, TypeError):
|
||||
info["value"] = value_str
|
||||
else:
|
||||
info["type"] = "unknown_structure"
|
||||
print(f"Advertencia: Access UID={uid} no es Symbol ni Constant.")
|
||||
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
|
||||
|
||||
|
||||
def parse_part(part_element):
|
||||
"""Parsea un nodo <flg:Part> de LAD/FBD."""
|
||||
if part_element is None:
|
||||
return None
|
||||
uid = part_element.get("UId")
|
||||
name = part_element.get("Name")
|
||||
if not uid or not name:
|
||||
print(
|
||||
f"Error: Part sin UID o Name: {etree.tostring(part_element, encoding='unicode')}"
|
||||
)
|
||||
return None
|
||||
template_values = {}
|
||||
negated_pins = {}
|
||||
try:
|
||||
for tv in part_element.xpath("./TemplateValue"):
|
||||
tv_name = tv.get("Name")
|
||||
tv_type = tv.get("Type")
|
||||
if tv_name and tv_type:
|
||||
template_values[tv_name] = tv_type
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
|
||||
try:
|
||||
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,
|
||||
"template_values": template_values,
|
||||
"negated_pins": negated_pins,
|
||||
}
|
||||
|
||||
|
||||
def parse_call(call_element):
|
||||
"""Parsea un nodo <flg:Call> de LAD/FBD."""
|
||||
if call_element is None:
|
||||
return None
|
||||
uid = call_element.get("UId")
|
||||
if not uid:
|
||||
print(
|
||||
f"Error: Call encontrado sin UID: {etree.tostring(call_element, encoding='unicode')}"
|
||||
)
|
||||
return None
|
||||
call_info_elem = call_element.xpath("./flg:CallInfo", namespaces=ns)
|
||||
if not call_info_elem:
|
||||
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.")
|
||||
return {"uid": uid, "type": "Call_error", "error": "Missing CallInfo"}
|
||||
else:
|
||||
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]
|
||||
block_name = call_info.get("Name")
|
||||
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 {
|
||||
"uid": uid,
|
||||
"type": "Call_error",
|
||||
"error": "Missing Name or BlockType in CallInfo",
|
||||
}
|
||||
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")
|
||||
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:
|
||||
instance_name = (
|
||||
f'"{db_name_raw}"'
|
||||
if not db_name_raw.startswith('"')
|
||||
else db_name_raw
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: <flg:Component> en <flg:Instance> FB Call UID {uid} sin 'Name'."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: No se encontró <flg:Component> en <flg:Instance> FB Call UID {uid}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Advertencia: FB Call '{block_name}' UID {uid} sin <flg:Instance>. ¿Llamada a multi-instancia STAT?"
|
||||
)
|
||||
call_scope = call_element.get("Scope")
|
||||
if call_scope == "LocalVariable":
|
||||
instance_name = f'"{block_name}"'
|
||||
instance_scope = "Static"
|
||||
print(
|
||||
f"INFO: Asumiendo instancia STAT '{instance_name}' para FB Call UID {uid}."
|
||||
)
|
||||
call_data = {
|
||||
"uid": uid,
|
||||
"type": "Call",
|
||||
"block_name": block_name,
|
||||
"block_type": block_type,
|
||||
}
|
||||
if instance_name:
|
||||
call_data["instance_db"] = instance_name
|
||||
if instance_scope:
|
||||
call_data["instance_scope"] = instance_scope
|
||||
return call_data
|
||||
|
||||
|
||||
def parse_interface_members(member_elements):
|
||||
"""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")
|
||||
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. Saltando.")
|
||||
continue
|
||||
member_dtype = (
|
||||
f"{member_dtype_raw}:v{member_version}"
|
||||
if member_version
|
||||
else member_dtype_raw
|
||||
)
|
||||
member_info = {
|
||||
"name": member_name,
|
||||
"datatype": member_dtype,
|
||||
"remanence": member_remanence,
|
||||
"accessibility": member_accessibility,
|
||||
"start_value": None,
|
||||
"comment": None,
|
||||
"children": [],
|
||||
"array_elements": {},
|
||||
}
|
||||
comment_node = member.xpath("./iface:Comment", namespaces=ns)
|
||||
if comment_node:
|
||||
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:
|
||||
constant_name = start_value_node[0].get("ConstantName")
|
||||
member_info["start_value"] = (
|
||||
constant_name
|
||||
if constant_name
|
||||
else (
|
||||
start_value_node[0].text
|
||||
if start_value_node[0].text is not None
|
||||
else None
|
||||
)
|
||||
) # Devolver None si está vacío
|
||||
nested_sections = member.xpath(
|
||||
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
|
||||
)
|
||||
if nested_sections:
|
||||
member_info["children"] = parse_interface_members(nested_sections)
|
||||
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")
|
||||
sub_start_value_node = sub.xpath("./iface:StartValue", namespaces=ns)
|
||||
if path and sub_start_value_node:
|
||||
constant_name = sub_start_value_node[0].get("ConstantName")
|
||||
value = (
|
||||
constant_name
|
||||
if constant_name
|
||||
else (
|
||||
sub_start_value_node[0].text
|
||||
if sub_start_value_node[0].text is not None
|
||||
else None
|
||||
)
|
||||
) # Devolver None si está vacío
|
||||
member_info["array_elements"][path] = value
|
||||
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
|
514
paste.py
514
paste.py
|
@ -1,184 +1,344 @@
|
|||
# processors/process_call.py
|
||||
# x3_generate_scl.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import sympy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
import sys
|
||||
import traceback
|
||||
# Asumiendo que estas funciones ahora existen y están adaptadas
|
||||
from .processor_utils import get_sympy_representation, sympy_expr_to_scl, format_variable_name, get_target_scl_name
|
||||
from .symbol_manager import SymbolManager # Necesitamos pasar el symbol_manager
|
||||
|
||||
# Definir sufijo globalmente o importar
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
# --- Importar Utilidades y Constantes ---
|
||||
try:
|
||||
from processors.processor_utils import format_variable_name
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||
except ImportError:
|
||||
print("Advertencia: No se pudo importar 'format_variable_name'. Usando fallback.")
|
||||
def format_variable_name(name): # Fallback
|
||||
if not name: return "_INVALID_NAME_"
|
||||
if name.startswith('"') and name.endswith('"'): return name
|
||||
prefix = "#" if name.startswith("#") else ""
|
||||
if prefix: name = name[1:]
|
||||
if name and name[0].isdigit(): name = "_" + name
|
||||
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
||||
return prefix + name
|
||||
SCL_SUFFIX = "_sympy_processed"
|
||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||
|
||||
def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||
instr_uid = instruction["instruction_uid"]
|
||||
# Get original type before potential suffix/error was added by x1 or previous passes
|
||||
# This requires storing the original type perhaps, or removing known suffixes
|
||||
# Let's assume 'block_type' (FC/FB) and 'block_name' are correct from x1
|
||||
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
|
||||
block_type = instruction.get("block_type") # FC, FB
|
||||
instance_db = instruction.get("instance_db") # Nombre del DB de instancia (para FB)
|
||||
|
||||
# Check if already processed
|
||||
if instruction.get("type", "").endswith(SCL_SUFFIX) or "_error" in instruction.get("type", ""):
|
||||
return False
|
||||
|
||||
# Formatear nombres SCL (para la llamada final)
|
||||
block_name_scl = format_variable_name(block_name)
|
||||
instance_db_scl = format_variable_name(instance_db) if instance_db else None
|
||||
|
||||
# --- Manejo de EN ---
|
||||
en_input = instruction["inputs"].get("en")
|
||||
sympy_en_expr = get_sympy_representation(en_input, network_id, sympy_map, symbol_manager) if en_input else sympy.true
|
||||
|
||||
if sympy_en_expr is None:
|
||||
# print(f"DEBUG Call {instr_uid}: EN dependency not ready.")
|
||||
return False # Dependencia EN no resuelta
|
||||
|
||||
# --- Procesar Parámetros de Entrada ---
|
||||
scl_call_params = []
|
||||
processed_inputs = {"en"} # Track processed pins to avoid duplicates if 'en' is also listed elsewhere
|
||||
dependencies_resolved = True
|
||||
|
||||
# Iterar sobre las entradas que x1 debería haber poblado
|
||||
# Ordenar por nombre de pin para consistencia en la llamada SCL
|
||||
input_pin_names = sorted(instruction.get("inputs", {}).keys())
|
||||
|
||||
for pin_name in input_pin_names:
|
||||
if pin_name not in processed_inputs: # Skip 'en' if already handled
|
||||
source_info = instruction["inputs"][pin_name]
|
||||
|
||||
# Get the representation of the source (SymPy, constant, or SCL string)
|
||||
source_sympy_or_const = get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
|
||||
|
||||
if source_sympy_or_const is None:
|
||||
# print(f"DEBUG Call {instr_uid}: Input param '{pin_name}' dependency not ready.")
|
||||
dependencies_resolved = False
|
||||
break # Exit if one dependency is not ready
|
||||
|
||||
# Convert the expression/constant to SCL for the call
|
||||
# Simplification of inputs is generally not needed here, convert directly
|
||||
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
|
||||
|
||||
# Parameter pin name needs formatting for SCL
|
||||
pin_name_scl = format_variable_name(pin_name)
|
||||
|
||||
# Special check for DB_ANY or ANY_POINTER - pass name directly without :=
|
||||
# We need the original parameter type info for this, which is not in the simplified JSON.
|
||||
# WORKAROUND: Check if param_scl_value looks like a DB name ("DB_NAME")
|
||||
# This is heuristic and might be wrong. Ideally, x1 should pass type info.
|
||||
# For now, we assume standard 'Param := Value' syntax.
|
||||
# if param_scl_value.startswith('"') and param_scl_value.endswith('"') and block_type == "FC": # Heuristic for DB_ANY?
|
||||
# scl_call_params.append(f"{pin_name_scl} := {param_scl_value}") # Still use := for clarity? TIA might infer
|
||||
# else:
|
||||
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
|
||||
|
||||
processed_inputs.add(pin_name)
|
||||
|
||||
if not dependencies_resolved:
|
||||
return False
|
||||
|
||||
# --- Construcción de la Llamada SCL (con parámetros) ---
|
||||
scl_call_body = ""
|
||||
param_string = ", ".join(scl_call_params) # Join parameters with commas
|
||||
|
||||
if block_type == "FB":
|
||||
if not instance_db_scl:
|
||||
print(f"Error: Call FB '{block_name_scl}' (UID {instr_uid}) sin instancia.")
|
||||
instruction["scl"] = f"// ERROR: FB Call {block_name_scl} sin instancia"
|
||||
instruction["type"] = f"Call_FB_error" # Mark with error
|
||||
return True # Processed (with error)
|
||||
# FB Call: InstanceName(Param1 := Value1, Param2 := Value2);
|
||||
scl_call_body = f"{instance_db_scl}({param_string});"
|
||||
elif block_type == "FC":
|
||||
# FC Call: BlockName(Param1 := Value1, Param2 := Value2);
|
||||
scl_call_body = f"{block_name_scl}({param_string});"
|
||||
else:
|
||||
print(f"Advertencia: Tipo de bloque no soportado para Call UID {instr_uid}: {block_type}")
|
||||
scl_call_body = f"// ERROR: Call a bloque tipo '{block_type}' no soportado: {block_name_scl}"
|
||||
# Mark instruction type with error
|
||||
instruction["type"] = f"Call_{block_type or 'Unknown'}_error" # Add specific type if known
|
||||
|
||||
|
||||
# --- Aplicar Condición EN (usando la expresión SymPy EN) ---
|
||||
scl_final = ""
|
||||
if sympy_en_expr != sympy.true:
|
||||
# Simplify the EN condition before converting to SCL
|
||||
# --- Función format_scl_start_value (SIN CAMBIOS) ---
|
||||
def format_scl_start_value(value, datatype):
|
||||
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]
|
||||
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}'"
|
||||
elif "bool" in datatype_lower: return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
||||
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}'"
|
||||
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
||||
try:
|
||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||
except Exception as e:
|
||||
print(f"Error simplifying EN for Call {instr_uid} ({block_name_scl}): {e}")
|
||||
simplified_en_expr = sympy_en_expr # Fallback
|
||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
||||
|
||||
# Avoid IF TRUE/FALSE blocks
|
||||
if en_condition_scl == "TRUE":
|
||||
scl_final = scl_call_body
|
||||
elif en_condition_scl == "FALSE":
|
||||
scl_final = f"// Call {block_name_scl} (UID {instr_uid}) condition simplified to FALSE."
|
||||
# Also update type to avoid further processing?
|
||||
# instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}_Optimized"
|
||||
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}'"
|
||||
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}"
|
||||
elif "date" in datatype_lower:
|
||||
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 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}"
|
||||
else: # Fallback
|
||||
if re.match(r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str):
|
||||
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: return value_str[1:-1]
|
||||
if '"' in value_str and "." in value_str and value_str.count('"') == 2: return value_str
|
||||
if not value_str.startswith('"') and not value_str.startswith("'"):
|
||||
if value_str.startswith("#") or value_str.startswith("%"): return value_str
|
||||
else: return value_str
|
||||
return value_str
|
||||
else:
|
||||
# Indent the call body within the IF block
|
||||
indented_call = "\n".join([f" {line}" for line in scl_call_body.splitlines()])
|
||||
scl_final = f"IF {en_condition_scl} THEN\n{indented_call}\nEND_IF;"
|
||||
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'"
|
||||
|
||||
# --- Función generate_scl_declarations (SIN CAMBIOS) ---
|
||||
def generate_scl_declarations(variables, indent_level=1):
|
||||
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")
|
||||
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 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, base_type_for_init = match.group(1), match.group(2).strip()
|
||||
declaration_dtype = var_dtype_raw
|
||||
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl:
|
||||
if 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:
|
||||
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
|
||||
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 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}*/")
|
||||
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: Todos los valores iniciales para array '{var_name_scl}' son None o 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:
|
||||
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: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"); init_value_scl = f"/*ERR_FMT_SIMPLE*/"
|
||||
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
|
||||
|
||||
# --- 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("")
|
||||
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)) # Llamada a la función auxiliar
|
||||
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 " ";
|
||||
# CORRECCIÓN: Manejar el caso en que comment sea None
|
||||
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
|
||||
|
||||
# --- Función Principal de Generación (SIN CAMBIOS EN LA LÓGICA PRINCIPAL) ---
|
||||
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.
|
||||
"""
|
||||
if not os.path.exists(processed_json_filepath): print(f"Error: Archivo JSON no encontrado: '{processed_json_filepath}'"); return
|
||||
print(f"Cargando JSON procesado desde: {processed_json_filepath}")
|
||||
try:
|
||||
with open(processed_json_filepath, "r", encoding="utf-8") as f: data = json.load(f)
|
||||
except Exception as e: print(f"Error al cargar o parsear JSON: {e}"); traceback.print_exc(); return
|
||||
|
||||
block_name = data.get("block_name", "UnknownBlock"); block_number = data.get("block_number")
|
||||
block_type = data.get("block_type", "Unknown"); block_comment = data.get("block_comment", "")
|
||||
scl_block_name = format_variable_name(block_name); output_content = []; output_extension = ".scl"
|
||||
print(f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})")
|
||||
|
||||
if block_type == "PlcUDT":
|
||||
print(" -> Modo de generación: UDT Markdown"); output_content = generate_udt_markdown(data); output_extension = ".md"
|
||||
elif block_type == "PlcTagTable":
|
||||
print(" -> Modo de generación: Tag Table Markdown"); output_content = generate_tag_table_markdown(data); output_extension = ".md"
|
||||
elif block_type == "GlobalDB":
|
||||
print(" -> Modo de generación: DATA_BLOCK SCL"); output_extension = ".scl"
|
||||
# (Lógica SCL DB...)
|
||||
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:"); [output_content.append(f"// {line}") for line in block_comment.splitlines()]
|
||||
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")
|
||||
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"); output_extension = ".scl"
|
||||
# (Lógica SCL FC/FB/OB...)
|
||||
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:"); [output_content.append(f"// {line}") for line in block_comment.splitlines()]
|
||||
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: [output_content.append(f" // {line}") for line in network_comment.splitlines()]
|
||||
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} ---"); [output_content.append(f" // {stl_line}") for stl_line in raw_stl_code.splitlines()]; 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; [output_content.append(f" {line}") for line in scl_code.splitlines()]; 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: print(f"Error: Tipo de bloque desconocido '{block_type}' en JSON."); return
|
||||
|
||||
# --- Escritura del Archivo ---
|
||||
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:
|
||||
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(f"Generación de {output_extension.upper()} completada.")
|
||||
except Exception as e: print(f"Error al escribir el archivo {output_extension.upper()}: {e}"); traceback.print_exc()
|
||||
|
||||
# --- Ejecución (SIN CAMBIOS) ---
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file from processed JSON.")
|
||||
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")
|
||||
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)
|
||||
else:
|
||||
# No IF needed if EN is always TRUE
|
||||
scl_final = scl_call_body
|
||||
|
||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
||||
instruction["scl"] = scl_final # Guardar el SCL final generado
|
||||
|
||||
# Update instruction type to mark as processed (unless already marked as error)
|
||||
if "_error" not in instruction.get("type", ""):
|
||||
instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}"
|
||||
|
||||
# Propagar el estado ENO (es la expresión SymPy de EN)
|
||||
map_key_eno = (network_id, instr_uid, "eno")
|
||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||
|
||||
# --- Propagar Valores de Salida (Importante pero complejo) ---
|
||||
# Esto requiere conocer la interfaz del bloque llamado (que no tenemos aquí directamente)
|
||||
# O asumir convenciones estándar (ej. FCs tienen Ret_Val, FBs tienen outputs en su instancia)
|
||||
|
||||
# Heurística simple: Si es un FC, intentar propagar Ret_Val si existe en outputs
|
||||
# Si es un FB, las salidas se acceden a través de la instancia (e.g., "MyInstance".Output1)
|
||||
# Por ahora, dejaremos la propagación de salidas más avanzada para una mejora futura
|
||||
# o requerirá pasar información de la interfaz del bloque llamado.
|
||||
|
||||
# Ejemplo básico (necesita mejorar):
|
||||
# for pin_name, dest_list in instruction.get("outputs", {}).items():
|
||||
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
||||
# map_key_out = (network_id, instr_uid, pin_name)
|
||||
# pin_name_scl = format_variable_name(pin_name)
|
||||
# if block_type == "FB" and instance_db_scl:
|
||||
# # Salida de FB: "Instancia".NombrePin
|
||||
# output_scl_access = f"{instance_db_scl}.{pin_name_scl}"
|
||||
# # Podríamos guardar el string SCL o crear/obtener un Symbol
|
||||
# sympy_out_symbol = symbol_manager.get_symbol(output_scl_access)
|
||||
# sympy_map[map_key_out] = sympy_out_symbol if sympy_out_symbol else output_scl_access # Prefiere Symbol
|
||||
# elif block_type == "FC":
|
||||
# # Salida de FC: Requiere asignar a una variable (temporal o de interfaz)
|
||||
# # Esto se complica porque el destino está en 'dest_list'
|
||||
# if len(dest_list) == 1 and dest_list[0].get("type") == "variable":
|
||||
# target_var_name = format_variable_name(dest_list[0].get("name"))
|
||||
# # Guardar el nombre del destino SCL que contendrá el valor
|
||||
# sympy_map[map_key_out] = target_var_name
|
||||
# # Necesitaríamos modificar scl_final para incluir la asignación:
|
||||
# # target_var_name := FC_Call(...); (requiere reestructurar la generación SCL)
|
||||
# else:
|
||||
# # Múltiples destinos o destino no variable es complejo para FC outputs
|
||||
# sympy_map[map_key_out] = f"/* TODO: Assign FC output {pin_name_scl} */"
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# --- Processor Information Function ---
|
||||
def get_processor_info():
|
||||
"""Devuelve la información para las llamadas a FC y FB."""
|
||||
# Asegurarse que los type_name coincidan con los usados en x1 y x2
|
||||
return [
|
||||
{'type_name': 'call_fc', 'processor_func': process_call, 'priority': 6}, # Prioridad alta
|
||||
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6} # Prioridad alta
|
||||
]
|
||||
try: 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)
|
1955
x1_to_json.py
1955
x1_to_json.py
File diff suppressed because it is too large
Load Diff
286
x2_process.py
286
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,10 +299,11 @@ 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) ---
|
||||
|
||||
# --- Bucle Principal de Procesamiento (MODIFICADO) ---
|
||||
def process_json_to_scl(json_filepath):
|
||||
"""
|
||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
|
||||
y guarda JSON procesado.
|
||||
"""
|
||||
global data
|
||||
|
@ -320,32 +320,30 @@ def process_json_to_scl(json_filepath):
|
|||
traceback.print_exc()
|
||||
return
|
||||
|
||||
# --- 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)
|
||||
# --- 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 DB (sin cambios lógicos) en: {output_filename}")
|
||||
print(f"Guardando JSON de {block_type} (sin cambios lógicos) en: {output_filename}")
|
||||
try:
|
||||
with open(output_filename, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
print("Guardado de DB completado.")
|
||||
print(f"Guardado de {block_type} completado.")
|
||||
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 # <<< SALIR TEMPRANO PARA DB/UDT/TAG TABLE
|
||||
|
||||
# --- 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)
|
||||
|
@ -354,42 +352,30 @@ def process_json_to_scl(json_filepath):
|
|||
return
|
||||
|
||||
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 +384,140 @@ 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
|
||||
output_filename = json_filepath.replace("_simplified.json", "_simplified_processed.json")
|
||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}")
|
||||
try:
|
||||
with open(output_filename, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
with open(output_filename, "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()
|
||||
except Exception as e: print(f"Error Crítico al guardar JSON procesado: {e}"); traceback.print_exc()
|
||||
|
||||
|
||||
# --- Ejecución (sin cambios en esta parte) ---
|
||||
# --- Ejecución (SIN CAMBIOS) ---
|
||||
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 (_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).")
|
||||
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_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")
|
||||
|
||||
# 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)}'")
|
||||
|
||||
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 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)
|
||||
else:
|
||||
# Llamar a la función principal de procesamiento del script
|
||||
try:
|
||||
process_json_to_scl(input_json_file)
|
||||
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}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1) # Salir con error si la función principal falla
|
||||
sys.exit(1)
|
|
@ -381,11 +381,103 @@ def generate_scl_declarations(variables, indent_level=1):
|
|||
|
||||
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("")
|
||||
|
||||
# --- 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
|
||||
# Extraer miembros (asumiendo que están en interface['None'])
|
||||
members = data.get("interface", {}).get("None", [])
|
||||
if members:
|
||||
md_lines.append("## Members")
|
||||
md_lines.append("")
|
||||
md_lines.append("| Name | Datatype | Start Value | Comment |")
|
||||
md_lines.append("|---|---|---|---|")
|
||||
# Usar una función auxiliar recursiva para manejar structs anidados
|
||||
md_lines.extend(generate_markdown_member_rows(members))
|
||||
md_lines.append("")
|
||||
else:
|
||||
md_lines.append("No members found in the UDT interface.")
|
||||
md_lines.append("")
|
||||
|
||||
return md_lines
|
||||
|
||||
# --- generate_markdown_member_rows (MODIFICADA) ---
|
||||
def generate_markdown_member_rows(members, level=0):
|
||||
"""Función auxiliar recursiva para generar filas Markdown para miembros de UDT."""
|
||||
md_rows = []; prefix = " " * level
|
||||
for member in members:
|
||||
name = member.get("name", "N/A"); datatype = member.get("datatype", "N/A")
|
||||
start_value_raw = member.get("start_value")
|
||||
start_value_fmt = format_scl_start_value(start_value_raw, datatype) if start_value_raw is not None else ""
|
||||
# CORRECCIÓN: Manejar el caso en que comment sea None
|
||||
comment_raw = member.get("comment")
|
||||
comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else "" # Usar "" si es None
|
||||
|
||||
md_rows.append(f"| {prefix}`{name}` | `{datatype}` | `{start_value_fmt}` | {comment} |")
|
||||
children = member.get("children")
|
||||
if children: md_rows.extend(generate_markdown_member_rows(children, level + 1))
|
||||
array_elements = member.get("array_elements")
|
||||
if array_elements:
|
||||
base_type_for_init = datatype
|
||||
if isinstance(datatype, str) and datatype.lower().startswith("array["):
|
||||
match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", datatype, re.IGNORECASE)
|
||||
if match: base_type_for_init = match.group(2).strip()
|
||||
md_rows.append(f"| {prefix} *(Initial Values)* | | | |")
|
||||
try:
|
||||
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
||||
sorted_indices_str = [str(k) for k in sorted(indices_numeric.keys())]
|
||||
except ValueError: sorted_indices_str = sorted(array_elements.keys())
|
||||
for idx_str in sorted_indices_str:
|
||||
val_raw = array_elements[idx_str]
|
||||
val_fmt = format_scl_start_value(val_raw, base_type_for_init) if val_raw is not None else ""
|
||||
md_rows.append(f"| {prefix} `[{idx_str}]` | | `{val_fmt}` | |")
|
||||
return md_rows
|
||||
|
||||
def generate_tag_table_markdown(data):
|
||||
"""Genera contenido Markdown para una tabla de tags."""
|
||||
md_lines = []
|
||||
table_name = data.get("block_name", "UnknownTagTable")
|
||||
tags = data.get("tags", [])
|
||||
|
||||
md_lines.append(f"# Tag Table: {table_name}")
|
||||
md_lines.append("")
|
||||
|
||||
if tags:
|
||||
md_lines.append("| Name | Datatype | Address | Comment |")
|
||||
md_lines.append("|---|---|---|---|")
|
||||
for tag in tags:
|
||||
name = tag.get("name", "N/A")
|
||||
datatype = tag.get("datatype", "N/A")
|
||||
address = tag.get("address", "N/A") or " " # Evitar None en la tabla
|
||||
comment = (
|
||||
tag.get("comment", "").replace("|", "\|").replace("\n", " ")
|
||||
) # Escapar pipes
|
||||
|
||||
md_lines.append(f"| `{name}` | `{datatype}` | `{address}` | {comment} |")
|
||||
md_lines.append("")
|
||||
else:
|
||||
md_lines.append("No tags found in this table.")
|
||||
md_lines.append("")
|
||||
|
||||
return md_lines
|
||||
|
||||
|
||||
# --- Función Principal de Generación (MODIFICADA) ---
|
||||
def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||
"""
|
||||
Genera un archivo SCL o Markdown a partir del JSON procesado,
|
||||
eligiendo el formato y la extensión según el tipo de bloque.
|
||||
"""
|
||||
if not os.path.exists(processed_json_filepath):
|
||||
print(
|
||||
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
|
||||
|
@ -401,106 +493,98 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
traceback.print_exc()
|
||||
return
|
||||
|
||||
# --- Extracción de Información del Bloque (Común) ---
|
||||
# --- Extracción de Información y Determinación de Tipo ---
|
||||
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
|
||||
) # FC, FB, OB, GlobalDB, PlcUDT, PlcTagTable
|
||||
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 = []
|
||||
scl_block_name = format_variable_name(block_name)
|
||||
output_content = []
|
||||
output_extension = ".scl" # Default
|
||||
|
||||
# --- 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}")
|
||||
print(
|
||||
f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})"
|
||||
)
|
||||
|
||||
# --- Selección del Generador y Extensión ---
|
||||
if block_type == "PlcUDT":
|
||||
print(" -> Modo de generación: UDT Markdown")
|
||||
output_content = generate_udt_markdown(data)
|
||||
output_extension = ".md"
|
||||
elif block_type == "PlcTagTable":
|
||||
print(" -> Modo de generación: Tag Table Markdown")
|
||||
output_content = generate_tag_table_markdown(data)
|
||||
output_extension = ".md"
|
||||
elif block_type == "GlobalDB":
|
||||
print(" -> Modo de generación: DATA_BLOCK SCL")
|
||||
output_extension = ".scl"
|
||||
# (Lógica de generación SCL para DB como estaba antes)
|
||||
output_content.append(f"// Block Type: {block_type}")
|
||||
if block_name != scl_block_name:
|
||||
output_content.append(f"// Block Name (Original): {block_name}")
|
||||
if block_number:
|
||||
scl_output.append(f"// Block Number: {block_number}")
|
||||
output_content.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("")
|
||||
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", {})
|
||||
# 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("")
|
||||
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."
|
||||
)
|
||||
# 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")
|
||||
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")
|
||||
|
||||
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
|
||||
else:
|
||||
# Determinar palabra clave SCL
|
||||
scl_block_keyword = "FUNCTION_BLOCK" # Default
|
||||
elif block_type in ["FC", "FB", "OB"]:
|
||||
print(f" -> Modo de generación: {block_type} SCL")
|
||||
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"
|
||||
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}")
|
||||
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:
|
||||
scl_output.append(f"// Block Number: {block_number}")
|
||||
# Indicar lenguaje original de las redes si es relevante
|
||||
output_content.append(f"// Block Number: {block_number}")
|
||||
original_net_langs = set(
|
||||
n.get("language", "Unknown") for n in data.get("networks", [])
|
||||
)
|
||||
scl_output.append(
|
||||
output_content.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("")
|
||||
output_content.append(f"// Block Comment:")
|
||||
for line in block_comment.splitlines():
|
||||
output_content.append(f"// {line}")
|
||||
output_content.append("")
|
||||
|
||||
# Manejar tipo de retorno para FUNCTION (FC)
|
||||
return_type = "Void" # Default
|
||||
return_type = "Void"
|
||||
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)
|
||||
|
@ -508,177 +592,127 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
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
|
||||
else:
|
||||
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}"')
|
||||
output_content.append(
|
||||
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
|
||||
)
|
||||
else:
|
||||
output_content.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("")
|
||||
output_content.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||
output_content.append("VERSION : 0.1")
|
||||
output_content.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
|
||||
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
|
||||
# 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
|
||||
scl_section_keyword = "VAR_STAT"
|
||||
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(
|
||||
scl_section_keyword = "CONSTANT"
|
||||
output_content.append(scl_section_keyword)
|
||||
output_content.extend(
|
||||
generate_scl_declarations(vars_in_section, indent_level=1)
|
||||
)
|
||||
# Añadir END_VAR (o END_CONSTANT)
|
||||
scl_output.append(
|
||||
output_content.append(
|
||||
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
||||
)
|
||||
scl_output.append("") # Línea en blanco
|
||||
|
||||
# Guardar nombres de Temp declarados explícitamente
|
||||
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")
|
||||
)
|
||||
# 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
|
||||
|
||||
temp_vars_detected = set()
|
||||
temp_pattern = re.compile(r'"?(#\w+)"?')
|
||||
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
|
||||
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:
|
||||
# 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")
|
||||
|
||||
output_content.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(
|
||||
inferred_type = "Bool"
|
||||
output_content.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("")
|
||||
output_content.append("END_VAR")
|
||||
output_content.append("")
|
||||
|
||||
# --- Cuerpo del Bloque (BEGIN...END) ---
|
||||
scl_output.append("BEGIN")
|
||||
scl_output.append("")
|
||||
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
|
||||
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)}'
|
||||
) # Usar i+1 si falta ID
|
||||
network_title = network.get("title", f'Network {network.get("id", i+1)}')
|
||||
network_comment = network.get("comment", "")
|
||||
network_lang = network.get("language", "LAD") # Lenguaje original de la red
|
||||
scl_output.append(
|
||||
network_lang = network.get("language", "LAD")
|
||||
output_content.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
|
||||
|
||||
output_content.append(f" // {line}")
|
||||
output_content.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("")
|
||||
output_content.append(f" // Network {i+1} has no logic elements.")
|
||||
output_content.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
|
||||
output_content.append(f" // --- BEGIN STL Network {i+1} ---")
|
||||
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
|
||||
output_content.append(f" // {stl_line}")
|
||||
output_content.append(f" // --- END STL Network {i+1} ---")
|
||||
output_content.append("")
|
||||
else:
|
||||
scl_output.append(
|
||||
output_content.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
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
@ -688,18 +722,14 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
"UNSUPPORTED_CONTENT",
|
||||
"PARSING_ERROR",
|
||||
]
|
||||
or "_error" in instruction_type # Incluir errores comentados
|
||||
or "_error" in instruction_type
|
||||
) 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
|
||||
|
@ -713,78 +743,72 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
|||
):
|
||||
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."
|
||||
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."
|
||||
)
|
||||
scl_output.append("")
|
||||
output_content.append("")
|
||||
output_content.append(f"END_{scl_block_keyword}")
|
||||
|
||||
# Fin del bloque FC/FB/OB
|
||||
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
|
||||
else: # Tipo desconocido
|
||||
print(
|
||||
f"Error: Tipo de bloque desconocido '{block_type}' encontrado en JSON. No se generará archivo."
|
||||
)
|
||||
return
|
||||
|
||||
# --- Escritura del Archivo SCL (Común) ---
|
||||
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
|
||||
# --- 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
|
||||
)
|
||||
output_filepath = os.path.join(output_directory, output_filename_base)
|
||||
|
||||
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:
|
||||
# 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:
|
||||
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."
|
||||
description="Generate final SCL or Markdown file from processed JSON (_simplified_processed.json)." # Actualizado
|
||||
)
|
||||
parser.add_argument(
|
||||
"source_xml_filepath", # Argumento posicional obligatorio
|
||||
"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() # Parsea los argumentos de sys.argv
|
||||
args = parser.parse_args()
|
||||
source_xml_file = args.source_xml_filepath
|
||||
|
||||
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
|
||||
|
||||
# Verificar si el archivo XML original existe (como referencia)
|
||||
if not os.path.exists(source_xml_file):
|
||||
print(
|
||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
||||
)
|
||||
|
||||
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||
# Asumir que los archivos están en el mismo directorio que el XML original
|
||||
base_dir = os.path.dirname(source_xml_file) # Directorio del XML original
|
||||
base_dir = os.path.dirname(source_xml_file)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# 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: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_scl_file)}'"
|
||||
)
|
||||
f"(x3) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}' en directorio: '{os.path.relpath(output_dir)}'"
|
||||
) # Log actualizado
|
||||
|
||||
# Verificar si el archivo JSON procesado de entrada EXISTE
|
||||
if not os.path.exists(input_json_file):
|
||||
print(
|
||||
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
|
||||
|
@ -792,16 +816,15 @@ if __name__ == "__main__":
|
|||
print(
|
||||
f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
|
||||
)
|
||||
sys.exit(1) # Salir si el archivo necesario no está
|
||||
sys.exit(1)
|
||||
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
|
||||
# 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 desde '{input_json_file}': {e}"
|
||||
f"Error Crítico (x3) durante la generación de SCL/MD 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
|
||||
sys.exit(1)
|
||||
|
|
Loading…
Reference in New Issue