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):
|
def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManager, data):
|
||||||
instr_uid = instruction["instruction_uid"]
|
instr_uid = instruction["instruction_uid"]
|
||||||
# Get original type before potential suffix/error was added by x1 or previous passes
|
instr_type_original = instruction.get("type", "") # Tipo antes de añadir sufijo
|
||||||
# This requires storing the original type perhaps, or removing known suffixes
|
if instr_type_original.endswith(SCL_SUFFIX) or "_error" in instr_type_original:
|
||||||
# Let's assume 'block_type' (FC/FB) and 'block_name' are correct from x1
|
return False
|
||||||
|
|
||||||
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
|
block_name = instruction.get("block_name", f"UnknownCall_{instr_uid}")
|
||||||
block_type = instruction.get("block_type") # FC, FB
|
block_type = instruction.get("block_type") # FC, FB
|
||||||
instance_db = instruction.get("instance_db") # Nombre del DB de instancia (para 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)
|
# Formatear nombres SCL (para la llamada final)
|
||||||
block_name_scl = format_variable_name(block_name)
|
block_name_scl = format_variable_name(block_name)
|
||||||
instance_db_scl = format_variable_name(instance_db) if instance_db else None
|
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 ---
|
# --- Procesar Parámetros de Entrada ---
|
||||||
scl_call_params = []
|
scl_call_params = []
|
||||||
processed_inputs = {"en"} # Track processed pins to avoid duplicates if 'en' is also listed elsewhere
|
processed_inputs = {"en"}
|
||||||
dependencies_resolved = True
|
dependencies_resolved = True
|
||||||
|
|
||||||
# Iterar sobre las entradas que x1 debería haber poblado
|
# Ordenar para consistencia
|
||||||
# Ordenar por nombre de pin para consistencia en la llamada SCL
|
|
||||||
input_pin_names = sorted(instruction.get("inputs", {}).keys())
|
input_pin_names = sorted(instruction.get("inputs", {}).keys())
|
||||||
|
|
||||||
for pin_name in input_pin_names:
|
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]
|
source_info = instruction["inputs"][pin_name]
|
||||||
|
# Obtener la representación de la fuente (puede ser SymPy o Constante/String)
|
||||||
# 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)
|
source_sympy_or_const = get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
|
||||||
|
|
||||||
if source_sympy_or_const is None:
|
if source_sympy_or_const is None:
|
||||||
# print(f"DEBUG Call {instr_uid}: Input param '{pin_name}' dependency not ready.")
|
# print(f"DEBUG Call {instr_uid}: Input param '{pin_name}' dependency not ready.")
|
||||||
dependencies_resolved = False
|
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
|
# Convertir la expresión/constante a SCL para la llamada
|
||||||
# Simplification of inputs is generally not needed here, convert directly
|
# 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)
|
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)
|
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}")
|
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
|
||||||
|
|
||||||
processed_inputs.add(pin_name)
|
processed_inputs.add(pin_name)
|
||||||
|
|
||||||
if not dependencies_resolved:
|
if not dependencies_resolved:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# --- Construcción de la Llamada SCL (con parámetros) ---
|
# --- Construcción de la Llamada SCL (similar a antes) ---
|
||||||
scl_call_body = ""
|
scl_call_body = ""
|
||||||
param_string = ", ".join(scl_call_params) # Join parameters with commas
|
param_string = ", ".join(scl_call_params)
|
||||||
|
|
||||||
if block_type == "FB":
|
if block_type == "FB":
|
||||||
if not instance_db_scl:
|
if not instance_db_scl:
|
||||||
print(f"Error: Call FB '{block_name_scl}' (UID {instr_uid}) sin instancia.")
|
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["scl"] = f"// ERROR: FB Call {block_name_scl} sin instancia"
|
||||||
instruction["type"] = f"Call_FB_error" # Mark with error
|
instruction["type"] = f"Call_FB_error"
|
||||||
return True # Processed (with error)
|
return True
|
||||||
# FB Call: InstanceName(Param1 := Value1, Param2 := Value2);
|
|
||||||
scl_call_body = f"{instance_db_scl}({param_string});"
|
scl_call_body = f"{instance_db_scl}({param_string});"
|
||||||
elif block_type == "FC":
|
elif block_type == "FC":
|
||||||
# FC Call: BlockName(Param1 := Value1, Param2 := Value2);
|
|
||||||
scl_call_body = f"{block_name_scl}({param_string});"
|
scl_call_body = f"{block_name_scl}({param_string});"
|
||||||
else:
|
else:
|
||||||
print(f"Advertencia: Tipo de bloque no soportado para Call UID {instr_uid}: {block_type}")
|
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}"
|
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}_error" # Marcar como 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) ---
|
# --- Aplicar Condición EN (usando la expresión SymPy EN) ---
|
||||||
scl_final = ""
|
scl_final = ""
|
||||||
if sympy_en_expr != sympy.true:
|
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:
|
try:
|
||||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
||||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
||||||
except Exception as e:
|
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
|
simplified_en_expr = sympy_en_expr # Fallback
|
||||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
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()])
|
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;"
|
scl_final = f"IF {en_condition_scl} THEN\n{indented_call}\nEND_IF;"
|
||||||
else:
|
else:
|
||||||
# No IF needed if EN is always TRUE
|
|
||||||
scl_final = scl_call_body
|
scl_final = scl_call_body
|
||||||
|
|
||||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
# --- Actualizar Instrucción y Mapa SymPy ---
|
||||||
instruction["scl"] = scl_final # Guardar el SCL final generado
|
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)
|
# Actualizar sympy_map con el estado ENO (es la expresión SymPy de EN)
|
||||||
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")
|
map_key_eno = (network_id, instr_uid, "eno")
|
||||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
||||||
|
|
||||||
# --- Propagar Valores de Salida (Importante pero complejo) ---
|
# Propagar valores de salida (requiere info de interfaz o heurística)
|
||||||
# Esto requiere conocer la interfaz del bloque llamado (que no tenemos aquí directamente)
|
# Si se sabe que hay una salida 'MyOutput', se podría añadir su SCL al mapa
|
||||||
# O asumir convenciones estándar (ej. FCs tienen Ret_Val, FBs tienen outputs en su instancia)
|
# Ejemplo MUY simplificado:
|
||||||
|
|
||||||
# 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():
|
# for pin_name, dest_list in instruction.get("outputs", {}).items():
|
||||||
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
||||||
# map_key_out = (network_id, instr_uid, pin_name)
|
# 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:
|
# if block_type == "FB" and instance_db_scl:
|
||||||
# # Salida de FB: "Instancia".NombrePin
|
# sympy_map[map_key_out] = f"{instance_db_scl}.{format_variable_name(pin_name)}" # Guardar el *string* de acceso SCL
|
||||||
# output_scl_access = f"{instance_db_scl}.{pin_name_scl}"
|
# # Para FCs es más complejo, necesitaría asignación explícita a temp
|
||||||
# # Podríamos guardar el string SCL o crear/obtener un Symbol
|
# # else: # FC output -> necesita temp var
|
||||||
# sympy_out_symbol = symbol_manager.get_symbol(output_scl_access)
|
# # temp_var = generate_temp_var_name(...)
|
||||||
# sympy_map[map_key_out] = sympy_out_symbol if sympy_out_symbol else output_scl_access # Prefiere Symbol
|
# # sympy_map[map_key_out] = temp_var
|
||||||
# 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
|
return True
|
||||||
|
|
||||||
|
@ -177,8 +125,7 @@ def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManag
|
||||||
# --- Processor Information Function ---
|
# --- Processor Information Function ---
|
||||||
def get_processor_info():
|
def get_processor_info():
|
||||||
"""Devuelve la información para las llamadas a FC y FB."""
|
"""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 [
|
return [
|
||||||
{'type_name': 'call_fc', '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} # Prioridad alta
|
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6}
|
||||||
]
|
]
|
|
@ -3,255 +3,163 @@ import subprocess
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import locale
|
import locale
|
||||||
import glob # <--- Importar glob para buscar archivos
|
import glob
|
||||||
|
|
||||||
|
# (Función get_console_encoding y variable CONSOLE_ENCODING como antes)
|
||||||
# (Función get_console_encoding y variable CONSOLE_ENCODING como en la respuesta anterior)
|
|
||||||
def get_console_encoding():
|
def get_console_encoding():
|
||||||
"""Obtiene la codificación preferida de la consola, con fallback."""
|
"""Obtiene la codificación preferida de la consola, con fallback."""
|
||||||
try:
|
try:
|
||||||
return locale.getpreferredencoding(False)
|
return locale.getpreferredencoding(False)
|
||||||
except Exception:
|
except Exception:
|
||||||
return "cp1252"
|
# Fallback común en Windows si falla getpreferredencoding
|
||||||
|
return "cp1252" # O prueba con 'utf-8' si cp1252 da problemas
|
||||||
|
|
||||||
CONSOLE_ENCODING = get_console_encoding()
|
CONSOLE_ENCODING = get_console_encoding()
|
||||||
# Descomenta la siguiente línea si quieres ver la codificación detectada:
|
|
||||||
# print(f"Detected console encoding: {CONSOLE_ENCODING}")
|
# print(f"Detected console encoding: {CONSOLE_ENCODING}")
|
||||||
|
|
||||||
|
# (Función run_script como antes, usando CONSOLE_ENCODING)
|
||||||
# (Función run_script como en la respuesta anterior, usando CONSOLE_ENCODING)
|
|
||||||
def run_script(script_name, xml_arg):
|
def run_script(script_name, xml_arg):
|
||||||
"""Runs a given script with the specified XML file argument."""
|
"""Runs a given script with the specified XML file argument."""
|
||||||
script_path = os.path.join(os.path.dirname(__file__), script_name)
|
# Asegurarse que la ruta al script sea absoluta o relativa al script actual
|
||||||
command = [sys.executable, script_path, xml_arg]
|
script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name)
|
||||||
|
# Usar la ruta absoluta al ejecutable de Python actual
|
||||||
|
python_executable = sys.executable
|
||||||
|
command = [python_executable, script_path, xml_arg] # Usar la ruta absoluta de python
|
||||||
print(f"\n--- Running {script_name} with argument: {xml_arg} ---")
|
print(f"\n--- Running {script_name} with argument: {xml_arg} ---")
|
||||||
try:
|
try:
|
||||||
|
# Ejecutar el proceso hijo
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
command,
|
command,
|
||||||
check=True,
|
check=True, # Lanza excepción si el script falla (return code != 0)
|
||||||
capture_output=True,
|
capture_output=True,# Captura stdout y stderr
|
||||||
text=True,
|
text=True, # Decodifica stdout/stderr como texto
|
||||||
encoding=CONSOLE_ENCODING,
|
encoding=CONSOLE_ENCODING, # Usa la codificación detectada
|
||||||
errors="replace",
|
errors='replace' # Reemplaza caracteres no decodificables
|
||||||
) # 'replace' para evitar errores
|
)
|
||||||
|
|
||||||
|
# Imprimir stdout y stderr si no están vacíos
|
||||||
|
stdout_clean = result.stdout.strip() if result.stdout else ""
|
||||||
|
stderr_clean = result.stderr.strip() if result.stderr else ""
|
||||||
|
|
||||||
# Imprimir stdout y stderr
|
|
||||||
# Eliminar saltos de línea extra al final si existen
|
|
||||||
stdout_clean = result.stdout.strip()
|
|
||||||
stderr_clean = result.stderr.strip()
|
|
||||||
if stdout_clean:
|
if stdout_clean:
|
||||||
print(stdout_clean)
|
print(stdout_clean)
|
||||||
if stderr_clean:
|
if stderr_clean:
|
||||||
print("--- Stderr ---")
|
# Imprimir stderr claramente para errores del script hijo
|
||||||
print(stderr_clean)
|
print(f"--- Stderr ({script_name}) ---", file=sys.stderr) # Imprimir en stderr
|
||||||
print("--------------")
|
print(stderr_clean, file=sys.stderr)
|
||||||
|
print("--------------------------", file=sys.stderr)
|
||||||
|
|
||||||
print(f"--- {script_name} finished successfully ---")
|
print(f"--- {script_name} finished successfully ---")
|
||||||
return True
|
return True # Indicar éxito
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"Error: Script '{script_path}' not found.")
|
# Error si el script python o el ejecutable no se encuentran
|
||||||
|
print(f"Error: Script '{script_path}' or Python executable '{python_executable}' not found.", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error running {script_name}:")
|
# Error si el script hijo devuelve un código de error (ej., sys.exit(1))
|
||||||
print(f"Return code: {e.returncode}")
|
print(f"Error running {script_name}: Script returned non-zero exit code {e.returncode}.", file=sys.stderr)
|
||||||
stdout_decoded = (
|
|
||||||
e.stdout.decode(CONSOLE_ENCODING, errors="replace").strip()
|
# Decodificar e imprimir stdout/stderr del proceso fallido
|
||||||
if isinstance(e.stdout, bytes)
|
stdout_decoded = e.stdout.strip() if e.stdout else ""
|
||||||
else (e.stdout or "").strip()
|
stderr_decoded = e.stderr.strip() if e.stderr else ""
|
||||||
)
|
|
||||||
stderr_decoded = (
|
|
||||||
e.stderr.decode(CONSOLE_ENCODING, errors="replace").strip()
|
|
||||||
if isinstance(e.stderr, bytes)
|
|
||||||
else (e.stderr or "").strip()
|
|
||||||
)
|
|
||||||
if stdout_decoded:
|
if stdout_decoded:
|
||||||
print("--- Stdout ---")
|
print(f"--- Stdout ({script_name}) ---", file=sys.stderr)
|
||||||
print(stdout_decoded)
|
print(stdout_decoded, file=sys.stderr)
|
||||||
if stderr_decoded:
|
if stderr_decoded:
|
||||||
print("--- Stderr ---")
|
print(f"--- Stderr ({script_name}) ---", file=sys.stderr)
|
||||||
print(stderr_decoded)
|
print(stderr_decoded, file=sys.stderr)
|
||||||
print("--------------")
|
print("--------------------------", file=sys.stderr)
|
||||||
return False
|
return False # Indicar fallo
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An unexpected error occurred while running {script_name}: {e}")
|
# Otros errores inesperados
|
||||||
return False
|
print(f"An unexpected error occurred while running {script_name}: {e}", file=sys.stderr)
|
||||||
|
# Imprimir traceback para depuración
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
return False # Indicar fallo
|
||||||
|
|
||||||
|
|
||||||
# --- NUEVA FUNCIÓN PARA SELECCIONAR ARCHIVO ---
|
# --- NO SE NECESITA select_xml_file() si procesamos todos ---
|
||||||
def select_xml_file():
|
|
||||||
"""Busca archivos .xml, los lista y pide al usuario que elija uno."""
|
|
||||||
print("No XML file specified. Searching for XML files in current directory...")
|
|
||||||
# Buscar archivos .xml en el directorio actual (.)
|
|
||||||
xml_files = sorted(glob.glob("*.xml")) # sorted para orden alfabético
|
|
||||||
|
|
||||||
if not xml_files:
|
|
||||||
print("Error: No .xml files found in the current directory.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\nAvailable XML files:")
|
|
||||||
for i, filename in enumerate(xml_files, start=1):
|
|
||||||
print(f" {i}: {filename}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
choice = input(
|
|
||||||
f"Enter the number of the file to process (1-{len(xml_files)}): "
|
|
||||||
)
|
|
||||||
choice_num = int(choice)
|
|
||||||
if 1 <= choice_num <= len(xml_files):
|
|
||||||
selected_file = xml_files[choice_num - 1]
|
|
||||||
print(f"Selected: {selected_file}")
|
|
||||||
return selected_file
|
|
||||||
else:
|
|
||||||
print("Invalid choice. Please enter a number from the list.")
|
|
||||||
except ValueError:
|
|
||||||
print("Invalid input. Please enter a number.")
|
|
||||||
except EOFError: # Manejar si la entrada se cierra inesperadamente
|
|
||||||
print("\nSelection cancelled.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
# --- FIN NUEVA FUNCIÓN ---
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Imports necesarios para esta sección
|
# --- PARTE 1: BUSCAR ARCHIVOS ---
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import glob # Asegúrate de que glob esté importado al principio del archivo
|
|
||||||
|
|
||||||
# Directorio base donde buscar los archivos XML (relativo al script)
|
# Directorio base donde buscar los archivos XML (relativo al script)
|
||||||
base_search_dir = "XML Project"
|
base_search_dir = "XML Project"
|
||||||
script_dir = os.path.dirname(__file__) # Directorio donde está x0_main.py
|
# Obtener la ruta absoluta del directorio donde está x0_main.py
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
xml_project_dir = os.path.join(script_dir, base_search_dir)
|
xml_project_dir = os.path.join(script_dir, base_search_dir)
|
||||||
|
|
||||||
print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'")
|
print(f"Buscando archivos XML recursivamente en: '{xml_project_dir}'")
|
||||||
|
|
||||||
# Verificar si el directorio 'XML Project' existe
|
# Verificar si el directorio 'XML Project' existe
|
||||||
if not os.path.isdir(xml_project_dir):
|
if not os.path.isdir(xml_project_dir):
|
||||||
print(
|
print(f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio.", file=sys.stderr)
|
||||||
f"Error: El directorio '{xml_project_dir}' no existe o no es un directorio."
|
print("Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro.")
|
||||||
)
|
sys.exit(1) # Salir con error
|
||||||
print(
|
|
||||||
"Por favor, crea el directorio 'XML Project' en la misma carpeta que este script y coloca tus archivos XML dentro."
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Buscar todos los archivos .xml recursivamente dentro de xml_project_dir
|
# Buscar todos los archivos .xml recursivamente
|
||||||
# Usamos os.path.join para construir la ruta de búsqueda correctamente
|
|
||||||
# y '**/*.xml' para la recursividad con glob
|
|
||||||
search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
|
search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
|
||||||
xml_files_found = glob.glob(search_pattern, recursive=True)
|
xml_files_found = glob.glob(search_pattern, recursive=True)
|
||||||
|
|
||||||
if not xml_files_found:
|
if not xml_files_found:
|
||||||
print(
|
print(f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.")
|
||||||
f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios."
|
|
||||||
)
|
|
||||||
sys.exit(0) # Salir limpiamente si no hay archivos
|
sys.exit(0) # Salir limpiamente si no hay archivos
|
||||||
|
|
||||||
print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:")
|
print(f"Se encontraron {len(xml_files_found)} archivos XML para procesar:")
|
||||||
# Ordenar para un procesamiento predecible (opcional)
|
xml_files_found.sort() # Ordenar para consistencia
|
||||||
xml_files_found.sort()
|
|
||||||
for xml_file in xml_files_found:
|
for xml_file in xml_files_found:
|
||||||
# Imprimir la ruta relativa desde el directorio del script para claridad
|
|
||||||
print(f" - {os.path.relpath(xml_file, script_dir)}")
|
print(f" - {os.path.relpath(xml_file, script_dir)}")
|
||||||
|
|
||||||
# Scripts a ejecutar en secuencia (asegúrate que los nombres son correctos)
|
# --- PARTE 2: PROCESAR CADA ARCHIVO ---
|
||||||
|
# Scripts a ejecutar en secuencia
|
||||||
script1 = "x1_to_json.py"
|
script1 = "x1_to_json.py"
|
||||||
script2 = "x2_process.py"
|
script2 = "x2_process.py"
|
||||||
script3 = "x3_generate_scl.py"
|
script3 = "x3_generate_scl.py"
|
||||||
|
|
||||||
# Procesar cada archivo encontrado
|
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
failed_count = 0
|
failed_count = 0
|
||||||
for xml_filepath in xml_files_found:
|
|
||||||
print(
|
|
||||||
f"\n--- Iniciando pipeline para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Usar la ruta absoluta para evitar problemas si los scripts cambian de directorio
|
# Procesar cada archivo encontrado en el bucle
|
||||||
|
for xml_filepath in xml_files_found:
|
||||||
|
relative_path = os.path.relpath(xml_filepath, script_dir)
|
||||||
|
print(f"\n--- Iniciando pipeline para: {relative_path} ---")
|
||||||
|
|
||||||
|
# Usar la ruta absoluta para los scripts hijos
|
||||||
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
absolute_xml_filepath = os.path.abspath(xml_filepath)
|
||||||
|
|
||||||
# Ejecutar los scripts en secuencia para el archivo actual
|
# Ejecutar los scripts en secuencia
|
||||||
# La función run_script ya está definida en tu script x0_main.py
|
|
||||||
success = True
|
success = True
|
||||||
if not run_script(script1, absolute_xml_filepath):
|
if not run_script(script1, absolute_xml_filepath):
|
||||||
print(
|
print(f"\nPipeline falló en el script '{script1}' para el archivo: {relative_path}", file=sys.stderr)
|
||||||
f"\nPipeline falló en el script '{script1}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
|
||||||
)
|
|
||||||
success = False
|
success = False
|
||||||
elif not run_script(script2, absolute_xml_filepath):
|
elif not run_script(script2, absolute_xml_filepath):
|
||||||
print(
|
print(f"\nPipeline falló en el script '{script2}' para el archivo: {relative_path}", file=sys.stderr)
|
||||||
f"\nPipeline falló en el script '{script2}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
|
||||||
)
|
|
||||||
success = False
|
success = False
|
||||||
elif not run_script(script3, absolute_xml_filepath):
|
elif not run_script(script3, absolute_xml_filepath):
|
||||||
print(
|
print(f"\nPipeline falló en el script '{script3}' para el archivo: {relative_path}", file=sys.stderr)
|
||||||
f"\nPipeline falló en el script '{script3}' para el archivo: {os.path.relpath(xml_filepath, script_dir)}"
|
|
||||||
)
|
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
|
# Actualizar contadores y mostrar estado
|
||||||
if success:
|
if success:
|
||||||
print(
|
print(f"--- Pipeline completado exitosamente para: {relative_path} ---")
|
||||||
f"--- Pipeline completado exitosamente para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
|
||||||
)
|
|
||||||
processed_count += 1
|
processed_count += 1
|
||||||
else:
|
else:
|
||||||
failed_count += 1
|
failed_count += 1
|
||||||
print(
|
print(f"--- Pipeline falló para: {relative_path} ---", file=sys.stderr) # Indicar fallo
|
||||||
f"--- Pipeline falló para: {os.path.relpath(xml_filepath, script_dir)} ---"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# --- PARTE 3: RESUMEN FINAL ---
|
||||||
print("\n--- Resumen Final del Procesamiento ---")
|
print("\n--- Resumen Final del Procesamiento ---")
|
||||||
print(f"Total de archivos XML encontrados: {len(xml_files_found)}")
|
print(f"Total de archivos XML encontrados: {len(xml_files_found)}")
|
||||||
print(
|
print(f"Archivos procesados exitosamente por el pipeline completo: {processed_count}")
|
||||||
f"Archivos procesados exitosamente por el pipeline completo: {processed_count}"
|
|
||||||
)
|
|
||||||
print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}")
|
print(f"Archivos que fallaron en algún punto del pipeline: {failed_count}")
|
||||||
print("---------------------------------------")
|
print("---------------------------------------")
|
||||||
xml_filename = None
|
|
||||||
|
|
||||||
# Comprobar si se pasó un argumento de línea de comandos
|
# Salir con código 0 si todo fue bien, 1 si hubo fallos
|
||||||
# sys.argv[0] es el nombre del script, sys.argv[1] sería el primer argumento
|
if failed_count > 0:
|
||||||
if len(sys.argv) > 1:
|
|
||||||
# Si hay argumentos, usar argparse para parsearlo (permite -h, etc.)
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Run the Simatic XML processing pipeline."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"xml_file",
|
|
||||||
# Ya no necesitamos nargs='?' ni default aquí porque sabemos que hay un argumento
|
|
||||||
help="Path to the XML file to process.",
|
|
||||||
)
|
|
||||||
# Parsear solo los argumentos conocidos, ignorar extras si los hubiera
|
|
||||||
args, unknown = parser.parse_known_args()
|
|
||||||
xml_filename = args.xml_file
|
|
||||||
print(f"XML file specified via argument: {xml_filename}")
|
|
||||||
else:
|
|
||||||
# Si no hay argumentos, llamar a la función interactiva
|
|
||||||
xml_filename = select_xml_file()
|
|
||||||
|
|
||||||
# --- El resto del script continúa igual, usando xml_filename ---
|
|
||||||
|
|
||||||
# Verificar si el archivo XML de entrada (seleccionado o pasado) existe
|
|
||||||
if not os.path.exists(xml_filename):
|
|
||||||
print(f"Error: Selected or specified XML file not found: {xml_filename}")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"\nStarting pipeline for: {xml_filename}")
|
|
||||||
|
|
||||||
# Run scripts sequentially (asegúrate que los nombres son correctos)
|
|
||||||
script1 = "x1_to_json.py"
|
|
||||||
script2 = "x2_process.py"
|
|
||||||
script3 = "x3_generate_scl.py"
|
|
||||||
|
|
||||||
if run_script(script1, xml_filename):
|
|
||||||
if run_script(script2, xml_filename):
|
|
||||||
if run_script(script3, xml_filename):
|
|
||||||
print("\nPipeline completed successfully.")
|
|
||||||
else:
|
else:
|
||||||
print("\nPipeline failed at script:", script3)
|
sys.exit(0)
|
||||||
else:
|
|
||||||
print("\nPipeline failed at script:", script2)
|
# --- FIN: Se elimina la lógica redundante que venía después del bucle ---
|
||||||
else:
|
|
||||||
print("\nPipeline failed at script:", script1)
|
|
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
|
from processors.symbol_manager import SymbolManager # Import the manager
|
||||||
|
|
||||||
# --- Constantes y Configuración ---
|
# --- Constantes y Configuración ---
|
||||||
# SCL_SUFFIX = "_scl" # Old suffix
|
|
||||||
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
|
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||||
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
|
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
|
||||||
|
|
||||||
# Global data dictionary (consider passing 'data' as argument if needed elsewhere)
|
# Global data dictionary
|
||||||
# It's currently used by process_group_ifs implicitly via the outer scope,
|
|
||||||
# which works but passing it explicitly might be cleaner.
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
|
# --- (Incluye aquí las funciones process_group_ifs y load_processors SIN CAMBIOS) ---
|
||||||
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
"""
|
"""
|
||||||
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
||||||
|
@ -112,6 +109,9 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
# SCoil/RCoil might also be groupable if their SCL is final assignment
|
# SCoil/RCoil might also be groupable if their SCL is final assignment
|
||||||
"SCoil",
|
"SCoil",
|
||||||
"RCoil",
|
"RCoil",
|
||||||
|
"BLKMOV", # Added BLKMOV
|
||||||
|
"TON", "TOF", "TP", "Se", "Sd", # Added timers
|
||||||
|
"CTU", "CTD", "CTUD", # Added counters
|
||||||
]
|
]
|
||||||
|
|
||||||
for consumer_instr in network_logic:
|
for consumer_instr in network_logic:
|
||||||
|
@ -135,26 +135,26 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
is_enabled_by_us = True
|
is_enabled_by_us = True
|
||||||
|
|
||||||
# Check if consumer is groupable AND has its final SCL generated
|
# Check if consumer is groupable AND has its final SCL generated
|
||||||
# The suffix check needs adjustment based on how terminating processors set it.
|
|
||||||
# Assuming processors like Move, Add, Call, SCoil, RCoil NOW generate final SCL and add a suffix.
|
|
||||||
if (
|
if (
|
||||||
is_enabled_by_us
|
is_enabled_by_us
|
||||||
and consumer_type.endswith(SCL_SUFFIX) # Or a specific "final_scl" suffix
|
and consumer_type.endswith(SCL_SUFFIX) # Check if processed
|
||||||
and consumer_type_original in groupable_types
|
and consumer_type_original in groupable_types
|
||||||
):
|
):
|
||||||
|
|
||||||
consumer_scl = consumer_instr.get("scl", "")
|
consumer_scl = consumer_instr.get("scl", "")
|
||||||
# Extract core SCL (logic is similar, maybe simpler if SCL is cleaner now)
|
# Extract core SCL
|
||||||
core_scl = None
|
core_scl = None
|
||||||
if consumer_scl:
|
if consumer_scl:
|
||||||
# If consumer SCL itself is an IF generated by EN, take the body
|
# If consumer SCL itself is an IF generated by EN, take the body
|
||||||
if consumer_scl.strip().startswith("IF"):
|
if consumer_scl.strip().startswith("IF"):
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"THEN\s*(.*?)\s*END_IF;",
|
r"IF\s+.*?THEN\s*(.*?)\s*END_IF;", # More robust regex
|
||||||
consumer_scl,
|
consumer_scl,
|
||||||
re.DOTALL | re.IGNORECASE,
|
re.DOTALL | re.IGNORECASE,
|
||||||
)
|
)
|
||||||
core_scl = match.group(1).strip() if match else None
|
core_scl = match.group(1).strip() if match else None
|
||||||
|
# If body contains another IF, maybe don't group? (optional complexity)
|
||||||
|
# if core_scl and core_scl.strip().startswith("IF"): core_scl = None
|
||||||
elif not consumer_scl.strip().startswith(
|
elif not consumer_scl.strip().startswith(
|
||||||
"//"
|
"//"
|
||||||
): # Otherwise, take the whole line if not comment
|
): # Otherwise, take the whole line if not comment
|
||||||
|
@ -300,8 +300,7 @@ def load_processors(processors_dir="processors"):
|
||||||
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
||||||
return processor_map, processor_list_sorted
|
return processor_map, processor_list_sorted
|
||||||
|
|
||||||
|
# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) ---
|
||||||
# --- Bucle Principal de Procesamiento (Modificado para STL) ---
|
|
||||||
def process_json_to_scl(json_filepath):
|
def process_json_to_scl(json_filepath):
|
||||||
"""
|
"""
|
||||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
||||||
|
@ -321,15 +320,14 @@ def process_json_to_scl(json_filepath):
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Obtener lenguaje del bloque principal ---
|
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) ---
|
||||||
block_language = data.get("language", "Unknown")
|
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB
|
||||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
|
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}")
|
||||||
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {block_language}")
|
|
||||||
|
|
||||||
# --- SI ES UN DB, SALTAR EL PROCESAMIENTO LÓGICO ---
|
# --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO ---
|
||||||
if block_language == "DB":
|
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
||||||
print(
|
print(
|
||||||
"INFO: El bloque es un Data Block (DB). Saltando procesamiento lógico de x2."
|
"INFO: El bloque es un Data Block (GlobalDB). Saltando procesamiento lógico de x2."
|
||||||
)
|
)
|
||||||
# Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo)
|
# Simplemente guardamos una copia (o el mismo archivo si no se requiere sufijo)
|
||||||
output_filename = json_filepath.replace(
|
output_filename = json_filepath.replace(
|
||||||
|
@ -345,8 +343,8 @@ def process_json_to_scl(json_filepath):
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return # <<< SALIR TEMPRANO PARA DBs
|
return # <<< SALIR TEMPRANO PARA DBs
|
||||||
|
|
||||||
# --- SI NO ES DB, CONTINUAR CON EL PROCESAMIENTO LÓGICO (FC/FB) ---
|
# --- SI NO ES DB (FC, FB, OB), CONTINUAR CON EL PROCESAMIENTO LÓGICO ---
|
||||||
print("INFO: El bloque es FC/FB. Iniciando procesamiento lógico...")
|
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...") # <-- Mensaje actualizado
|
||||||
|
|
||||||
script_dir = os.path.dirname(__file__)
|
script_dir = os.path.dirname(__file__)
|
||||||
processors_dir_path = os.path.join(script_dir, "processors")
|
processors_dir_path = os.path.join(script_dir, "processors")
|
||||||
|
@ -391,7 +389,7 @@ def process_json_to_scl(json_filepath):
|
||||||
passes = 0
|
passes = 0
|
||||||
processing_complete = False
|
processing_complete = False
|
||||||
|
|
||||||
print("\n--- Iniciando Bucle de Procesamiento Iterativo (FC/FB) ---")
|
print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---") # <-- Mensaje actualizado
|
||||||
while passes < max_passes and not processing_complete:
|
while passes < max_passes and not processing_complete:
|
||||||
passes += 1
|
passes += 1
|
||||||
made_change_in_base_pass = False
|
made_change_in_base_pass = False
|
||||||
|
@ -408,34 +406,44 @@ def process_json_to_scl(json_filepath):
|
||||||
func_to_call = processor_info["func"]
|
func_to_call = processor_info["func"]
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network["id"]
|
network_id = network["id"]
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD") # Lenguaje de la red
|
||||||
if network_lang == "STL":
|
if network_lang == "STL": # Saltar redes STL
|
||||||
continue # Saltar STL
|
continue
|
||||||
|
|
||||||
access_map = network_access_maps.get(network_id, {})
|
access_map = network_access_maps.get(network_id, {})
|
||||||
network_logic = network.get("logic", [])
|
network_logic = network.get("logic", [])
|
||||||
for instruction in network_logic:
|
for instruction in network_logic:
|
||||||
instr_uid = instruction.get("instruction_uid")
|
instr_uid = instruction.get("instruction_uid")
|
||||||
instr_type_original = instruction.get("type", "Unknown")
|
# Usar el tipo *actual* de la instrucción para el lookup
|
||||||
|
instr_type_current = instruction.get("type", "Unknown")
|
||||||
|
|
||||||
|
# Saltar si ya está procesado, es error, agrupado, o tipo crudo
|
||||||
if (
|
if (
|
||||||
instr_type_original.endswith(SCL_SUFFIX)
|
instr_type_current.endswith(SCL_SUFFIX)
|
||||||
or "_error" in instr_type_original
|
or "_error" in instr_type_current
|
||||||
or instruction.get("grouped", False)
|
or instruction.get("grouped", False)
|
||||||
or instr_type_original
|
or instr_type_current
|
||||||
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]
|
in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
lookup_key = instr_type_original.lower()
|
|
||||||
effective_type_name = lookup_key
|
|
||||||
if instr_type_original == "Call":
|
|
||||||
block_type = instruction.get("block_type", "").upper()
|
|
||||||
if block_type == "FC":
|
|
||||||
effective_type_name = "call_fc"
|
|
||||||
elif block_type == "FB":
|
|
||||||
effective_type_name = "call_fb"
|
|
||||||
|
|
||||||
|
# El lookup usa el tipo actual (que aún no tiene el sufijo)
|
||||||
|
lookup_key = instr_type_current.lower()
|
||||||
|
effective_type_name = lookup_key
|
||||||
|
|
||||||
|
# Mapeo especial para llamadas FC/FB
|
||||||
|
if instr_type_current == "Call":
|
||||||
|
call_block_type = instruction.get("block_type", "").upper()
|
||||||
|
if call_block_type == "FC":
|
||||||
|
effective_type_name = "call_fc"
|
||||||
|
elif call_block_type == "FB":
|
||||||
|
effective_type_name = "call_fb"
|
||||||
|
# Añadir otros tipos de llamada si es necesario
|
||||||
|
|
||||||
|
# Si el tipo efectivo coincide con el procesador actual
|
||||||
if effective_type_name == current_type_name:
|
if effective_type_name == current_type_name:
|
||||||
try:
|
try:
|
||||||
|
# Pasar 'data' a la función del procesador
|
||||||
changed = func_to_call(
|
changed = func_to_call(
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
instruction, network_id, sympy_map, symbol_manager, data
|
||||||
)
|
)
|
||||||
|
@ -444,22 +452,24 @@ def process_json_to_scl(json_filepath):
|
||||||
num_sympy_processed_this_pass += 1
|
num_sympy_processed_this_pass += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"ERROR(SymPy Base) al procesar {instr_type_original} UID {instr_uid}: {e}"
|
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
|
||||||
)
|
)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
instruction["scl"] = (
|
instruction["scl"] = (
|
||||||
f"// ERROR en SymPy procesador base: {e}"
|
f"// ERROR en SymPy procesador base: {e}"
|
||||||
)
|
)
|
||||||
instruction["type"] = instr_type_original + "_error"
|
# Añadir sufijo de error al tipo actual
|
||||||
made_change_in_base_pass = True
|
instruction["type"] = instr_type_current + "_error"
|
||||||
|
made_change_in_base_pass = True # Se hizo un cambio (marcar como error)
|
||||||
print(
|
print(
|
||||||
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
|
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- FASE 2: Agrupación IF (Ignorando STL) ---
|
# --- FASE 2: Agrupación IF (Ignorando STL) ---
|
||||||
if (
|
if (
|
||||||
made_change_in_base_pass or passes == 1
|
made_change_in_base_pass or passes == 1
|
||||||
): # Ejecutar siempre en el primer pase
|
): # Ejecutar siempre en el primer pase o si hubo cambios
|
||||||
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
||||||
num_grouped_this_pass = 0 # Resetear contador para el pase
|
num_grouped_this_pass = 0 # Resetear contador para el pase
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
|
@ -468,7 +478,18 @@ def process_json_to_scl(json_filepath):
|
||||||
if network_lang == "STL":
|
if network_lang == "STL":
|
||||||
continue # Saltar STL
|
continue # Saltar STL
|
||||||
network_logic = network.get("logic", [])
|
network_logic = network.get("logic", [])
|
||||||
for instruction in network_logic:
|
# Iterar en orden por UID puede ser más estable para agrupación
|
||||||
|
uids_in_network = sorted([instr.get("instruction_uid", "Z") for instr in network_logic if instr.get("instruction_uid")])
|
||||||
|
for uid_to_process in uids_in_network:
|
||||||
|
instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None)
|
||||||
|
if not instruction: continue
|
||||||
|
|
||||||
|
# Saltar si ya está agrupada, es error, etc.
|
||||||
|
if instruction.get("grouped") or "_error" in instruction.get("type", ""):
|
||||||
|
continue
|
||||||
|
# La agrupación sólo aplica a instrucciones que generan condiciones booleanas
|
||||||
|
# y que ya fueron procesadas (tienen el sufijo)
|
||||||
|
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
||||||
try:
|
try:
|
||||||
group_changed = process_group_ifs(
|
group_changed = process_group_ifs(
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
instruction, network_id, sympy_map, symbol_manager, data
|
||||||
|
@ -503,14 +524,16 @@ def process_json_to_scl(json_filepath):
|
||||||
# --- FIN BUCLE ITERATIVO ---
|
# --- FIN BUCLE ITERATIVO ---
|
||||||
|
|
||||||
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
|
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
|
||||||
print("\n--- Verificación Final de Instrucciones No Procesadas (FC/FB) ---")
|
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
|
||||||
unprocessed_count = 0
|
unprocessed_count = 0
|
||||||
unprocessed_details = []
|
unprocessed_details = []
|
||||||
ignored_types = [
|
ignored_types = [
|
||||||
"raw_scl_chunk",
|
"raw_scl_chunk",
|
||||||
"unsupported_lang",
|
"unsupported_lang",
|
||||||
"raw_stl_chunk",
|
"raw_stl_chunk",
|
||||||
] # Añadido raw_stl_chunk
|
"unsupported_content", # Añadido de x1
|
||||||
|
"parsing_error", # Añadido de x1
|
||||||
|
]
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network.get("id", "Unknown ID")
|
network_id = network.get("id", "Unknown ID")
|
||||||
network_title = network.get("title", f"Network {network_id}")
|
network_title = network.get("title", f"Network {network_id}")
|
||||||
|
@ -547,7 +570,7 @@ def process_json_to_scl(json_filepath):
|
||||||
output_filename = json_filepath.replace(
|
output_filename = json_filepath.replace(
|
||||||
"_simplified.json", "_simplified_processed.json"
|
"_simplified.json", "_simplified_processed.json"
|
||||||
)
|
)
|
||||||
print(f"\nGuardando JSON procesado (FC/FB) en: {output_filename}")
|
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_filename, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
|
@ -557,7 +580,7 @@ def process_json_to_scl(json_filepath):
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
# --- Ejecución (sin cambios) ---
|
# --- Ejecución (sin cambios en esta parte) ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Imports necesarios solo para la ejecución como script principal
|
# Imports necesarios solo para la ejecución como script principal
|
||||||
import argparse
|
import argparse
|
||||||
|
@ -577,12 +600,10 @@ if __name__ == "__main__":
|
||||||
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
|
source_xml_file = args.source_xml_filepath # Obtiene la ruta del XML original
|
||||||
|
|
||||||
# Verificar si el archivo XML original existe (como referencia, útil para depuración)
|
# Verificar si el archivo XML original existe (como referencia, útil para depuración)
|
||||||
# No es estrictamente necesario para la lógica aquí, pero ayuda a confirmar
|
|
||||||
if not os.path.exists(source_xml_file):
|
if not os.path.exists(source_xml_file):
|
||||||
print(
|
print(
|
||||||
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente."
|
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente."
|
||||||
)
|
)
|
||||||
# No salir necesariamente, pero es bueno saberlo.
|
|
||||||
|
|
||||||
# Derivar nombre del archivo JSON de entrada (_simplified.json)
|
# Derivar nombre del archivo JSON de entrada (_simplified.json)
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
|
@ -610,14 +631,13 @@ if __name__ == "__main__":
|
||||||
sys.exit(1) # Salir si el archivo necesario no está
|
sys.exit(1) # Salir si el archivo necesario no está
|
||||||
else:
|
else:
|
||||||
# Llamar a la función principal de procesamiento del script
|
# Llamar a la función principal de procesamiento del script
|
||||||
# Asumiendo que tu función principal se llama process_json_to_scl(input_json_path)
|
|
||||||
try:
|
try:
|
||||||
process_json_to_scl(input_json_file)
|
process_json_to_scl(input_json_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
|
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
|
||||||
)
|
)
|
||||||
import traceback
|
import traceback # Asegurar que traceback está importado
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
sys.exit(1) # Salir con error si la función principal falla
|
|
@ -46,24 +46,23 @@ except ImportError:
|
||||||
# para formatear valores iniciales
|
# para formatear valores iniciales
|
||||||
def format_scl_start_value(value, datatype):
|
def format_scl_start_value(value, datatype):
|
||||||
"""Formatea un valor para la inicialización SCL según el tipo."""
|
"""Formatea un valor para la inicialización SCL según el tipo."""
|
||||||
|
# Add initial debug print
|
||||||
|
# print(f"DEBUG format_scl_start_value: value='{value}', datatype='{datatype}'")
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None # Retornar None si no hay valor
|
||||||
datatype_lower = datatype.lower() if datatype else ""
|
datatype_lower = datatype.lower() if datatype else ""
|
||||||
value_str = str(value)
|
value_str = str(value)
|
||||||
|
|
||||||
if "bool" in datatype_lower:
|
# Intentar quitar comillas si existen (para manejar "TRUE" vs TRUE)
|
||||||
return "TRUE" if value_str.lower() == "true" else "FALSE"
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1:
|
||||||
elif "string" in datatype_lower:
|
value_str_unquoted = value_str[1:-1]
|
||||||
escaped_value = value_str.replace("'", "''")
|
elif value_str.startswith("'") and value_str.endswith("'") and len(value_str) > 1:
|
||||||
if escaped_value.startswith("'") and escaped_value.endswith("'"):
|
value_str_unquoted = value_str[1:-1]
|
||||||
escaped_value = escaped_value[1:-1]
|
else:
|
||||||
return f"'{escaped_value}'"
|
value_str_unquoted = value_str
|
||||||
elif "char" in datatype_lower: # Añadido Char
|
|
||||||
escaped_value = value_str.replace("'", "''")
|
# --- Integer-like types ---
|
||||||
if escaped_value.startswith("'") and escaped_value.endswith("'"):
|
if any(
|
||||||
escaped_value = escaped_value[1:-1]
|
|
||||||
return f"'{escaped_value}'"
|
|
||||||
elif any(
|
|
||||||
t in datatype_lower
|
t in datatype_lower
|
||||||
for t in [
|
for t in [
|
||||||
"int",
|
"int",
|
||||||
|
@ -79,72 +78,169 @@ def format_scl_start_value(value, datatype):
|
||||||
"udint",
|
"udint",
|
||||||
"ulint",
|
"ulint",
|
||||||
]
|
]
|
||||||
): # Ampliado
|
):
|
||||||
try:
|
try:
|
||||||
return str(int(value_str))
|
# Intentar convertir el valor (sin comillas) a entero
|
||||||
|
return str(int(value_str_unquoted))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str):
|
# Si no es un entero válido, podría ser una constante simbólica
|
||||||
return value_str
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
||||||
return f"'{value_str}'" # O como string si no es entero ni símbolo
|
return value_str_unquoted # Devolver como símbolo
|
||||||
|
|
||||||
|
# --- Fallback for non-integer, non-symbol ---
|
||||||
|
print(
|
||||||
|
f"DEBUG format_scl_start_value: Fallback for int-like. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
||||||
|
) # More debug
|
||||||
|
# MODIFIED FALLBACK: Escape newlines and use repr() for safety before formatting
|
||||||
|
try:
|
||||||
|
# Escape backslashes and single quotes properly for SCL string literal
|
||||||
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace(
|
||||||
|
"'", "''"
|
||||||
|
)
|
||||||
|
# Remove potential newlines that break Python f-string; SCL strings usually don't span lines implicitly
|
||||||
|
escaped_for_scl = escaped_for_scl.replace("\n", "").replace("\r", "")
|
||||||
|
# Format as SCL string literal
|
||||||
|
formatted_scl_string = f"'{escaped_for_scl}'"
|
||||||
|
print(
|
||||||
|
f"DEBUG format_scl_start_value: Fallback result='{formatted_scl_string}'"
|
||||||
|
)
|
||||||
|
return formatted_scl_string
|
||||||
|
except Exception as format_exc:
|
||||||
|
print(
|
||||||
|
f"ERROR format_scl_start_value: Exception during fallback formatting: {format_exc}"
|
||||||
|
)
|
||||||
|
return f"'ERROR_FORMATTING_{value_str_unquoted[:20]}'" # Return an error string
|
||||||
|
|
||||||
|
# --- Other types (Bool, Real, String, Char, Time, Date, etc.) ---
|
||||||
|
elif "bool" in datatype_lower:
|
||||||
|
# Comparar sin importar mayúsculas/minúsculas y sin comillas
|
||||||
|
return "TRUE" if value_str_unquoted.lower() == "true" else "FALSE"
|
||||||
|
elif "string" in datatype_lower:
|
||||||
|
# Usar el valor sin comillas originales y escapar las internas
|
||||||
|
escaped_value = value_str_unquoted.replace("'", "''")
|
||||||
|
return f"'{escaped_value}'"
|
||||||
|
elif "char" in datatype_lower:
|
||||||
|
# Usar el valor sin comillas originales y escapar las internas
|
||||||
|
escaped_value = value_str_unquoted.replace("'", "''")
|
||||||
|
# SCL usa comillas simples para Char. Asegurar que sea un solo caracter si es posible?
|
||||||
|
# Por ahora, solo formatear. Longitud se verifica en TIA.
|
||||||
|
return f"'{escaped_value}'"
|
||||||
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
elif "real" in datatype_lower or "lreal" in datatype_lower:
|
||||||
try:
|
try:
|
||||||
f_val = float(value_str)
|
# Intentar convertir a float
|
||||||
|
f_val = float(value_str_unquoted)
|
||||||
s_val = str(f_val)
|
s_val = str(f_val)
|
||||||
|
# Asegurar que tenga punto decimal si es entero
|
||||||
if "." not in s_val and "e" not in s_val.lower():
|
if "." not in s_val and "e" not in s_val.lower():
|
||||||
s_val += ".0"
|
s_val += ".0"
|
||||||
return s_val
|
return s_val
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str):
|
# Podría ser constante simbólica
|
||||||
return value_str
|
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value_str_unquoted):
|
||||||
return f"'{value_str}'"
|
return value_str_unquoted
|
||||||
elif "time" in datatype_lower: # Añadido Time, S5Time, LTime
|
print(
|
||||||
# Quitar T#, LT#, S5T# si existen
|
f"Advertencia: Valor '{value_str}' no reconocido como real o símbolo para tipo {datatype}. Devolviendo como string."
|
||||||
|
)
|
||||||
|
# Use the robust fallback formatting here too
|
||||||
|
escaped_for_scl = (
|
||||||
|
value_str_unquoted.replace("\\", "\\\\")
|
||||||
|
.replace("'", "''")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
)
|
||||||
|
return f"'{escaped_for_scl}'"
|
||||||
|
elif "time" in datatype_lower:
|
||||||
|
# Quitar prefijos y añadir el correcto según el tipo específico
|
||||||
prefix = ""
|
prefix = ""
|
||||||
if value_str.upper().startswith("T#"):
|
val_to_use = value_str_unquoted # Usar valor sin comillas
|
||||||
|
if val_to_use.upper().startswith("T#"):
|
||||||
prefix = "T#"
|
prefix = "T#"
|
||||||
value_str = value_str[2:]
|
val_to_use = val_to_use[2:]
|
||||||
elif value_str.upper().startswith("LT#"):
|
elif val_to_use.upper().startswith("LT#"):
|
||||||
prefix = "LT#"
|
prefix = "LT#"
|
||||||
value_str = value_str[3:]
|
val_to_use = val_to_use[3:]
|
||||||
elif value_str.upper().startswith("S5T#"):
|
elif val_to_use.upper().startswith("S5T#"):
|
||||||
prefix = "S5T#"
|
prefix = "S5T#"
|
||||||
value_str = value_str[4:]
|
val_to_use = val_to_use[4:]
|
||||||
# Devolver con el prefijo correcto o T# por defecto si no había
|
|
||||||
if prefix:
|
if "s5time" in datatype_lower:
|
||||||
return f"{prefix}{value_str}"
|
return f"S5T#{val_to_use}"
|
||||||
elif "s5time" in datatype_lower:
|
|
||||||
return f"S5T#{value_str}"
|
|
||||||
elif "ltime" in datatype_lower:
|
elif "ltime" in datatype_lower:
|
||||||
return f"LT#{value_str}"
|
return f"LT#{val_to_use}"
|
||||||
else:
|
else:
|
||||||
return f"T#{value_str}" # Default a TIME
|
return f"T#{val_to_use}" # Default a TIME
|
||||||
elif "date" in datatype_lower: # Añadido Date, DT, TOD
|
elif "date" in datatype_lower:
|
||||||
if value_str.upper().startswith("D#"):
|
val_to_use = value_str_unquoted
|
||||||
return value_str
|
# Handle DTL first as it's longer
|
||||||
elif "dt" in datatype_lower or "date_and_time" in datatype_lower:
|
if "dtl" in datatype_lower or "date_and_time" in datatype_lower:
|
||||||
if value_str.upper().startswith("DT#"):
|
prefix = "DTL#" if val_to_use.upper().startswith("DTL#") else "DTL#"
|
||||||
return value_str
|
val_to_use = (
|
||||||
else:
|
val_to_use[4:] if val_to_use.upper().startswith("DTL#") else val_to_use
|
||||||
return f"DT#{value_str}" # Añadir prefijo DT#
|
)
|
||||||
|
return f"{prefix}{val_to_use}"
|
||||||
|
elif "dt" in datatype_lower:
|
||||||
|
prefix = "DT#" if val_to_use.upper().startswith("DT#") else "DT#"
|
||||||
|
val_to_use = (
|
||||||
|
val_to_use[3:] if val_to_use.upper().startswith("DT#") else val_to_use
|
||||||
|
)
|
||||||
|
return f"{prefix}{val_to_use}"
|
||||||
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
elif "tod" in datatype_lower or "time_of_day" in datatype_lower:
|
||||||
if value_str.upper().startswith("TOD#"):
|
prefix = "TOD#" if val_to_use.upper().startswith("TOD#") else "TOD#"
|
||||||
return value_str
|
val_to_use = (
|
||||||
else:
|
val_to_use[4:] if val_to_use.upper().startswith("TOD#") else val_to_use
|
||||||
return f"TOD#{value_str}" # Añadir prefijo TOD#
|
)
|
||||||
else:
|
return f"{prefix}{val_to_use}"
|
||||||
return f"D#{value_str}" # Default a Date
|
else: # Default a Date D#
|
||||||
# Fallback genérico
|
prefix = "D#" if val_to_use.upper().startswith("D#") else "D#"
|
||||||
|
val_to_use = (
|
||||||
|
val_to_use[2:] if val_to_use.upper().startswith("D#") else val_to_use
|
||||||
|
)
|
||||||
|
return f"{prefix}{val_to_use}"
|
||||||
|
|
||||||
|
# --- Fallback for completely unknown types or complex structures ---
|
||||||
else:
|
else:
|
||||||
|
# Si es un nombre válido (posiblemente UDT, constante global, etc.), devolverlo tal cual
|
||||||
|
# Ajustar regex para permitir más caracteres si es necesario
|
||||||
if re.match(
|
if re.match(
|
||||||
r'^[a-zA-Z_][a-zA-Z0-9_."#\[\]]+$', value_str
|
r'^[a-zA-Z_#"][a-zA-Z0-9_."#\[\]%]+$', value_str
|
||||||
): # Permitir más caracteres en símbolos/tipos
|
): # Permitir % para accesos tipo %DB1.DBD0
|
||||||
# Si es un UDT o Struct complejo, podría venir con comillas, quitarlas
|
# Quitar comillas externas si es un UDT o struct complejo
|
||||||
if value_str.startswith('"') and value_str.endswith('"'):
|
if (
|
||||||
|
value_str.startswith('"')
|
||||||
|
and value_str.endswith('"')
|
||||||
|
and len(value_str) > 1
|
||||||
|
):
|
||||||
return value_str[1:-1]
|
return value_str[1:-1]
|
||||||
|
# Mantener comillas si es acceso a DB ("DB_Name".Var)
|
||||||
|
if '"' in value_str and "." in value_str and value_str.count('"') == 2:
|
||||||
|
return value_str
|
||||||
|
# Si no tiene comillas y es un nombre simple o acceso #temp o %I0.0 etc
|
||||||
|
if not value_str.startswith('"') and not value_str.startswith("'"):
|
||||||
|
# Formatear nombres simples, pero dejar accesos % y # tal cual
|
||||||
|
if value_str.startswith("#") or value_str.startswith("%"):
|
||||||
return value_str
|
return value_str
|
||||||
else:
|
else:
|
||||||
escaped_value = value_str.replace("'", "''")
|
# return format_variable_name(value_str) # Evitar formatear aquí, puede ser una constante
|
||||||
return f"'{escaped_value}'"
|
return value_str # Return as is if it looks symbolic
|
||||||
|
# Devolver el valor original si tiene comillas internas o estructura compleja no manejada arriba
|
||||||
|
return value_str
|
||||||
|
else:
|
||||||
|
# Si no parece un nombre/símbolo/acceso, tratarlo como string (último recurso)
|
||||||
|
print(
|
||||||
|
f"DEBUG format_scl_start_value: Fallback final. value_str_unquoted='{repr(value_str_unquoted)}', datatype='{datatype}'"
|
||||||
|
)
|
||||||
|
# Use the robust fallback formatting
|
||||||
|
escaped_for_scl = (
|
||||||
|
value_str_unquoted.replace("\\", "\\\\")
|
||||||
|
.replace("'", "''")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
)
|
||||||
|
return f"'{escaped_for_scl}'"
|
||||||
|
|
||||||
|
|
||||||
|
# ... (generate_scl_declarations and generate_scl function remain the same as the previous version) ...
|
||||||
|
# --- (Incluye aquí las funciones generate_scl_declarations y generate_scl SIN CAMBIOS respecto a la respuesta anterior) ---
|
||||||
|
|
||||||
|
|
||||||
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
|
# --- NUEVA FUNCIÓN RECURSIVA para generar declaraciones SCL (VAR/STRUCT/ARRAY) ---
|
||||||
|
@ -155,87 +251,132 @@ def generate_scl_declarations(variables, indent_level=1):
|
||||||
for var in variables:
|
for var in variables:
|
||||||
var_name_scl = format_variable_name(var.get("name"))
|
var_name_scl = format_variable_name(var.get("name"))
|
||||||
var_dtype_raw = var.get("datatype", "VARIANT")
|
var_dtype_raw = var.get("datatype", "VARIANT")
|
||||||
# Limpiar comillas de tipos de datos UDT ("MyType" -> MyType)
|
|
||||||
var_dtype = (
|
|
||||||
var_dtype_raw.strip('"')
|
|
||||||
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"')
|
|
||||||
else var_dtype_raw
|
|
||||||
)
|
|
||||||
|
|
||||||
var_comment = var.get("comment")
|
var_comment = var.get("comment")
|
||||||
start_value = var.get("start_value")
|
start_value = var.get("start_value")
|
||||||
children = var.get("children") # Para structs
|
children = var.get("children") # Para structs
|
||||||
array_elements = var.get("array_elements") # Para arrays
|
array_elements = var.get("array_elements") # Para arrays
|
||||||
|
|
||||||
# Manejar tipos de datos Array especiales
|
# Limpiar comillas del tipo de dato si es UDT/String/etc.
|
||||||
array_match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", var_dtype, re.IGNORECASE)
|
var_dtype_cleaned = var_dtype_raw
|
||||||
base_type_for_init = var_dtype
|
if isinstance(var_dtype_raw, str):
|
||||||
declaration_dtype = var_dtype
|
if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"'):
|
||||||
if array_match:
|
var_dtype_cleaned = var_dtype_raw[1:-1]
|
||||||
array_prefix = array_match.group(1)
|
# Manejar caso 'Array [...] of "MyUDT"'
|
||||||
base_type_raw = array_match.group(2).strip()
|
array_match = re.match(
|
||||||
# Limpiar comillas del tipo base del array
|
r'(Array\[.*\]\s+of\s+)"(.*)"', var_dtype_raw, re.IGNORECASE
|
||||||
base_type_for_init = (
|
|
||||||
base_type_raw.strip('"')
|
|
||||||
if base_type_raw.startswith('"') and base_type_raw.endswith('"')
|
|
||||||
else base_type_raw
|
|
||||||
)
|
)
|
||||||
declaration_dtype = (
|
if array_match:
|
||||||
f'{array_prefix}"{base_type_for_init}"'
|
var_dtype_cleaned = f"{array_match.group(1)}{array_match.group(2)}" # Quitar comillas del tipo base
|
||||||
if '"' not in base_type_raw
|
|
||||||
else f"{array_prefix}{base_type_raw}"
|
|
||||||
) # Reconstruir con comillas si es UDT
|
|
||||||
|
|
||||||
# Reconstruir declaración con comillas si es UDT y no array
|
# Determinar tipo base para inicialización (importante para arrays)
|
||||||
elif (
|
base_type_for_init = var_dtype_cleaned
|
||||||
not array_match and var_dtype != base_type_for_init
|
array_prefix_for_decl = ""
|
||||||
): # Es un tipo que necesita comillas (UDT)
|
if var_dtype_cleaned.lower().startswith("array["):
|
||||||
declaration_dtype = f'"{var_dtype}"'
|
match = re.match(
|
||||||
|
r"(Array\[.*\]\s+of\s+)(.*)", var_dtype_cleaned, re.IGNORECASE
|
||||||
|
)
|
||||||
|
if match:
|
||||||
|
array_prefix_for_decl = match.group(1)
|
||||||
|
base_type_for_init = match.group(2).strip()
|
||||||
|
|
||||||
|
# Construir tipo de dato para la declaración SCL
|
||||||
|
declaration_dtype = var_dtype_raw # Usar el raw por defecto
|
||||||
|
# Si es UDT o tipo complejo que requiere comillas y no es array simple
|
||||||
|
if base_type_for_init != var_dtype_cleaned and not array_prefix_for_decl:
|
||||||
|
# Poner comillas si no las tiene ya el tipo base
|
||||||
|
if not base_type_for_init.startswith('"'):
|
||||||
|
declaration_dtype = f'"{base_type_for_init}"'
|
||||||
|
else:
|
||||||
|
declaration_dtype = base_type_for_init # Ya tiene comillas
|
||||||
|
# Si es array de UDT/complejo, reconstruir con comillas en el tipo base
|
||||||
|
elif array_prefix_for_decl and base_type_for_init != var_dtype_cleaned:
|
||||||
|
if not base_type_for_init.startswith('"'):
|
||||||
|
declaration_dtype = f'{array_prefix_for_decl}"{base_type_for_init}"'
|
||||||
|
else:
|
||||||
|
declaration_dtype = f"{array_prefix_for_decl}{base_type_for_init}"
|
||||||
|
|
||||||
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
|
||||||
init_value = None
|
init_value_scl = None
|
||||||
|
|
||||||
# ---- Arrays ----
|
# ---- Arrays ----
|
||||||
if array_elements:
|
if array_elements:
|
||||||
# Ordenar índices (asumiendo que son numéricos)
|
# Ordenar índices (asumiendo que son numéricos '0', '1', ...)
|
||||||
try:
|
try:
|
||||||
sorted_indices = sorted(array_elements.keys(), key=int)
|
# Extraer números de los índices string
|
||||||
|
indices_numeric = {int(k): v for k, v in array_elements.items()}
|
||||||
|
sorted_indices = sorted(indices_numeric.keys())
|
||||||
|
# Mapear de nuevo a string para buscar valor
|
||||||
|
sorted_indices_str = [str(k) for k in sorted_indices]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
sorted_indices = sorted(
|
# Fallback a orden alfabético si los índices no son números
|
||||||
array_elements.keys()
|
print(
|
||||||
) # Fallback a orden alfabético
|
f"Advertencia: Índices de array no numéricos para '{var_name_scl}'. Usando orden alfabético."
|
||||||
|
)
|
||||||
|
sorted_indices_str = sorted(array_elements.keys())
|
||||||
|
|
||||||
init_values = [
|
init_values = []
|
||||||
format_scl_start_value(array_elements[idx], base_type_for_init)
|
for idx_str in sorted_indices_str:
|
||||||
for idx in sorted_indices
|
try:
|
||||||
]
|
formatted_val = format_scl_start_value(
|
||||||
|
array_elements[idx_str], base_type_for_init
|
||||||
|
)
|
||||||
|
init_values.append(formatted_val)
|
||||||
|
except Exception as e_fmt:
|
||||||
|
print(
|
||||||
|
f"ERROR: Falló formateo para índice {idx_str} de array '{var_name_scl}'. Valor: {array_elements[idx_str]}. Error: {e_fmt}"
|
||||||
|
)
|
||||||
|
init_values.append(f"/*ERR_FMT_{idx_str}*/") # Placeholder de error
|
||||||
|
|
||||||
|
# Filtrar Nones que pueden venir de format_scl_start_value si el valor era None
|
||||||
valid_inits = [v for v in init_values if v is not None]
|
valid_inits = [v for v in init_values if v is not None]
|
||||||
if valid_inits:
|
if valid_inits:
|
||||||
init_value = f"[{', '.join(valid_inits)}]"
|
# Si todos los valores son iguales y es un array grande, podríamos usar notación x(value)
|
||||||
|
# Simplificación: por ahora, listar todos
|
||||||
|
init_value_scl = f"[{', '.join(valid_inits)}]"
|
||||||
|
elif array_elements: # Si había elementos pero todos formatearon a None
|
||||||
|
print(
|
||||||
|
f"Advertencia: Todos los valores iniciales para array '{var_name_scl}' son None o inválidos."
|
||||||
|
)
|
||||||
|
|
||||||
# ---- Structs ----
|
# ---- Structs ----
|
||||||
elif children:
|
elif children:
|
||||||
# No añadir comentario // Struct aquí, es redundante
|
# El valor inicial de un struct se maneja recursivamente dentro
|
||||||
scl_lines.append(declaration_line) # Añadir línea de declaración base
|
# Añadir comentario? Puede ser redundante.
|
||||||
|
scl_lines.append(
|
||||||
|
declaration_line
|
||||||
|
) # Añadir línea de declaración base STRUCT
|
||||||
scl_lines.append(f"{indent}STRUCT")
|
scl_lines.append(f"{indent}STRUCT")
|
||||||
|
# Llamada recursiva para los miembros internos
|
||||||
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
scl_lines.extend(generate_scl_declarations(children, indent_level + 1))
|
||||||
scl_lines.append(f"{indent}END_STRUCT;")
|
scl_lines.append(f"{indent}END_STRUCT;")
|
||||||
if var_comment:
|
if var_comment: # Comentario después de END_STRUCT
|
||||||
scl_lines.append(f"{indent}// {var_comment}")
|
scl_lines.append(f"{indent}// {var_comment}")
|
||||||
scl_lines.append("") # Línea extra
|
scl_lines.append("") # Línea extra para legibilidad
|
||||||
continue # Saltar resto para Struct
|
continue # Saltar el resto de la lógica para este struct
|
||||||
|
|
||||||
# ---- Tipos Simples ----
|
# ---- Tipos Simples ----
|
||||||
else:
|
else:
|
||||||
if start_value is not None:
|
if start_value is not None:
|
||||||
init_value = format_scl_start_value(start_value, var_dtype)
|
try:
|
||||||
|
init_value_scl = format_scl_start_value(
|
||||||
|
start_value, base_type_for_init
|
||||||
|
) # Usar tipo base
|
||||||
|
except Exception as e_fmt_simple:
|
||||||
|
print(
|
||||||
|
f"ERROR: Falló formateo para valor simple de '{var_name_scl}'. Valor: {start_value}. Error: {e_fmt_simple}"
|
||||||
|
)
|
||||||
|
init_value_scl = f"/*ERR_FMT_SIMPLE*/" # Placeholder
|
||||||
|
|
||||||
|
# Añadir inicialización si existe y no es None
|
||||||
|
if init_value_scl is not None:
|
||||||
|
declaration_line += f" := {init_value_scl}"
|
||||||
|
|
||||||
# Añadir inicialización si existe
|
|
||||||
if init_value:
|
|
||||||
declaration_line += f" := {init_value}"
|
|
||||||
declaration_line += ";"
|
declaration_line += ";"
|
||||||
|
|
||||||
|
# Añadir comentario si existe
|
||||||
if var_comment:
|
if var_comment:
|
||||||
declaration_line += f" // {var_comment}"
|
declaration_line += f" // {var_comment}"
|
||||||
|
|
||||||
scl_lines.append(declaration_line)
|
scl_lines.append(declaration_line)
|
||||||
|
|
||||||
return scl_lines
|
return scl_lines
|
||||||
|
@ -243,7 +384,7 @@ def generate_scl_declarations(variables, indent_level=1):
|
||||||
|
|
||||||
# --- Función Principal de Generación SCL ---
|
# --- Función Principal de Generación SCL ---
|
||||||
def generate_scl(processed_json_filepath, output_scl_filepath):
|
def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
"""Genera un archivo SCL a partir del JSON procesado (FC/FB o DB)."""
|
"""Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
|
||||||
|
|
||||||
if not os.path.exists(processed_json_filepath):
|
if not os.path.exists(processed_json_filepath):
|
||||||
print(
|
print(
|
||||||
|
@ -263,33 +404,41 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
# --- Extracción de Información del Bloque (Común) ---
|
# --- Extracción de Información del Bloque (Común) ---
|
||||||
block_name = data.get("block_name", "UnknownBlock")
|
block_name = data.get("block_name", "UnknownBlock")
|
||||||
block_number = data.get("block_number")
|
block_number = data.get("block_number")
|
||||||
block_lang_original = data.get("language", "Unknown") # Será "DB" para Data Blocks
|
# block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
|
||||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB
|
block_type = data.get(
|
||||||
|
"block_type", "Unknown"
|
||||||
|
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
|
||||||
block_comment = data.get("block_comment", "")
|
block_comment = data.get("block_comment", "")
|
||||||
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
|
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
|
||||||
print(
|
print(
|
||||||
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name}, Lang: {block_lang_original})"
|
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
|
||||||
)
|
)
|
||||||
scl_output = []
|
scl_output = []
|
||||||
|
|
||||||
# --- GENERACIÓN PARA DATA BLOCK (DB) ---
|
# --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
|
||||||
if block_lang_original == "DB":
|
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
||||||
print("Modo de generación: DATA_BLOCK")
|
print("Modo de generación: DATA_BLOCK")
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
scl_output.append(f"// Block Type: {block_type}")
|
||||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
scl_output.append(f"// Block Name (Original): {block_name}")
|
||||||
if block_number:
|
if block_number:
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
scl_output.append(f"// Block Number: {block_number}")
|
||||||
if block_comment:
|
if block_comment:
|
||||||
scl_output.append(f"// Block Comment: {block_comment}")
|
# Dividir comentarios largos en múltiples líneas
|
||||||
|
comment_lines = block_comment.splitlines()
|
||||||
|
scl_output.append(f"// Block Comment:")
|
||||||
|
for line in comment_lines:
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
|
scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
||||||
scl_output.append("VERSION : 0.1")
|
scl_output.append("VERSION : 0.1")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
interface_data = data.get("interface", {})
|
interface_data = data.get("interface", {})
|
||||||
|
# En DBs, la sección relevante suele ser 'Static'
|
||||||
static_vars = interface_data.get("Static", [])
|
static_vars = interface_data.get("Static", [])
|
||||||
if static_vars:
|
if static_vars:
|
||||||
scl_output.append("VAR")
|
scl_output.append("VAR")
|
||||||
|
# Usar la función recursiva para generar declaraciones
|
||||||
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
||||||
scl_output.append("END_VAR")
|
scl_output.append("END_VAR")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
|
@ -297,182 +446,288 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
print(
|
print(
|
||||||
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
||||||
)
|
)
|
||||||
|
# Añadir bloque VAR vacío si no hay variables
|
||||||
scl_output.append("VAR")
|
scl_output.append("VAR")
|
||||||
scl_output.append("END_VAR")
|
scl_output.append("END_VAR")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
scl_output.append("BEGIN")
|
scl_output.append("BEGIN")
|
||||||
scl_output.append("")
|
scl_output.append(
|
||||||
|
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
|
||||||
|
)
|
||||||
scl_output.append("END_DATA_BLOCK")
|
scl_output.append("END_DATA_BLOCK")
|
||||||
|
|
||||||
# --- GENERACIÓN PARA FUNCTION BLOCK / FUNCTION (FC/FB) ---
|
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
|
||||||
else:
|
else:
|
||||||
print("Modo de generación: FUNCTION_BLOCK / FUNCTION")
|
# Determinar palabra clave SCL
|
||||||
scl_block_keyword = "FUNCTION_BLOCK" if block_type == "FB" else "FUNCTION"
|
scl_block_keyword = "FUNCTION_BLOCK" # Default
|
||||||
|
if block_type == "FC":
|
||||||
|
scl_block_keyword = "FUNCTION"
|
||||||
|
elif block_type == "OB":
|
||||||
|
scl_block_keyword = "ORGANIZATION_BLOCK"
|
||||||
|
elif block_type == "FB":
|
||||||
|
scl_block_keyword = "FUNCTION_BLOCK"
|
||||||
|
else: # Fallback
|
||||||
|
print(
|
||||||
|
f"Advertencia: Tipo de bloque desconocido '{block_type}', usando FUNCTION_BLOCK."
|
||||||
|
)
|
||||||
|
scl_block_keyword = "FUNCTION_BLOCK" # O quizás lanzar error?
|
||||||
|
|
||||||
|
print(f"Modo de generación: {scl_block_keyword}")
|
||||||
|
|
||||||
# Cabecera del Bloque
|
# Cabecera del Bloque
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
scl_output.append(f"// Block Type: {block_type}")
|
||||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
scl_output.append(f"// Block Name (Original): {block_name}")
|
||||||
if block_number:
|
if block_number:
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
scl_output.append(f"// Block Number: {block_number}")
|
||||||
scl_output.append(f"// Original Language: {block_lang_original}")
|
# Indicar lenguaje original de las redes si es relevante
|
||||||
|
original_net_langs = set(
|
||||||
|
n.get("language", "Unknown") for n in data.get("networks", [])
|
||||||
|
)
|
||||||
|
scl_output.append(
|
||||||
|
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
|
||||||
|
)
|
||||||
if block_comment:
|
if block_comment:
|
||||||
scl_output.append(f"// Block Comment: {block_comment}")
|
comment_lines = block_comment.splitlines()
|
||||||
|
scl_output.append(f"// Block Comment:")
|
||||||
|
for line in comment_lines:
|
||||||
|
scl_output.append(f"// {line}")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
# Manejar tipo de retorno para FUNCTION
|
|
||||||
|
# Manejar tipo de retorno para FUNCTION (FC)
|
||||||
return_type = "Void" # Default
|
return_type = "Void" # Default
|
||||||
interface_data = data.get("interface", {})
|
interface_data = data.get("interface", {})
|
||||||
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
|
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
|
||||||
return_member = interface_data["Return"][
|
# Asumir un solo valor de retorno
|
||||||
0
|
return_member = interface_data["Return"][0]
|
||||||
] # Asumir un solo valor de retorno
|
|
||||||
return_type_raw = return_member.get("datatype", "Void")
|
return_type_raw = return_member.get("datatype", "Void")
|
||||||
|
# Limpiar comillas si es UDT/String
|
||||||
return_type = (
|
return_type = (
|
||||||
return_type_raw.strip('"')
|
return_type_raw[1:-1]
|
||||||
if return_type_raw.startswith('"') and return_type_raw.endswith('"')
|
if isinstance(return_type_raw, str)
|
||||||
|
and return_type_raw.startswith('"')
|
||||||
|
and return_type_raw.endswith('"')
|
||||||
else return_type_raw
|
else return_type_raw
|
||||||
)
|
)
|
||||||
# Añadir comillas si es UDT
|
# Añadir comillas si es UDT y no las tenía
|
||||||
if return_type != return_type_raw:
|
if (
|
||||||
|
return_type != return_type_raw
|
||||||
|
and not return_type_raw.lower().startswith("array")
|
||||||
|
):
|
||||||
return_type = f'"{return_type}"'
|
return_type = f'"{return_type}"'
|
||||||
|
else: # Mantener raw si es tipo básico o ya tenía comillas
|
||||||
|
return_type = return_type_raw
|
||||||
|
|
||||||
scl_output.append(
|
# Línea de declaración del bloque
|
||||||
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
|
if scl_block_keyword == "FUNCTION":
|
||||||
if scl_block_keyword == "FUNCTION"
|
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
|
||||||
else f'{scl_block_keyword} "{scl_block_name}"'
|
else: # FB y OB
|
||||||
)
|
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
|
|
||||||
|
# Atributos y versión
|
||||||
|
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
||||||
scl_output.append("VERSION : 0.1")
|
scl_output.append("VERSION : 0.1")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
|
|
||||||
# Declaraciones de Interfaz FC/FB
|
# Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant)
|
||||||
section_order = [
|
# Orden estándar SCL
|
||||||
"Input",
|
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
||||||
"Output",
|
declared_temps = set() # Para rastrear temps ya declaradas
|
||||||
"InOut",
|
has_declarations = False
|
||||||
"Static",
|
|
||||||
"Temp",
|
|
||||||
"Constant",
|
|
||||||
] # Return ya está en cabecera
|
|
||||||
declared_temps = set()
|
|
||||||
for section_name in section_order:
|
for section_name in section_order:
|
||||||
vars_in_section = interface_data.get(section_name, [])
|
vars_in_section = interface_data.get(section_name, [])
|
||||||
if vars_in_section:
|
if vars_in_section:
|
||||||
|
has_declarations = True
|
||||||
|
# Mapeo de nombres de sección JSON a palabras clave SCL VAR_
|
||||||
scl_section_keyword = f"VAR_{section_name.upper()}"
|
scl_section_keyword = f"VAR_{section_name.upper()}"
|
||||||
if section_name == "Static":
|
if section_name == "Static":
|
||||||
scl_section_keyword = "VAR_STAT"
|
scl_section_keyword = "VAR_STAT" # Para FBs
|
||||||
if section_name == "Temp":
|
if section_name == "Temp":
|
||||||
scl_section_keyword = "VAR_TEMP"
|
scl_section_keyword = "VAR_TEMP"
|
||||||
if section_name == "Constant":
|
if section_name == "Constant":
|
||||||
scl_section_keyword = "CONSTANT"
|
scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_
|
||||||
|
|
||||||
scl_output.append(scl_section_keyword)
|
scl_output.append(scl_section_keyword)
|
||||||
|
# Usar la función recursiva para generar declaraciones
|
||||||
scl_output.extend(
|
scl_output.extend(
|
||||||
generate_scl_declarations(vars_in_section, indent_level=1)
|
generate_scl_declarations(vars_in_section, indent_level=1)
|
||||||
)
|
)
|
||||||
|
# Añadir END_VAR (o END_CONSTANT)
|
||||||
|
scl_output.append(
|
||||||
|
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
||||||
|
)
|
||||||
|
scl_output.append("") # Línea en blanco
|
||||||
|
|
||||||
|
# Guardar nombres de Temp declarados explícitamente
|
||||||
if section_name == "Temp":
|
if section_name == "Temp":
|
||||||
declared_temps.update(
|
declared_temps.update(
|
||||||
format_variable_name(v.get("name"))
|
format_variable_name(v.get("name"))
|
||||||
for v in vars_in_section
|
for v in vars_in_section
|
||||||
if v.get("name")
|
if v.get("name")
|
||||||
)
|
)
|
||||||
scl_output.append("END_VAR")
|
# Declaraciones VAR_TEMP adicionales (auto-detectadas)
|
||||||
scl_output.append("")
|
# Buscar variables que empiecen con #_temp_ en el SCL generado
|
||||||
|
temp_vars_detected = set()
|
||||||
# Declaraciones VAR_TEMP adicionales detectadas
|
# Patrón para encontrar #variable o "#variable"
|
||||||
temp_vars = set()
|
|
||||||
temp_pattern = re.compile(
|
temp_pattern = re.compile(
|
||||||
r'"?#(_temp_[a-zA-Z0-9_]+)"?|"?(_temp_[a-zA-Z0-9_]+)"?'
|
r'"?(#\w+)"?'
|
||||||
)
|
) # Busca # seguido de caracteres alfanuméricos
|
||||||
|
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
for instruction in network.get("logic", []):
|
for instruction in network.get("logic", []):
|
||||||
|
# Revisar el SCL final y el SCL de actualización de memoria si existe
|
||||||
scl_code = instruction.get("scl", "")
|
scl_code = instruction.get("scl", "")
|
||||||
edge_update_code = instruction.get("_edge_mem_update_scl", "")
|
edge_update_code = instruction.get(
|
||||||
|
"_edge_mem_update_scl", ""
|
||||||
|
) # Para flancos
|
||||||
code_to_scan = (
|
code_to_scan = (
|
||||||
(scl_code if scl_code else "")
|
(scl_code if scl_code else "")
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ (edge_update_code if edge_update_code else "")
|
+ (edge_update_code if edge_update_code else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
if code_to_scan:
|
if code_to_scan:
|
||||||
|
# Usar findall para encontrar todas las ocurrencias
|
||||||
found_temps = temp_pattern.findall(code_to_scan)
|
found_temps = temp_pattern.findall(code_to_scan)
|
||||||
for temp_tuple in found_temps:
|
for temp_name in found_temps:
|
||||||
temp_name = next((t for t in temp_tuple if t), None)
|
# findall devuelve el grupo capturado (#...)
|
||||||
if temp_name:
|
if temp_name:
|
||||||
temp_vars.add(
|
temp_vars_detected.add(temp_name)
|
||||||
"#" + temp_name
|
|
||||||
if not temp_name.startswith("#")
|
# Filtrar las que ya estaban declaradas
|
||||||
else temp_name
|
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||||
)
|
|
||||||
additional_temps = sorted(list(temp_vars - declared_temps))
|
|
||||||
if additional_temps:
|
if additional_temps:
|
||||||
if not interface_data.get("Temp"):
|
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
||||||
|
# Si no se declaró la sección Temp antes, añadirla ahora
|
||||||
|
if "Temp" not in interface_data or not interface_data["Temp"]:
|
||||||
scl_output.append("VAR_TEMP")
|
scl_output.append("VAR_TEMP")
|
||||||
for var_name in additional_temps:
|
|
||||||
scl_name = format_variable_name(var_name)
|
for temp_name in additional_temps:
|
||||||
inferred_type = "Bool" # Asumir Bool
|
# Formatear por si acaso, aunque el patrón ya debería dar #nombre
|
||||||
|
scl_name = format_variable_name(temp_name)
|
||||||
|
# Inferir tipo (Bool es lo más común para temporales internos)
|
||||||
|
# Se podría mejorar si el nombre da pistas (ej. _temp_r para Real)
|
||||||
|
inferred_type = "Bool" # Asumir Bool por defecto
|
||||||
scl_output.append(
|
scl_output.append(
|
||||||
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
||||||
)
|
)
|
||||||
if not interface_data.get("Temp"):
|
|
||||||
|
# Si abrimos la sección aquí, cerrarla
|
||||||
|
if "Temp" not in interface_data or not interface_data["Temp"]:
|
||||||
scl_output.append("END_VAR")
|
scl_output.append("END_VAR")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
|
|
||||||
# Cuerpo del Bloque FC/FB
|
# --- Cuerpo del Bloque (BEGIN...END) ---
|
||||||
scl_output.append("BEGIN")
|
scl_output.append("BEGIN")
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
# Iterar por redes y lógica (como antes, incluyendo manejo STL Markdown)
|
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
|
||||||
for i, network in enumerate(data.get("networks", [])):
|
for i, network in enumerate(data.get("networks", [])):
|
||||||
network_title = network.get("title", f'Network {network.get("id")}')
|
network_title = network.get(
|
||||||
|
"title", f'Network {network.get("id", i+1)}'
|
||||||
|
) # Usar i+1 si falta ID
|
||||||
network_comment = network.get("comment", "")
|
network_comment = network.get("comment", "")
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD") # Lenguaje original de la red
|
||||||
scl_output.append(
|
scl_output.append(
|
||||||
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
||||||
)
|
)
|
||||||
if network_comment:
|
if network_comment:
|
||||||
|
# Indentar comentarios de red
|
||||||
for line in network_comment.splitlines():
|
for line in network_comment.splitlines():
|
||||||
scl_output.append(f" // {line}")
|
scl_output.append(f" // {line}")
|
||||||
scl_output.append("")
|
scl_output.append("") # Línea en blanco antes del código de red
|
||||||
|
|
||||||
network_has_code = False
|
network_has_code = False
|
||||||
|
logic_in_network = network.get("logic", [])
|
||||||
|
|
||||||
|
if not logic_in_network:
|
||||||
|
scl_output.append(f" // Network {i+1} has no logic elements.")
|
||||||
|
scl_output.append("")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Manejo Especial Redes STL ---
|
||||||
if network_lang == "STL":
|
if network_lang == "STL":
|
||||||
|
# Asumir que la lógica STL está en el primer elemento como RAW_STL_CHUNK
|
||||||
|
if logic_in_network[0].get("type") == "RAW_STL_CHUNK":
|
||||||
network_has_code = True
|
network_has_code = True
|
||||||
if (
|
raw_stl_code = logic_in_network[0].get(
|
||||||
network.get("logic")
|
|
||||||
and network["logic"][0].get("type") == "RAW_STL_CHUNK"
|
|
||||||
):
|
|
||||||
raw_stl_code = network["logic"][0].get(
|
|
||||||
"stl", "// ERROR: STL code missing"
|
"stl", "// ERROR: STL code missing"
|
||||||
)
|
)
|
||||||
scl_output.append(f" {'//'} ```STL")
|
# Incrustar STL como comentario multi-línea o delimitado
|
||||||
|
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
|
||||||
|
# Comentar cada línea STL
|
||||||
for stl_line in raw_stl_code.splitlines():
|
for stl_line in raw_stl_code.splitlines():
|
||||||
scl_output.append(f" {stl_line}")
|
scl_output.append(f" // {stl_line}")
|
||||||
scl_output.append(f" {'//'} ```")
|
scl_output.append(f" // --- END STL Network {i+1} ---")
|
||||||
|
scl_output.append("") # Línea en blanco después
|
||||||
else:
|
else:
|
||||||
scl_output.append(" // ERROR: Contenido STL inesperado.")
|
scl_output.append(
|
||||||
else: # LAD, FBD, SCL, etc.
|
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
||||||
for instruction in network.get("logic", []):
|
)
|
||||||
|
scl_output.append("")
|
||||||
|
|
||||||
|
# --- Manejo Redes SCL/LAD/FBD procesadas ---
|
||||||
|
else:
|
||||||
|
# Iterar por las instrucciones procesadas
|
||||||
|
for instruction in logic_in_network:
|
||||||
instruction_type = instruction.get("type", "")
|
instruction_type = instruction.get("type", "")
|
||||||
scl_code = instruction.get("scl", "")
|
scl_code = instruction.get("scl", "")
|
||||||
is_grouped = instruction.get("grouped", False)
|
is_grouped = instruction.get("grouped", False)
|
||||||
|
|
||||||
|
# Saltar instrucciones agrupadas (su lógica está en el IF)
|
||||||
if is_grouped:
|
if is_grouped:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder
|
||||||
if (
|
if (
|
||||||
instruction_type.endswith(SCL_SUFFIX)
|
instruction_type.endswith(SCL_SUFFIX)
|
||||||
or instruction_type in ["RAW_SCL_CHUNK", "UNSUPPORTED_LANG"]
|
or instruction_type
|
||||||
|
in [
|
||||||
|
"RAW_SCL_CHUNK",
|
||||||
|
"UNSUPPORTED_LANG",
|
||||||
|
"UNSUPPORTED_CONTENT",
|
||||||
|
"PARSING_ERROR",
|
||||||
|
]
|
||||||
|
or "_error" in instruction_type # Incluir errores comentados
|
||||||
) and scl_code:
|
) and scl_code:
|
||||||
|
|
||||||
|
# Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF)
|
||||||
is_only_comment = all(
|
is_only_comment = all(
|
||||||
line.strip().startswith("//")
|
line.strip().startswith("//")
|
||||||
for line in scl_code.splitlines()
|
for line in scl_code.splitlines()
|
||||||
if line.strip()
|
if line.strip()
|
||||||
)
|
)
|
||||||
is_if_block = scl_code.strip().startswith("IF")
|
is_if_block = scl_code.strip().startswith("IF")
|
||||||
if not is_only_comment or is_if_block:
|
|
||||||
|
# Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error)
|
||||||
|
if (
|
||||||
|
not is_only_comment
|
||||||
|
or is_if_block
|
||||||
|
or "_error" in instruction_type
|
||||||
|
or instruction_type
|
||||||
|
in [
|
||||||
|
"UNSUPPORTED_LANG",
|
||||||
|
"UNSUPPORTED_CONTENT",
|
||||||
|
"PARSING_ERROR",
|
||||||
|
]
|
||||||
|
):
|
||||||
network_has_code = True
|
network_has_code = True
|
||||||
for line in scl_code.splitlines():
|
for line in scl_code.splitlines():
|
||||||
scl_output.append(f" {line}")
|
scl_output.append(f" {line}") # Indentar código
|
||||||
if network_has_code:
|
# Añadir línea en blanco después de cada bloque SCL para legibilidad
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
else:
|
|
||||||
scl_output.append(f" // Network did not produce printable SCL code.")
|
# Si la red no produjo código SCL imprimible (ej. solo lógica interna)
|
||||||
|
if (
|
||||||
|
not network_has_code and network_lang != "STL"
|
||||||
|
): # No añadir para STL ya comentado
|
||||||
|
scl_output.append(
|
||||||
|
f" // Network {i+1} did not produce printable SCL code."
|
||||||
|
)
|
||||||
scl_output.append("")
|
scl_output.append("")
|
||||||
# Fin del bloque FC/FB
|
|
||||||
scl_output.append(f"END_{scl_block_keyword}")
|
# Fin del bloque FC/FB/OB
|
||||||
|
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
|
||||||
|
|
||||||
# --- Escritura del Archivo SCL (Común) ---
|
# --- Escritura del Archivo SCL (Común) ---
|
||||||
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
|
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
|
||||||
|
@ -492,7 +747,7 @@ if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback # Asegurarse que traceback está importado si se usa en generate_scl
|
import traceback # Asegurarse que traceback está importado
|
||||||
|
|
||||||
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
|
# Configurar ArgumentParser para recibir la ruta del XML original obligatoria
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
@ -511,7 +766,6 @@ if __name__ == "__main__":
|
||||||
print(
|
print(
|
||||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
||||||
)
|
)
|
||||||
# No salir necesariamente.
|
|
||||||
|
|
||||||
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
|
@ -521,8 +775,9 @@ if __name__ == "__main__":
|
||||||
input_json_file = os.path.join(
|
input_json_file = os.path.join(
|
||||||
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
||||||
)
|
)
|
||||||
|
# Cambiar extensión de salida a .scl
|
||||||
output_scl_file = os.path.join(
|
output_scl_file = os.path.join(
|
||||||
base_dir, f"{xml_filename_base}_simplified_processed.scl"
|
base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
|
||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
|
@ -540,13 +795,13 @@ if __name__ == "__main__":
|
||||||
sys.exit(1) # Salir si el archivo necesario no está
|
sys.exit(1) # Salir si el archivo necesario no está
|
||||||
else:
|
else:
|
||||||
# Llamar a la función principal de generación SCL del script
|
# Llamar a la función principal de generación SCL del script
|
||||||
# Asumiendo que tu función principal se llama generate_scl(input_json_path, output_scl_path)
|
|
||||||
try:
|
try:
|
||||||
generate_scl(input_json_file, output_scl_file)
|
generate_scl(input_json_file, output_scl_file)
|
||||||
|
sys.exit(0) # Salir con éxito explícitamente
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
|
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
|
||||||
)
|
)
|
||||||
# traceback ya debería estar importado si generate_scl lo necesita
|
# traceback ya debería estar importado
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
sys.exit(1) # Salir con error si la función principal falla
|
||||||
|
|
|
@ -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
|
494
paste.py
494
paste.py
|
@ -1,184 +1,344 @@
|
||||||
# processors/process_call.py
|
# x3_generate_scl.py
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sympy
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
import traceback
|
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
|
# --- Importar Utilidades y Constantes ---
|
||||||
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
|
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
#simplified_en_expr = sympy.simplify_logic(sympy_en_expr, force=True)
|
from processors.processor_utils import format_variable_name
|
||||||
simplified_en_expr = sympy.logic.boolalg.to_dnf(sympy_en_expr, simplify=True)
|
SCL_SUFFIX = "_sympy_processed"
|
||||||
except Exception as e:
|
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||||
print(f"Error simplifying EN for Call {instr_uid} ({block_name_scl}): {e}")
|
except ImportError:
|
||||||
simplified_en_expr = sympy_en_expr # Fallback
|
print("Advertencia: No se pudo importar 'format_variable_name'. Usando fallback.")
|
||||||
en_condition_scl = sympy_expr_to_scl(simplified_en_expr, symbol_manager)
|
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"
|
||||||
|
|
||||||
# Avoid IF TRUE/FALSE blocks
|
# --- Función format_scl_start_value (SIN CAMBIOS) ---
|
||||||
if en_condition_scl == "TRUE":
|
def format_scl_start_value(value, datatype):
|
||||||
scl_final = scl_call_body
|
if value is None: return None
|
||||||
elif en_condition_scl == "FALSE":
|
datatype_lower = datatype.lower() if datatype else ""
|
||||||
scl_final = f"// Call {block_name_scl} (UID {instr_uid}) condition simplified to FALSE."
|
value_str = str(value); value_str_unquoted = value_str
|
||||||
# Also update type to avoid further processing?
|
if value_str.startswith('"') and value_str.endswith('"') and len(value_str) > 1: value_str_unquoted = value_str[1:-1]
|
||||||
# instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}_Optimized"
|
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:
|
||||||
|
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:
|
else:
|
||||||
# Indent the call body within the IF block
|
escaped_for_scl = value_str_unquoted.replace("\\", "\\\\").replace("'", "''").replace("\n", "").replace("\r", ""); return f"'{escaped_for_scl}'"
|
||||||
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;"
|
# --- 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:
|
else:
|
||||||
# No IF needed if EN is always TRUE
|
if start_value is not None:
|
||||||
scl_final = scl_call_body
|
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
|
||||||
|
|
||||||
# --- Actualizar Instrucción y Mapa SymPy ---
|
# --- NUEVAS FUNCIONES para generar Markdown ---
|
||||||
instruction["scl"] = scl_final # Guardar el SCL final generado
|
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
|
||||||
|
|
||||||
# Update instruction type to mark as processed (unless already marked as error)
|
# --- generate_markdown_member_rows (MODIFICADA) ---
|
||||||
if "_error" not in instruction.get("type", ""):
|
def generate_markdown_member_rows(members, level=0):
|
||||||
instruction["type"] = f"Call_{block_type}{SCL_SUFFIX}"
|
"""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
|
||||||
|
|
||||||
# Propagar el estado ENO (es la expresión SymPy de EN)
|
md_rows.append(f"| {prefix}`{name}` | `{datatype}` | `{start_value_fmt}` | {comment} |")
|
||||||
map_key_eno = (network_id, instr_uid, "eno")
|
children = member.get("children")
|
||||||
sympy_map[map_key_eno] = sympy_en_expr # Guardar la expresión SymPy para ENO
|
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
|
||||||
|
|
||||||
# --- Propagar Valores de Salida (Importante pero complejo) ---
|
def generate_tag_table_markdown(data):
|
||||||
# Esto requiere conocer la interfaz del bloque llamado (que no tenemos aquí directamente)
|
"""Genera contenido Markdown para una tabla de tags."""
|
||||||
# O asumir convenciones estándar (ej. FCs tienen Ret_Val, FBs tienen outputs en su instancia)
|
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
|
||||||
|
|
||||||
# Heurística simple: Si es un FC, intentar propagar Ret_Val si existe en outputs
|
# --- Función Principal de Generación (SIN CAMBIOS EN LA LÓGICA PRINCIPAL) ---
|
||||||
# Si es un FB, las salidas se acceden a través de la instancia (e.g., "MyInstance".Output1)
|
def generate_scl_or_markdown(processed_json_filepath, output_directory):
|
||||||
# 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.
|
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
|
||||||
|
|
||||||
# Ejemplo básico (necesita mejorar):
|
block_name = data.get("block_name", "UnknownBlock"); block_number = data.get("block_number")
|
||||||
# for pin_name, dest_list in instruction.get("outputs", {}).items():
|
block_type = data.get("block_type", "Unknown"); block_comment = data.get("block_comment", "")
|
||||||
# if pin_name != 'eno' and dest_list: # Asumir que hay un destino
|
scl_block_name = format_variable_name(block_name); output_content = []; output_extension = ".scl"
|
||||||
# map_key_out = (network_id, instr_uid, pin_name)
|
print(f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_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} */"
|
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
return True
|
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()
|
||||||
|
|
||||||
# --- Processor Information Function ---
|
# --- Ejecución (SIN CAMBIOS) ---
|
||||||
def get_processor_info():
|
if __name__ == "__main__":
|
||||||
"""Devuelve la información para las llamadas a FC y FB."""
|
parser = argparse.ArgumentParser(description="Generate final SCL or Markdown file from processed JSON.")
|
||||||
# Asegurarse que los type_name coincidan con los usados en x1 y x2
|
parser.add_argument("source_xml_filepath",help="Path to the original source XML file.")
|
||||||
return [
|
args = parser.parse_args(); source_xml_file = args.source_xml_filepath
|
||||||
{'type_name': 'call_fc', 'processor_func': process_call, 'priority': 6}, # Prioridad alta
|
if not os.path.exists(source_xml_file): print(f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'.")
|
||||||
{'type_name': 'call_fb', 'processor_func': process_call, 'priority': 6} # Prioridad alta
|
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:
|
||||||
|
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)
|
1831
x1_to_json.py
1831
x1_to_json.py
File diff suppressed because it is too large
Load Diff
272
x2_process.py
272
x2_process.py
|
@ -18,14 +18,14 @@ from processors.processor_utils import (
|
||||||
from processors.symbol_manager import SymbolManager # Import the manager
|
from processors.symbol_manager import SymbolManager # Import the manager
|
||||||
|
|
||||||
# --- Constantes y Configuración ---
|
# --- Constantes y Configuración ---
|
||||||
SCL_SUFFIX = "_sympy_processed" # New suffix to indicate processing method
|
SCL_SUFFIX = "_sympy_processed"
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
GROUPED_COMMENT = "// Logic included in grouped IF"
|
||||||
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script" # May still be useful
|
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script"
|
||||||
|
|
||||||
# Global data dictionary
|
# Global data dictionary
|
||||||
data = {}
|
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):
|
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
"""
|
"""
|
||||||
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
Busca condiciones (ya procesadas -> tienen expr SymPy en sympy_map)
|
||||||
|
@ -203,7 +203,6 @@ def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
||||||
|
|
||||||
return made_change
|
return made_change
|
||||||
|
|
||||||
|
|
||||||
def load_processors(processors_dir="processors"):
|
def load_processors(processors_dir="processors"):
|
||||||
"""
|
"""
|
||||||
Escanea el directorio, importa módulos, construye el mapa y una lista
|
Escanea el directorio, importa módulos, construye el mapa y una lista
|
||||||
|
@ -300,10 +299,11 @@ def load_processors(processors_dir="processors"):
|
||||||
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
# Devolver el mapa (para lookup rápido si es necesario) y la lista ordenada
|
||||||
return processor_map, processor_list_sorted
|
return processor_map, processor_list_sorted
|
||||||
|
|
||||||
# --- Bucle Principal de Procesamiento (Modificado para STL y tipo de bloque) ---
|
|
||||||
|
# --- Bucle Principal de Procesamiento (MODIFICADO) ---
|
||||||
def process_json_to_scl(json_filepath):
|
def process_json_to_scl(json_filepath):
|
||||||
"""
|
"""
|
||||||
Lee JSON simplificado, aplica procesadores dinámicos (ignorando redes STL y bloques DB),
|
Lee JSON simplificado, aplica procesadores dinámicos (ignorando STL, UDT, TagTable, DB),
|
||||||
y guarda JSON procesado.
|
y guarda JSON procesado.
|
||||||
"""
|
"""
|
||||||
global data
|
global data
|
||||||
|
@ -320,32 +320,30 @@ def process_json_to_scl(json_filepath):
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB) ---
|
# --- MODIFICADO: Obtener tipo de bloque (FC, FB, GlobalDB, OB, PlcUDT, PlcTagTable) ---
|
||||||
block_type = data.get("block_type", "Unknown") # FC, FB, GlobalDB, OB
|
block_type = data.get("block_type", "Unknown")
|
||||||
print(f"Procesando bloque tipo: {block_type}, Lenguaje principal: {data.get('language', 'Unknown')}")
|
print(f"Procesando bloque tipo: {block_type}")
|
||||||
|
|
||||||
# --- MODIFICADO: SI ES UN GlobalDB, SALTAR EL PROCESAMIENTO LÓGICO ---
|
# --- MODIFICADO: SALTAR PROCESAMIENTO PARA DB, UDT, TAG TABLE ---
|
||||||
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable"]: # <-- Comprobar tipos a saltar
|
||||||
print(
|
print(f"INFO: El bloque es {block_type}. 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(
|
output_filename = json_filepath.replace(
|
||||||
"_simplified.json", "_simplified_processed.json"
|
"_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:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_filename, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
print("Guardado de DB completado.")
|
print(f"Guardado de {block_type} completado.")
|
||||||
except Exception as e:
|
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()
|
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 ---
|
# --- 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...") # <-- Mensaje actualizado
|
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__)
|
script_dir = os.path.dirname(__file__)
|
||||||
processors_dir_path = os.path.join(script_dir, "processors")
|
processors_dir_path = os.path.join(script_dir, "processors")
|
||||||
processor_map, sorted_processors = load_processors(processors_dir_path)
|
processor_map, sorted_processors = load_processors(processors_dir_path)
|
||||||
|
@ -354,42 +352,30 @@ def process_json_to_scl(json_filepath):
|
||||||
return
|
return
|
||||||
|
|
||||||
network_access_maps = {}
|
network_access_maps = {}
|
||||||
# Crear mapas de acceso por red (copiado/adaptado de versión anterior)
|
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
net_id = network["id"]
|
net_id = network["id"]
|
||||||
current_access_map = {}
|
current_access_map = {}
|
||||||
for instr in network.get("logic", []):
|
for instr in network.get("logic", []):
|
||||||
for _, source in instr.get("inputs", {}).items():
|
for _, source in instr.get("inputs", {}).items():
|
||||||
sources_to_check = (
|
sources_to_check = (source if isinstance(source, list) else ([source] if isinstance(source, dict) else []))
|
||||||
source
|
|
||||||
if isinstance(source, list)
|
|
||||||
else ([source] if isinstance(source, dict) else [])
|
|
||||||
)
|
|
||||||
for src in sources_to_check:
|
for src in sources_to_check:
|
||||||
if (
|
if (isinstance(src, dict) and src.get("uid") and src.get("type") in ["variable", "constant"]):
|
||||||
isinstance(src, dict)
|
|
||||||
and src.get("uid")
|
|
||||||
and src.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[src["uid"]] = src
|
current_access_map[src["uid"]] = src
|
||||||
for _, dest_list in instr.get("outputs", {}).items():
|
for _, dest_list in instr.get("outputs", {}).items():
|
||||||
if isinstance(dest_list, list):
|
if isinstance(dest_list, list):
|
||||||
for dest in dest_list:
|
for dest in dest_list:
|
||||||
if (
|
if (isinstance(dest, dict) and dest.get("uid") and dest.get("type") in ["variable", "constant"]):
|
||||||
isinstance(dest, dict)
|
|
||||||
and dest.get("uid")
|
|
||||||
and dest.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[dest["uid"]] = dest
|
current_access_map[dest["uid"]] = dest
|
||||||
network_access_maps[net_id] = current_access_map
|
network_access_maps[net_id] = current_access_map
|
||||||
|
|
||||||
|
# (Inicialización de SymbolManager y bucle iterativo SIN CAMBIOS)
|
||||||
symbol_manager = SymbolManager()
|
symbol_manager = SymbolManager()
|
||||||
sympy_map = {}
|
sympy_map = {}
|
||||||
max_passes = 30
|
max_passes = 30
|
||||||
passes = 0
|
passes = 0
|
||||||
processing_complete = False
|
processing_complete = False
|
||||||
|
|
||||||
print(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:
|
while passes < max_passes and not processing_complete:
|
||||||
passes += 1
|
passes += 1
|
||||||
made_change_in_base_pass = False
|
made_change_in_base_pass = False
|
||||||
|
@ -398,246 +384,140 @@ def process_json_to_scl(json_filepath):
|
||||||
num_sympy_processed_this_pass = 0
|
num_sympy_processed_this_pass = 0
|
||||||
num_grouped_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):")
|
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:
|
for processor_info in sorted_processors:
|
||||||
current_type_name = processor_info["type_name"]
|
current_type_name = processor_info["type_name"]
|
||||||
func_to_call = processor_info["func"]
|
func_to_call = processor_info["func"]
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network["id"]
|
network_id = network["id"]
|
||||||
network_lang = network.get("language", "LAD") # Lenguaje de la red
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL": # Saltar redes STL
|
if network_lang == "STL": continue
|
||||||
continue
|
|
||||||
|
|
||||||
access_map = network_access_maps.get(network_id, {})
|
access_map = network_access_maps.get(network_id, {})
|
||||||
network_logic = network.get("logic", [])
|
network_logic = network.get("logic", [])
|
||||||
for instruction in network_logic:
|
for instruction in network_logic:
|
||||||
instr_uid = instruction.get("instruction_uid")
|
instr_uid = instruction.get("instruction_uid")
|
||||||
# Usar el tipo *actual* de la instrucción para el lookup
|
|
||||||
instr_type_current = instruction.get("type", "Unknown")
|
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
|
||||||
if (
|
instr_type_current in ["RAW_STL_CHUNK", "RAW_SCL_CHUNK", "UNSUPPORTED_LANG", "UNSUPPORTED_CONTENT", "PARSING_ERROR"]):
|
||||||
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
|
continue
|
||||||
|
|
||||||
# El lookup usa el tipo actual (que aún no tiene el sufijo)
|
|
||||||
lookup_key = instr_type_current.lower()
|
lookup_key = instr_type_current.lower()
|
||||||
effective_type_name = lookup_key
|
effective_type_name = lookup_key
|
||||||
|
|
||||||
# Mapeo especial para llamadas FC/FB
|
|
||||||
if instr_type_current == "Call":
|
if instr_type_current == "Call":
|
||||||
call_block_type = instruction.get("block_type", "").upper()
|
call_block_type = instruction.get("block_type", "").upper()
|
||||||
if call_block_type == "FC":
|
if call_block_type == "FC": effective_type_name = "call_fc"
|
||||||
effective_type_name = "call_fc"
|
elif call_block_type == "FB": effective_type_name = "call_fb"
|
||||||
elif call_block_type == "FB":
|
|
||||||
effective_type_name = "call_fb"
|
|
||||||
# Añadir otros tipos de llamada si es necesario
|
|
||||||
|
|
||||||
# Si el tipo efectivo coincide con el procesador actual
|
|
||||||
if effective_type_name == current_type_name:
|
if effective_type_name == current_type_name:
|
||||||
try:
|
try:
|
||||||
# Pasar 'data' a la función del procesador
|
changed = func_to_call(instruction, network_id, sympy_map, symbol_manager, data)
|
||||||
changed = func_to_call(
|
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
)
|
|
||||||
if changed:
|
if changed:
|
||||||
made_change_in_base_pass = True
|
made_change_in_base_pass = True
|
||||||
num_sympy_processed_this_pass += 1
|
num_sympy_processed_this_pass += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}")
|
||||||
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
instruction["scl"] = (
|
instruction["scl"] = f"// ERROR en SymPy procesador base: {e}"
|
||||||
f"// ERROR en SymPy procesador base: {e}"
|
|
||||||
)
|
|
||||||
# Añadir sufijo de error al tipo actual
|
|
||||||
instruction["type"] = instr_type_current + "_error"
|
instruction["type"] = instr_type_current + "_error"
|
||||||
made_change_in_base_pass = True # Se hizo un cambio (marcar como error)
|
made_change_in_base_pass = True
|
||||||
print(
|
print(f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy.")
|
||||||
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# FASE 2: Agrupación IF (Ignorando STL)
|
||||||
# --- FASE 2: Agrupación IF (Ignorando STL) ---
|
if made_change_in_base_pass or passes == 1:
|
||||||
if (
|
|
||||||
made_change_in_base_pass or passes == 1
|
|
||||||
): # Ejecutar siempre en el primer pase o si hubo cambios
|
|
||||||
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
||||||
num_grouped_this_pass = 0 # Resetear contador para el pase
|
num_grouped_this_pass = 0
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network["id"]
|
network_id = network["id"]
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL":
|
if network_lang == "STL": continue
|
||||||
continue # Saltar STL
|
|
||||||
network_logic = network.get("logic", [])
|
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")])
|
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:
|
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)
|
instruction = next((instr for instr in network_logic if instr.get("instruction_uid") == uid_to_process), None)
|
||||||
if not instruction: continue
|
if not instruction: continue
|
||||||
|
if instruction.get("grouped") or "_error" in instruction.get("type", ""): 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):
|
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
||||||
try:
|
try:
|
||||||
group_changed = process_group_ifs(
|
group_changed = process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data)
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
)
|
|
||||||
if group_changed:
|
if group_changed:
|
||||||
made_change_in_group_pass = True
|
made_change_in_group_pass = True
|
||||||
num_grouped_this_pass += 1
|
num_grouped_this_pass += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}")
|
||||||
f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print(
|
print(f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL).")
|
||||||
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:
|
if not made_change_in_base_pass and not made_change_in_group_pass:
|
||||||
print(
|
print(f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---")
|
||||||
f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---"
|
|
||||||
)
|
|
||||||
processing_complete = True
|
processing_complete = True
|
||||||
else:
|
else:
|
||||||
print(
|
print(f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando...")
|
||||||
f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..."
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Comprobar límite de pases ---
|
|
||||||
if passes == max_passes and not processing_complete:
|
if passes == max_passes and not processing_complete:
|
||||||
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
|
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
|
||||||
|
|
||||||
# --- FIN BUCLE ITERATIVO ---
|
# --- FIN BUCLE ITERATIVO ---
|
||||||
|
|
||||||
# --- Verificación Final (Ajustada para RAW_STL_CHUNK) ---
|
# (Verificación Final y Guardado JSON SIN CAMBIOS)
|
||||||
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---") # <-- Mensaje actualizado
|
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---")
|
||||||
unprocessed_count = 0
|
unprocessed_count = 0
|
||||||
unprocessed_details = []
|
unprocessed_details = []
|
||||||
ignored_types = [
|
ignored_types = ["raw_scl_chunk", "unsupported_lang", "raw_stl_chunk", "unsupported_content", "parsing_error"]
|
||||||
"raw_scl_chunk",
|
|
||||||
"unsupported_lang",
|
|
||||||
"raw_stl_chunk",
|
|
||||||
"unsupported_content", # Añadido de x1
|
|
||||||
"parsing_error", # Añadido de x1
|
|
||||||
]
|
|
||||||
for network in data.get("networks", []):
|
for network in data.get("networks", []):
|
||||||
network_id = network.get("id", "Unknown ID")
|
network_id = network.get("id", "Unknown ID")
|
||||||
network_title = network.get("title", f"Network {network_id}")
|
network_title = network.get("title", f"Network {network_id}")
|
||||||
network_lang = network.get("language", "LAD")
|
network_lang = network.get("language", "LAD")
|
||||||
if network_lang == "STL":
|
if network_lang == "STL": continue
|
||||||
continue # No verificar redes STL
|
|
||||||
for instruction in network.get("logic", []):
|
for instruction in network.get("logic", []):
|
||||||
instr_uid = instruction.get("instruction_uid", "Unknown UID")
|
instr_uid = instruction.get("instruction_uid", "Unknown UID")
|
||||||
instr_type = instruction.get("type", "Unknown Type")
|
instr_type = instruction.get("type", "Unknown Type")
|
||||||
is_grouped = instruction.get("grouped", False)
|
is_grouped = instruction.get("grouped", False)
|
||||||
if (
|
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):
|
||||||
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_count += 1
|
||||||
unprocessed_details.append(
|
unprocessed_details.append(f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), Instrucción UID: {instr_uid}, Tipo: '{instr_type}'")
|
||||||
f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), "
|
|
||||||
f"Instrucción UID: {instr_uid}, Tipo: '{instr_type}'"
|
|
||||||
)
|
|
||||||
if unprocessed_count > 0:
|
if unprocessed_count > 0:
|
||||||
print(
|
print(f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:")
|
||||||
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.")
|
||||||
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")
|
||||||
output_filename = json_filepath.replace(
|
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}")
|
||||||
"_simplified.json", "_simplified_processed.json"
|
|
||||||
)
|
|
||||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_filename}") # <-- Mensaje actualizado
|
|
||||||
try:
|
try:
|
||||||
with open(output_filename, "w", encoding="utf-8") as f:
|
with open(output_filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
||||||
print("Guardado completado.")
|
print("Guardado completado.")
|
||||||
except Exception as e:
|
except Exception as e: print(f"Error Crítico al guardar JSON procesado: {e}"); traceback.print_exc()
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
# Imports necesarios solo para la ejecución como script principal
|
parser = argparse.ArgumentParser(description="Process simplified JSON (_simplified.json) to embed SCL logic (SymPy version). Expects original XML filepath as argument.")
|
||||||
import argparse
|
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).")
|
||||||
import os
|
args = parser.parse_args()
|
||||||
import sys
|
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):
|
if not os.path.exists(source_xml_file):
|
||||||
print(
|
print(f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.")
|
||||||
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Derivar nombre del archivo JSON de entrada (_simplified.json)
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
# Asumir que el JSON simplificado está en el mismo directorio que el XML original
|
input_dir = os.path.dirname(source_xml_file)
|
||||||
input_dir = os.path.dirname(source_xml_file) # Directorio del XML original
|
|
||||||
input_json_file = os.path.join(input_dir, f"{xml_filename_base}_simplified.json")
|
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
|
print(f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'")
|
||||||
output_json_file = os.path.join(
|
|
||||||
input_dir, f"{xml_filename_base}_simplified_processed.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verificar si el archivo JSON de entrada (_simplified.json) EXISTE antes de procesar
|
|
||||||
if not os.path.exists(input_json_file):
|
if not os.path.exists(input_json_file):
|
||||||
print(
|
print(f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'")
|
||||||
f"Error Fatal (x2): El archivo de entrada JSON simplificado no existe: '{input_json_file}'"
|
print(f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.")
|
||||||
)
|
sys.exit(1)
|
||||||
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á
|
|
||||||
else:
|
else:
|
||||||
# Llamar a la función principal de procesamiento del script
|
|
||||||
try:
|
try:
|
||||||
process_json_to_scl(input_json_file)
|
process_json_to_scl(input_json_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}")
|
||||||
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}"
|
import traceback
|
||||||
)
|
|
||||||
import traceback # Asegurar que traceback está importado
|
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
sys.exit(1)
|
|
@ -381,11 +381,103 @@ def generate_scl_declarations(variables, indent_level=1):
|
||||||
|
|
||||||
return scl_lines
|
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 ---
|
# Extraer miembros (asumiendo que están en interface['None'])
|
||||||
def generate_scl(processed_json_filepath, output_scl_filepath):
|
members = data.get("interface", {}).get("None", [])
|
||||||
"""Genera un archivo SCL a partir del JSON procesado (FC/FB/OB o DB).""" # Actualizado
|
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):
|
if not os.path.exists(processed_json_filepath):
|
||||||
print(
|
print(
|
||||||
f"Error: Archivo JSON procesado no encontrado en '{processed_json_filepath}'"
|
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()
|
traceback.print_exc()
|
||||||
return
|
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_name = data.get("block_name", "UnknownBlock")
|
||||||
block_number = data.get("block_number")
|
block_number = data.get("block_number")
|
||||||
# block_lang_original = data.get("language", "Unknown") # Lenguaje original (SCL, LAD, DB...)
|
|
||||||
block_type = data.get(
|
block_type = data.get(
|
||||||
"block_type", "Unknown"
|
"block_type", "Unknown"
|
||||||
) # Tipo de bloque (FC, FB, GlobalDB, OB) <-- Usar este
|
) # FC, FB, OB, GlobalDB, PlcUDT, PlcTagTable
|
||||||
block_comment = data.get("block_comment", "")
|
block_comment = data.get("block_comment", "")
|
||||||
scl_block_name = format_variable_name(block_name) # Nombre SCL seguro
|
scl_block_name = format_variable_name(block_name)
|
||||||
print(
|
output_content = []
|
||||||
f"Generando SCL para: {block_type} '{scl_block_name}' (Original: {block_name})" # Quitado lenguaje original del log
|
output_extension = ".scl" # Default
|
||||||
)
|
|
||||||
scl_output = []
|
|
||||||
|
|
||||||
# --- MODIFICADO: GENERACIÓN PARA DATA BLOCK (GlobalDB) ---
|
print(
|
||||||
if block_type == "GlobalDB": # <-- Comprobar tipo de bloque
|
f"Generando salida para: {block_type} '{scl_block_name}' (Original: {block_name})"
|
||||||
print("Modo de generación: DATA_BLOCK")
|
)
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
|
||||||
scl_output.append(f"// 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:
|
if block_number:
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
output_content.append(f"// Block Number: {block_number}")
|
||||||
if block_comment:
|
if block_comment:
|
||||||
# Dividir comentarios largos en múltiples líneas
|
output_content.append(f"// Block Comment:")
|
||||||
comment_lines = block_comment.splitlines()
|
for line in block_comment.splitlines():
|
||||||
scl_output.append(f"// Block Comment:")
|
output_content.append(f"// {line}")
|
||||||
for line in comment_lines:
|
output_content.append("")
|
||||||
scl_output.append(f"// {line}")
|
output_content.append(f'DATA_BLOCK "{scl_block_name}"')
|
||||||
scl_output.append("")
|
output_content.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||||
scl_output.append(f'DATA_BLOCK "{scl_block_name}"')
|
output_content.append("VERSION : 0.1")
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
output_content.append("")
|
||||||
scl_output.append("VERSION : 0.1")
|
|
||||||
scl_output.append("")
|
|
||||||
interface_data = data.get("interface", {})
|
interface_data = data.get("interface", {})
|
||||||
# En DBs, la sección relevante suele ser 'Static'
|
|
||||||
static_vars = interface_data.get("Static", [])
|
static_vars = interface_data.get("Static", [])
|
||||||
if static_vars:
|
if static_vars:
|
||||||
scl_output.append("VAR")
|
output_content.append("VAR")
|
||||||
# Usar la función recursiva para generar declaraciones
|
output_content.extend(
|
||||||
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1))
|
generate_scl_declarations(static_vars, indent_level=1)
|
||||||
scl_output.append("END_VAR")
|
)
|
||||||
scl_output.append("")
|
output_content.append("END_VAR")
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
"Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB."
|
||||||
)
|
)
|
||||||
# Añadir bloque VAR vacío si no hay variables
|
output_content.append("VAR\nEND_VAR") # Añadir vacío
|
||||||
scl_output.append("VAR")
|
output_content.append("")
|
||||||
scl_output.append("END_VAR")
|
output_content.append("BEGIN")
|
||||||
scl_output.append("")
|
output_content.append(" // Data Blocks have no executable code")
|
||||||
scl_output.append("BEGIN")
|
output_content.append("END_DATA_BLOCK")
|
||||||
scl_output.append(
|
|
||||||
" // Los Data Blocks no tienen código ejecutable en BEGIN/END"
|
|
||||||
)
|
|
||||||
scl_output.append("END_DATA_BLOCK")
|
|
||||||
|
|
||||||
# --- MODIFICADO: GENERACIÓN PARA FC/FB/OB ---
|
elif block_type in ["FC", "FB", "OB"]:
|
||||||
else:
|
print(f" -> Modo de generación: {block_type} SCL")
|
||||||
# Determinar palabra clave SCL
|
output_extension = ".scl"
|
||||||
scl_block_keyword = "FUNCTION_BLOCK" # Default
|
# (Lógica de generación SCL para FC/FB/OB como estaba antes)
|
||||||
|
scl_block_keyword = "FUNCTION_BLOCK"
|
||||||
if block_type == "FC":
|
if block_type == "FC":
|
||||||
scl_block_keyword = "FUNCTION"
|
scl_block_keyword = "FUNCTION"
|
||||||
elif block_type == "OB":
|
elif block_type == "OB":
|
||||||
scl_block_keyword = "ORGANIZATION_BLOCK"
|
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}")
|
output_content.append(f"// Block Type: {block_type}")
|
||||||
|
if block_name != scl_block_name:
|
||||||
# Cabecera del Bloque
|
output_content.append(f"// Block Name (Original): {block_name}")
|
||||||
scl_output.append(f"// Block Type: {block_type}")
|
|
||||||
scl_output.append(f"// Block Name (Original): {block_name}")
|
|
||||||
if block_number:
|
if block_number:
|
||||||
scl_output.append(f"// Block Number: {block_number}")
|
output_content.append(f"// Block Number: {block_number}")
|
||||||
# Indicar lenguaje original de las redes si es relevante
|
|
||||||
original_net_langs = set(
|
original_net_langs = set(
|
||||||
n.get("language", "Unknown") for n in data.get("networks", [])
|
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')}"
|
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
|
||||||
)
|
)
|
||||||
if block_comment:
|
if block_comment:
|
||||||
comment_lines = block_comment.splitlines()
|
output_content.append(f"// Block Comment:")
|
||||||
scl_output.append(f"// Block Comment:")
|
for line in block_comment.splitlines():
|
||||||
for line in comment_lines:
|
output_content.append(f"// {line}")
|
||||||
scl_output.append(f"// {line}")
|
output_content.append("")
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Manejar tipo de retorno para FUNCTION (FC)
|
return_type = "Void"
|
||||||
return_type = "Void" # Default
|
|
||||||
interface_data = data.get("interface", {})
|
interface_data = data.get("interface", {})
|
||||||
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
|
if scl_block_keyword == "FUNCTION" and interface_data.get("Return"):
|
||||||
# Asumir un solo valor de retorno
|
|
||||||
return_member = interface_data["Return"][0]
|
return_member = interface_data["Return"][0]
|
||||||
return_type_raw = return_member.get("datatype", "Void")
|
return_type_raw = return_member.get("datatype", "Void")
|
||||||
# Limpiar comillas si es UDT/String
|
|
||||||
return_type = (
|
return_type = (
|
||||||
return_type_raw[1:-1]
|
return_type_raw[1:-1]
|
||||||
if isinstance(return_type_raw, str)
|
if isinstance(return_type_raw, str)
|
||||||
|
@ -508,177 +592,127 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
and return_type_raw.endswith('"')
|
and return_type_raw.endswith('"')
|
||||||
else return_type_raw
|
else return_type_raw
|
||||||
)
|
)
|
||||||
# Añadir comillas si es UDT y no las tenía
|
|
||||||
if (
|
if (
|
||||||
return_type != return_type_raw
|
return_type != return_type_raw
|
||||||
and not return_type_raw.lower().startswith("array")
|
and not return_type_raw.lower().startswith("array")
|
||||||
):
|
):
|
||||||
return_type = f'"{return_type}"'
|
return_type = f'"{return_type}"'
|
||||||
else: # Mantener raw si es tipo básico o ya tenía comillas
|
else:
|
||||||
return_type = return_type_raw
|
return_type = return_type_raw
|
||||||
|
|
||||||
# Línea de declaración del bloque
|
|
||||||
if scl_block_keyword == "FUNCTION":
|
if scl_block_keyword == "FUNCTION":
|
||||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
|
output_content.append(
|
||||||
else: # FB y OB
|
f'{scl_block_keyword} "{scl_block_name}" : {return_type}'
|
||||||
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
|
)
|
||||||
|
else:
|
||||||
|
output_content.append(f'{scl_block_keyword} "{scl_block_name}"')
|
||||||
|
|
||||||
# Atributos y versión
|
output_content.append("{ S7_Optimized_Access := 'TRUE' }")
|
||||||
scl_output.append("{ S7_Optimized_Access := 'TRUE' }") # Asumir optimizado
|
output_content.append("VERSION : 0.1")
|
||||||
scl_output.append("VERSION : 0.1")
|
output_content.append("")
|
||||||
scl_output.append("")
|
|
||||||
|
|
||||||
# Declaraciones de Interfaz (Input, Output, InOut, Static, Temp, Constant)
|
|
||||||
# Orden estándar SCL
|
|
||||||
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
section_order = ["Input", "Output", "InOut", "Static", "Temp", "Constant"]
|
||||||
declared_temps = set() # Para rastrear temps ya declaradas
|
declared_temps = set()
|
||||||
has_declarations = False
|
has_declarations = False
|
||||||
|
|
||||||
for section_name in section_order:
|
for section_name in section_order:
|
||||||
vars_in_section = interface_data.get(section_name, [])
|
vars_in_section = interface_data.get(section_name, [])
|
||||||
if vars_in_section:
|
if vars_in_section:
|
||||||
has_declarations = True
|
has_declarations = True
|
||||||
# Mapeo de nombres de sección JSON a palabras clave SCL VAR_
|
|
||||||
scl_section_keyword = f"VAR_{section_name.upper()}"
|
scl_section_keyword = f"VAR_{section_name.upper()}"
|
||||||
if section_name == "Static":
|
if section_name == "Static":
|
||||||
scl_section_keyword = "VAR_STAT" # Para FBs
|
scl_section_keyword = "VAR_STAT"
|
||||||
if section_name == "Temp":
|
if section_name == "Temp":
|
||||||
scl_section_keyword = "VAR_TEMP"
|
scl_section_keyword = "VAR_TEMP"
|
||||||
if section_name == "Constant":
|
if section_name == "Constant":
|
||||||
scl_section_keyword = "CONSTANT" # CONSTANT no usa VAR_
|
scl_section_keyword = "CONSTANT"
|
||||||
|
output_content.append(scl_section_keyword)
|
||||||
scl_output.append(scl_section_keyword)
|
output_content.extend(
|
||||||
# Usar la función recursiva para generar declaraciones
|
|
||||||
scl_output.extend(
|
|
||||||
generate_scl_declarations(vars_in_section, indent_level=1)
|
generate_scl_declarations(vars_in_section, indent_level=1)
|
||||||
)
|
)
|
||||||
# Añadir END_VAR (o END_CONSTANT)
|
output_content.append(
|
||||||
scl_output.append(
|
|
||||||
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
"END_VAR" if section_name != "Constant" else "END_CONSTANT"
|
||||||
)
|
)
|
||||||
scl_output.append("") # Línea en blanco
|
output_content.append("")
|
||||||
|
|
||||||
# Guardar nombres de Temp declarados explícitamente
|
|
||||||
if section_name == "Temp":
|
if section_name == "Temp":
|
||||||
declared_temps.update(
|
declared_temps.update(
|
||||||
format_variable_name(v.get("name"))
|
format_variable_name(v.get("name"))
|
||||||
for v in vars_in_section
|
for v in vars_in_section
|
||||||
if v.get("name")
|
if v.get("name")
|
||||||
)
|
)
|
||||||
# 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 network in data.get("networks", []):
|
||||||
for instruction in network.get("logic", []):
|
for instruction in network.get("logic", []):
|
||||||
# Revisar el SCL final y el SCL de actualización de memoria si existe
|
|
||||||
scl_code = instruction.get("scl", "")
|
scl_code = instruction.get("scl", "")
|
||||||
edge_update_code = instruction.get(
|
edge_update_code = instruction.get("_edge_mem_update_scl", "")
|
||||||
"_edge_mem_update_scl", ""
|
|
||||||
) # Para flancos
|
|
||||||
code_to_scan = (
|
code_to_scan = (
|
||||||
(scl_code if scl_code else "")
|
(scl_code if scl_code else "")
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ (edge_update_code if edge_update_code else "")
|
+ (edge_update_code if edge_update_code else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
if code_to_scan:
|
if code_to_scan:
|
||||||
# Usar findall para encontrar todas las ocurrencias
|
|
||||||
found_temps = temp_pattern.findall(code_to_scan)
|
found_temps = temp_pattern.findall(code_to_scan)
|
||||||
for temp_name in found_temps:
|
for temp_name in found_temps:
|
||||||
# findall devuelve el grupo capturado (#...)
|
|
||||||
if temp_name:
|
if temp_name:
|
||||||
temp_vars_detected.add(temp_name)
|
temp_vars_detected.add(temp_name)
|
||||||
|
|
||||||
# Filtrar las que ya estaban declaradas
|
|
||||||
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
additional_temps = sorted(list(temp_vars_detected - declared_temps))
|
||||||
|
|
||||||
if additional_temps:
|
if additional_temps:
|
||||||
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
|
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"]:
|
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:
|
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)
|
scl_name = format_variable_name(temp_name)
|
||||||
# Inferir tipo (Bool es lo más común para temporales internos)
|
inferred_type = "Bool"
|
||||||
# Se podría mejorar si el nombre da pistas (ej. _temp_r para Real)
|
output_content.append(
|
||||||
inferred_type = "Bool" # Asumir Bool por defecto
|
|
||||||
scl_output.append(
|
|
||||||
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
|
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"]:
|
if "Temp" not in interface_data or not interface_data["Temp"]:
|
||||||
scl_output.append("END_VAR")
|
output_content.append("END_VAR")
|
||||||
scl_output.append("")
|
output_content.append("")
|
||||||
|
|
||||||
# --- Cuerpo del Bloque (BEGIN...END) ---
|
output_content.append("BEGIN")
|
||||||
scl_output.append("BEGIN")
|
output_content.append("")
|
||||||
scl_output.append("")
|
|
||||||
# Iterar por redes y lógica (incluyendo manejo STL/SCL crudo)
|
|
||||||
for i, network in enumerate(data.get("networks", [])):
|
for i, network in enumerate(data.get("networks", [])):
|
||||||
network_title = network.get(
|
network_title = network.get("title", f'Network {network.get("id", i+1)}')
|
||||||
"title", f'Network {network.get("id", i+1)}'
|
|
||||||
) # Usar i+1 si falta ID
|
|
||||||
network_comment = network.get("comment", "")
|
network_comment = network.get("comment", "")
|
||||||
network_lang = network.get("language", "LAD") # Lenguaje original de la red
|
network_lang = network.get("language", "LAD")
|
||||||
scl_output.append(
|
output_content.append(
|
||||||
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
|
||||||
)
|
)
|
||||||
if network_comment:
|
if network_comment:
|
||||||
# Indentar comentarios de red
|
|
||||||
for line in network_comment.splitlines():
|
for line in network_comment.splitlines():
|
||||||
scl_output.append(f" // {line}")
|
output_content.append(f" // {line}")
|
||||||
scl_output.append("") # Línea en blanco antes del código de red
|
output_content.append("")
|
||||||
|
|
||||||
network_has_code = False
|
network_has_code = False
|
||||||
logic_in_network = network.get("logic", [])
|
logic_in_network = network.get("logic", [])
|
||||||
|
|
||||||
if not logic_in_network:
|
if not logic_in_network:
|
||||||
scl_output.append(f" // Network {i+1} has no logic elements.")
|
output_content.append(f" // Network {i+1} has no logic elements.")
|
||||||
scl_output.append("")
|
output_content.append("")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- Manejo Especial Redes STL ---
|
|
||||||
if network_lang == "STL":
|
if network_lang == "STL":
|
||||||
# Asumir que la lógica STL está en el primer elemento como RAW_STL_CHUNK
|
|
||||||
if logic_in_network[0].get("type") == "RAW_STL_CHUNK":
|
if logic_in_network[0].get("type") == "RAW_STL_CHUNK":
|
||||||
network_has_code = True
|
network_has_code = True
|
||||||
raw_stl_code = logic_in_network[0].get(
|
raw_stl_code = logic_in_network[0].get(
|
||||||
"stl", "// ERROR: STL code missing"
|
"stl", "// ERROR: STL code missing"
|
||||||
)
|
)
|
||||||
# Incrustar STL como comentario multi-línea o delimitado
|
output_content.append(f" // --- BEGIN STL Network {i+1} ---")
|
||||||
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
|
|
||||||
# Comentar cada línea STL
|
|
||||||
for stl_line in raw_stl_code.splitlines():
|
for stl_line in raw_stl_code.splitlines():
|
||||||
scl_output.append(f" // {stl_line}")
|
output_content.append(f" // {stl_line}")
|
||||||
scl_output.append(f" // --- END STL Network {i+1} ---")
|
output_content.append(f" // --- END STL Network {i+1} ---")
|
||||||
scl_output.append("") # Línea en blanco después
|
output_content.append("")
|
||||||
else:
|
else:
|
||||||
scl_output.append(
|
output_content.append(
|
||||||
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
f" // ERROR: Contenido STL inesperado en Network {i+1}."
|
||||||
)
|
)
|
||||||
scl_output.append("")
|
output_content.append("")
|
||||||
|
else: # SCL/LAD/FBD
|
||||||
# --- Manejo Redes SCL/LAD/FBD procesadas ---
|
|
||||||
else:
|
|
||||||
# Iterar por las instrucciones procesadas
|
|
||||||
for instruction in logic_in_network:
|
for instruction in logic_in_network:
|
||||||
instruction_type = instruction.get("type", "")
|
instruction_type = instruction.get("type", "")
|
||||||
scl_code = instruction.get("scl", "")
|
scl_code = instruction.get("scl", "")
|
||||||
is_grouped = instruction.get("grouped", False)
|
is_grouped = instruction.get("grouped", False)
|
||||||
|
|
||||||
# Saltar instrucciones agrupadas (su lógica está en el IF)
|
|
||||||
if is_grouped:
|
if is_grouped:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Incluir SCL si la instrucción fue procesada o es un chunk crudo/error/placeholder
|
|
||||||
if (
|
if (
|
||||||
instruction_type.endswith(SCL_SUFFIX)
|
instruction_type.endswith(SCL_SUFFIX)
|
||||||
or instruction_type
|
or instruction_type
|
||||||
|
@ -688,18 +722,14 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
"UNSUPPORTED_CONTENT",
|
"UNSUPPORTED_CONTENT",
|
||||||
"PARSING_ERROR",
|
"PARSING_ERROR",
|
||||||
]
|
]
|
||||||
or "_error" in instruction_type # Incluir errores comentados
|
or "_error" in instruction_type
|
||||||
) and scl_code:
|
) and scl_code:
|
||||||
|
|
||||||
# Comprobar si el SCL es solo un comentario (a menos que sea un bloque IF)
|
|
||||||
is_only_comment = all(
|
is_only_comment = all(
|
||||||
line.strip().startswith("//")
|
line.strip().startswith("//")
|
||||||
for line in scl_code.splitlines()
|
for line in scl_code.splitlines()
|
||||||
if line.strip()
|
if line.strip()
|
||||||
)
|
)
|
||||||
is_if_block = scl_code.strip().startswith("IF")
|
is_if_block = scl_code.strip().startswith("IF")
|
||||||
|
|
||||||
# Añadir el SCL indentado si no es solo un comentario (o si es un IF/Error)
|
|
||||||
if (
|
if (
|
||||||
not is_only_comment
|
not is_only_comment
|
||||||
or is_if_block
|
or is_if_block
|
||||||
|
@ -713,78 +743,72 @@ def generate_scl(processed_json_filepath, output_scl_filepath):
|
||||||
):
|
):
|
||||||
network_has_code = True
|
network_has_code = True
|
||||||
for line in scl_code.splitlines():
|
for line in scl_code.splitlines():
|
||||||
scl_output.append(f" {line}") # Indentar código
|
output_content.append(f" {line}")
|
||||||
# Añadir línea en blanco después de cada bloque SCL para legibilidad
|
output_content.append("")
|
||||||
scl_output.append("")
|
if not network_has_code and network_lang != "STL":
|
||||||
|
output_content.append(
|
||||||
# Si la red no produjo código SCL imprimible (ej. solo lógica interna)
|
f" // Network {i+1} did not produce printable SCL/MD code."
|
||||||
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("")
|
output_content.append("")
|
||||||
|
output_content.append(f"END_{scl_block_keyword}")
|
||||||
|
|
||||||
# Fin del bloque FC/FB/OB
|
else: # Tipo desconocido
|
||||||
scl_output.append(f"END_{scl_block_keyword}") # <-- Usar keyword determinada
|
print(
|
||||||
|
f"Error: Tipo de bloque desconocido '{block_type}' encontrado en JSON. No se generará archivo."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# --- Escritura del Archivo SCL (Común) ---
|
# --- Escritura del Archivo de Salida (.scl o .md) ---
|
||||||
print(f"Escribiendo archivo SCL en: {output_scl_filepath}")
|
# 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:
|
try:
|
||||||
with open(output_scl_filepath, "w", encoding="utf-8") as f:
|
# Crear directorio si no existe
|
||||||
for line in scl_output:
|
os.makedirs(output_directory, exist_ok=True)
|
||||||
|
with open(output_filepath, "w", encoding="utf-8") as f:
|
||||||
|
for line in output_content:
|
||||||
f.write(line + "\n")
|
f.write(line + "\n")
|
||||||
print("Generación de SCL completada.")
|
print(f"Generación de {output_extension.upper()} completada.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al escribir el archivo SCL: {e}")
|
print(f"Error al escribir el archivo {output_extension.upper()}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
# --- Ejecución ---
|
# --- Ejecución ---
|
||||||
if __name__ == "__main__":
|
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(
|
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(
|
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).",
|
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):
|
if not os.path.exists(source_xml_file):
|
||||||
print(
|
print(
|
||||||
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON procesado."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Derivar nombres de archivos de entrada (JSON procesado) y salida (SCL)
|
|
||||||
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
|
||||||
# Asumir que los archivos están en el mismo directorio que el XML original
|
base_dir = os.path.dirname(source_xml_file)
|
||||||
base_dir = os.path.dirname(source_xml_file) # Directorio del XML original
|
|
||||||
|
|
||||||
input_json_file = os.path.join(
|
input_json_file = os.path.join(
|
||||||
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
base_dir, f"{xml_filename_base}_simplified_processed.json"
|
||||||
)
|
)
|
||||||
# Cambiar extensión de salida a .scl
|
|
||||||
output_scl_file = os.path.join(
|
# MODIFICADO: El directorio de salida ahora es el mismo que el de entrada
|
||||||
base_dir, f"{xml_filename_base}_generated.scl" # Cambiado nombre de salida
|
output_dir = base_dir # Escribir .scl/.md en el mismo directorio
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
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):
|
if not os.path.exists(input_json_file):
|
||||||
print(
|
print(
|
||||||
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
|
f"Error Fatal (x3): Archivo JSON procesado no encontrado: '{input_json_file}'"
|
||||||
|
@ -792,16 +816,15 @@ if __name__ == "__main__":
|
||||||
print(
|
print(
|
||||||
f"Asegúrate de que 'x2_process.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'."
|
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:
|
else:
|
||||||
# Llamar a la función principal de generación SCL del script
|
|
||||||
try:
|
try:
|
||||||
generate_scl(input_json_file, output_scl_file)
|
# Pasar el directorio de salida a la función principal
|
||||||
sys.exit(0) # Salir con éxito explícitamente
|
generate_scl_or_markdown(input_json_file, output_dir)
|
||||||
|
sys.exit(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error Crítico (x3) durante la generación de SCL desde '{input_json_file}': {e}"
|
f"Error Crítico (x3) durante la generación de SCL/MD desde '{input_json_file}': {e}"
|
||||||
)
|
)
|
||||||
# traceback ya debería estar importado
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1) # Salir con error si la función principal falla
|
sys.exit(1)
|
||||||
|
|
Loading…
Reference in New Issue