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:
parent
c3088e9957
commit
75cdf080f5
|
@ -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}")
|
|
@ -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"
|
||||
```
|
|
@ -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
|
|
@ -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
|
@ -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 ---
|
|
@ -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 ---
|
|
@ -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 ---
|
|
@ -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(
|
||||
middle_child
|
||||
)
|
||||
if idx_result and idx_result.strip():
|
||||
indices_parts.append(idx_result.strip())
|
||||
# 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()
|
||||
)
|
||||
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,8 +534,56 @@ 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:
|
||||
access_str = "/*_ERR_NO_CALLINFO_*/"
|
||||
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_*/"
|
||||
|
||||
# Añadir más scopes si son necesarios (e.g., Address, Label, Reference)
|
||||
|
||||
|
|
|
@ -516,11 +516,18 @@ def parse_interface_members(member_elements):
|
|||
)
|
||||
|
||||
# Procesar miembros anidados (Struct)
|
||||
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)
|
||||
# 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,
|
||||
)
|
||||
if nested_sections:
|
||||
member_info["children"] = parse_interface_members(nested_sections)
|
||||
|
||||
# Procesar valores iniciales de Array
|
||||
if isinstance(member_dtype, str) and member_dtype.lower().startswith("array["):
|
||||
|
|
79417
data/log.txt
79417
data/log.txt
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue