Add XML parser script and example SCL function block for HMI interlock

- Created a new script to convert XML data to SCL format.
- Added a readme file with instructions for generating SCL from XML.
- Introduced an example SCL function block "FB_HMI_Interlock" with logic for managing HMI requests.
This commit is contained in:
Miguel 2025-08-24 10:06:45 +02:00
parent c3088e9957
commit 75cdf080f5
13 changed files with 63531 additions and 40899 deletions

View File

@ -1,56 +0,0 @@
"""
Test script para verificar la función is_block_exportable
"""
# Mock class para simular un bloque de TIA Portal
class MockBlock:
def __init__(self, programming_language):
self.programming_language = programming_language
def get_property(self, name):
if name == "ProgrammingLanguage":
return self.programming_language
raise Exception(f"Property {name} not found")
# Importar la función desde x1.py
import sys
import os
sys.path.append(os.path.dirname(__file__))
from x1 import is_block_exportable
# Testear diferentes tipos de bloques
test_cases = [
("LAD", True, "LAD blocks should be exportable"),
("FBD", True, "FBD blocks should be exportable"),
("STL", True, "STL blocks should be exportable"),
("SCL", True, "SCL blocks should be exportable"),
("ProDiag_OB", False, "ProDiag_OB blocks should not be exportable"),
("ProDiag", False, "ProDiag blocks should not be exportable"),
("GRAPH", False, "GRAPH blocks should not be exportable"),
]
print("=== Test de validación de bloques ===")
for prog_lang, expected_exportable, description in test_cases:
block = MockBlock(prog_lang)
is_exportable, detected_lang, reason = is_block_exportable(block)
status = "✓ PASS" if is_exportable == expected_exportable else "✗ FAIL"
print(f"{status} - {description}")
print(f" Lenguaje: {detected_lang}, Exportable: {is_exportable}, Razón: {reason}")
print()
# Test con bloque que genera excepción
class MockBlockError:
def get_property(self, name):
raise Exception("Cannot access property")
print("=== Test de manejo de errores ===")
error_block = MockBlockError()
is_exportable, detected_lang, reason = is_block_exportable(error_block)
print(f"Bloque con error - Exportable: {is_exportable}, Lenguaje: {detected_lang}")
print(f"Razón: {reason}")

View File

@ -0,0 +1,6 @@
Para generar un resultado directo desde un xml a 1 scl
```bash
C:/Users/migue/miniconda3/envs/tia_scripting/python.exe "d:/Proyectos/Scripts/ParamManagerScripts/backend/script_groups/XML Parser to SCL/x0_main.py" --plc-dir "D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia" --source-xml "D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia\PLC_TL27_Q1\ProgramBlocks_XML\FB HMI Interlock.xml" --dest-scl "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\XML Parser to SCL\.example\FB_HMI_Interlock.scl"
```

View File

@ -0,0 +1,80 @@
// FB10
// Block Type: FB
// Block Name (Original): FB HMI Interlock
// Block Number: 10
// Original Network Languages: SCL
FUNCTION_BLOCK "FB_HMI_Interlock"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_STAT
Status : STRUCT
HMI_InUse : Bool;
HMI_CanBeUsedBy : Int;
HMI_Max_Count : Int;
i_Request : Array[0..7] of Bool;
o_HMI_CanBeUsedBy : Array[0..7] of Bool;
END_STRUCT;
AUX : STRUCT
Cycle_Count : Int;
Time_OUT : STRUCT
PT : Time;
ET : Time;
IN : Bool;
Q : Bool;
END_STRUCT;
END_STRUCT;
END_VAR
VAR_TEMP
i : Int;
END_VAR
CONSTANT
HMI_REQUEST_TRANSPORT : Int := 1;
HMI_REQUEST_ELECTRIC_GUIDES : Int := 2;
END_CONSTANT
#_5s : Bool; // Auto-generated temporary
#AUX : Bool; // Auto-generated temporary
#Status : Bool; // Auto-generated temporary
#i : Bool; // Auto-generated temporary
BEGIN
// Network 1: Number of logics that can use the HMI at the same time (Original Language: SCL)
#Status.HMI_Max_Count := 2; // Only Transport & Electric Guides
// Timeout after the HMI is not more requested by the area
#AUX.Time_OUT(IN :=NOT (#Status.i_Request[#Status.HMI_CanBeUsedBy]), PT :=T#5s);
IF #AUX.Time_OUT.Q THEN
// Cancel the actual use after timeout
#Status.HMI_InUse := FALSE;
FOR #i:=0 TO 7 DO
#Status.o_HMI_CanBeUsedBy[] := FALSE;
END_FOR;
END_IF;
IF NOT #Status.HMI_InUse THEN
#AUX.Cycle_Count := #AUX.Cycle_Count + "DINT_TO_INT"("DB ScanTime_OB1".SCAN_TIME_ms);
IF #AUX.Cycle_Count> 1000 THEN
#Status.HMI_CanBeUsedBy := (#Status.HMI_CanBeUsedBy MOD #Status.HMI_Max_Count) + 1;
#AUX.Cycle_Count := #AUX.Cycle_Count - 1000;
END_IF;
IF #Status.HMI_CanBeUsedBy > 0 AND #Status.i_Request[#Status.HMI_CanBeUsedBy] THEN
// Return that the actual request is active and can use the HMI
#Status.o_HMI_CanBeUsedBy[] := TRUE;
#Status.HMI_InUse := TRUE;
END_IF;
END_IF;
// Every cycle we reset all request. When some area need the HMI need to
// request on every cycle until is granted.
//
FOR #i:=0 TO 7 DO
#Status.i_Request[] := FALSE;
END_FOR;
#Status.o_HMI_CanBeUsedBy[0] := NOT #Status.HMI_InUse;
END_FUNCTION_BLOCK

View File

@ -5,7 +5,7 @@
"path": "."
},
{
"path": "../../../../../../Trabajo/VM/45 - HENKEL - VM Auto Changeover/ExportTia/PLC_TL25_Q1"
"path": "../../../../../../Trabajo/VM/45 - HENKEL - VM Auto Changeover/ExportTia"
}
],
"settings": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
--- Log de Ejecución: x1_to_json.py ---
Grupo: XML Parser to SCL
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-03 20:08:18
Fin: 2025-05-03 20:08:22
Duración: 0:00:03.850097
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
Por favor, selecciona el archivo XML de entrada...
--- ERRORES (STDERR) ---
No se seleccionó ningún archivo. Saliendo.
--- FIN DEL LOG ---

View File

@ -1,34 +0,0 @@
--- Log de Ejecución: x4_cross_reference.py ---
Grupo: XML Parser to SCL
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-05 16:34:28
Fin: 2025-05-05 16:34:30
Duración: 0:00:01.642768
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
(x4 - Standalone) Ejecutando generación de referencias cruzadas...
--- Iniciando Generación de Referencias Cruzadas y Fuentes MD (x4) ---
Buscando archivos JSON procesados en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC
Directorio de salida XRef: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\xref_output
Directorio fuente SCL/MD (para análisis DB/Tag y copia): scl_output
Subdirectorio fuentes MD para XRef: source
Copiando y preparando archivos fuente para Obsidian en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\xref_output\source
Archivos fuente preparados: 378 SCL convertidos, 30 MD copiados.
Buscando archivos XML XRef en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\ProgramBlocks_CR
Archivos JSON encontrados: 342
Datos cargados para 342 bloques.
Mapa InstanciaDB -> FB creado con 0 entradas.
Datos cargados para 342 bloques (1793 PLC Tags globales).
Construyendo grafo de llamadas desde archivos XML XRef...
Archivos XML XRef encontrados: 138
Generando ÁRBOL XRef de llamadas en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\xref_output\xref_calls_tree.md
Generando RESUMEN XRef de uso de DBs en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\xref_output\xref_db_usage_summary.md
Generando RESUMEN XRef de uso de PLC Tags en: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\xref_output\xref_plc_tags_summary.md
--- Generación de Referencias Cruzadas y Fuentes MD (x4) Completada ---
(x4 - Standalone) Proceso completado exitosamente.
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -1,34 +0,0 @@
--- Log de Ejecución: x7_clear.py ---
Grupo: XML Parser to SCL
Directorio de Trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\ExportTia
Inicio: 2025-06-20 18:53:46
Fin: 2025-06-20 18:53:47
Duración: 0:00:01.131243
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
INFO: format_variable_name importado desde generators.generator_utils
=== Limpiando PLC: PLC ===
- Eliminado directorio de parsing: PLC\PlcDataTypes\parsing
- Eliminado directorio de parsing: PLC\PlcDataTypes_CR\parsing
- Eliminado directorio de parsing: PLC\PlcTags\parsing
- Eliminado directorio de parsing: PLC\PlcTags\IO Not in Hardware\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\SystemBlocks_CR\parsing
- Eliminado directorio 'scl_output': PLC\scl_output
- Eliminado directorio 'xref_output': PLC\xref_output
- Eliminado archivo agregado: PLC\full_project_representation.md
- Eliminado log: log_PLC.txt
--- Resumen de limpieza ---
Directorios eliminados: 11
Archivos eliminados: 2
Limpieza completada.
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -124,9 +124,15 @@ def reconstruct_scl_from_tokens(st_node):
has_quotes = has_quotes_comp or has_quotes_access
is_temp = name.startswith("#")
# Para variables locales, usar prefijo # en lugar de comillas
if scope == "LocalVariable" and i == 0 and not is_temp:
symbol_text_parts.append(f"#{name}")
# 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
elif has_quotes or (
i == 0
and not is_temp
and '"' not in name
and scope != "LocalVariable"
): # Avoid double quotes
symbol_text_parts.append(f'"{name}"')
else:
@ -198,12 +204,72 @@ def reconstruct_scl_from_tokens(st_node):
val_nodes[0].text.strip()
)
else:
# Para otros tipos de acceso, usar la función recursiva
idx_result = reconstruct_scl_from_tokens(
# Para otros tipos de acceso, procesar manualmente en lugar de recursión
if (
middle_child.get("Scope")
== "LocalVariable"
):
# Procesar LocalVariable manualmente
symbol_elem = middle_child.xpath(
"./st:Symbol", namespaces=ns
)
if not symbol_elem:
symbol_elem = middle_child.xpath(
"./Symbol"
)
if symbol_elem:
components = symbol_elem[0].xpath(
"./st:Component", namespaces=ns
)
if not components:
components = symbol_elem[
0
].xpath("./Component")
# Construir la variable manualmente
var_parts = []
for i, comp in enumerate(
components
):
name = comp.get(
"Name", "_ERR_COMP_"
)
if i == 0:
var_parts.append(
f"#{name}"
) # Primer componente con #
else:
var_parts.append(
f".{name}"
) # Componentes subsecuentes con .
idx_result = "".join(var_parts)
if idx_result:
indices_parts.append(idx_result)
else:
indices_parts.append(
"/*_ERR_EMPTY_VAR_*/"
)
else:
indices_parts.append(
"/*_ERR_NO_SYMBOL_*/"
)
else:
# Para otros scopes, usar recursión como fallback
idx_result = (
reconstruct_scl_from_tokens(
middle_child
)
)
if idx_result and idx_result.strip():
indices_parts.append(idx_result.strip())
indices_parts.append(
idx_result.strip()
)
else:
indices_parts.append(
"/*_ERR_RECURSIVE_EMPTY_*/"
)
elif child_tag == "Token":
# Token de separación (como ",")
token_text = middle_child.get("Text", "")
@ -259,6 +325,28 @@ def reconstruct_scl_from_tokens(st_node):
else:
access_str = f"/*_ERR_NO_SYMBOL_IN_{scope}_*/"
# --- Constantes Tipadas (TypedConstant) ---
elif scope == "TypedConstant":
constant_elem = elem.xpath("./st:Constant", namespaces=ns)
if not constant_elem:
constant_elem = elem.xpath("./Constant")
if constant_elem:
const_value_elem = constant_elem[0].xpath(
"./st:ConstantValue", namespaces=ns
)
if not const_value_elem:
const_value_elem = constant_elem[0].xpath("./ConstantValue")
if const_value_elem and const_value_elem[0].text:
const_val = const_value_elem[0].text.strip()
# Para constantes tipadas, usar el valor directamente (ya incluye el tipo como T#5s)
access_str = const_val
else:
access_str = "/*_ERR_NO_CONST_VALUE_*/"
else:
access_str = "/*_ERR_NO_CONST_ELEM_*/"
# --- Constantes Literales ---
elif scope == "LiteralConstant":
# Buscar nodos Constant tanto con namespace st: como sin namespace
@ -406,6 +494,7 @@ def reconstruct_scl_from_tokens(st_node):
# --- Llamadas a Funciones/Bloques (Scope=Call) ---
elif scope == "Call":
# Primero intentar con CallInfo (estructura tradicional)
call_info_node = elem.xpath("./st:CallInfo", namespaces=ns)
if call_info_node:
ci = call_info_node[0]
@ -445,6 +534,54 @@ def reconstruct_scl_from_tokens(st_node):
access_str = f'"{call_name}"({", ".join(param_parts)})'
else: # Otros tipos de llamada
access_str = f'"{call_name}"({", ".join(param_parts)}) (* Tipo: {call_type} *)'
# Si no hay CallInfo, intentar con Instruction (estructura de SCL nativo)
else:
instruction_node = elem.xpath("./st:Instruction", namespaces=ns)
if instruction_node:
instr = instruction_node[0]
instr_name = instr.get(
"Name"
) # Puede ser None para llamadas sin nombre específico
# Parámetros con nombre y sin nombre
named_params = instr.xpath("./st:Parameter", namespaces=ns)
nameless_params = instr.xpath(
"./st:NamelessParameter", namespaces=ns
)
param_parts = []
# Procesar parámetros con nombre
for p in named_params:
p_name = p.get("Name", "_ERR_PARAMNAME_")
# Reconstruir el valor del parámetro
p_value_scl = reconstruct_scl_from_tokens(p)
p_value_scl = p_value_scl.replace("\n", "").strip()
# Si el valor ya contiene ":=", no lo duplicar
if p_value_scl.startswith(":="):
param_parts.append(f"{p_name} {p_value_scl}")
elif p_value_scl:
param_parts.append(f"{p_name} := {p_value_scl}")
else:
param_parts.append(f"{p_name} := /*_ERR_PARAM_VALUE_*/")
# Procesar parámetros sin nombre
for p in nameless_params:
p_value_scl = reconstruct_scl_from_tokens(p)
p_value_scl = p_value_scl.replace("\n", "").strip()
if p_value_scl:
param_parts.append(p_value_scl)
else:
param_parts.append("/*_ERR_NAMELESS_PARAM_*/")
# Construir la llamada
if instr_name:
access_str = f'"{instr_name}"({", ".join(param_parts)})'
else:
# Llamada sin nombre específico, probablemente un FB instance call
access_str = f'({", ".join(param_parts)})'
else:
access_str = "/*_ERR_NO_CALLINFO_*/"

View File

@ -516,8 +516,15 @@ def parse_interface_members(member_elements):
)
# Procesar miembros anidados (Struct)
# Primero buscar miembros directos (estructura común)
direct_nested_members = member.xpath("./iface:Member", namespaces=ns)
if direct_nested_members:
member_info["children"] = parse_interface_members(direct_nested_members)
else:
# Si no hay miembros directos, buscar en Sections (para tipos como TON_TIME)
nested_sections = member.xpath(
"./iface:Sections/iface:Section[@Name='None']/iface:Member", namespaces=ns
"./iface:Sections/iface:Section[@Name='None']/iface:Member",
namespaces=ns,
)
if nested_sections:
member_info["children"] = parse_interface_members(nested_sections)

79417
data/log.txt

File diff suppressed because it is too large Load Diff