Compare commits

...

6 Commits

166 changed files with 10422 additions and 375 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,6 +10,11 @@ from pathlib import Path
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
import difflib
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# Forzar UTF-8 en la salida estándar
sys.stdout.reconfigure(encoding="utf-8")
@ -257,7 +262,8 @@ class CSharpCodeMerger:
return ''.join(diff)
def main():
configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
# configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
configs = load_configuration()
working_directory = configs.get("working_directory", ".")
work_config = configs.get("level3", {})

View File

@ -9,6 +9,11 @@ from utils.email_parser import procesar_eml
from utils.markdown_handler import cargar_cronologia_existente
from utils.beautify import BeautifyProcessor
import json
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# Forzar UTF-8 en la salida estándar
sys.stdout.reconfigure(encoding="utf-8")
@ -29,7 +34,8 @@ def generar_indice(mensajes):
def main():
# Cargar configuraciones del entorno
configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
# configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
configs = load_configuration()
# Obtener working directory
working_directory = configs.get("working_directory", ".")

View File

@ -0,0 +1,12 @@
# HTML to Markdown Conversion Tool
This script processes HTML files and converts them to Markdown format, extracting images and preserving the document structure.
## Dependencies
The script requires the following Python libraries:
- beautifulsoup4
- requests
- html2text
Install dependencies using:

View File

@ -0,0 +1,4 @@
{
"attachments_dir": "adjuntos",
"output_file": "contenidoImportado.md"
}

View File

@ -0,0 +1,6 @@
{
"name": "Importación de Archivos HTML",
"description": "Este script procesa archivos HTML en un directorio y los convierte en un único archivo Markdown, extrayendo las imágenes a una carpeta de adjuntos y manteniendo los enlaces. También genera un índice al principio del archivo.",
"version": "1.0",
"author": "Miguel"
}

View File

@ -0,0 +1,15 @@
{
"type": "object",
"properties": {
"attachments_dir": {
"type": "string",
"title": "Directorio de adjuntos",
"description": "adjuntos"
},
"output_file": {
"type": "string",
"title": "Nombre del archivo de salida",
"description": "contenido.md"
}
}
}

View File

@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"output_directory": {
"type": "string",
"format": "directory",
"title": "Directorio donde escribir el archivo de salida",
"description": "Lugar para el archivo de salida markdown"
}
}
}

View File

@ -0,0 +1,74 @@
# models/pagina_html.py
import os
import re
import hashlib
from pathlib import Path
class PaginaHTML:
def __init__(self, ruta_archivo, titulo=None, contenido=None, imagenes=None):
self.ruta_archivo = ruta_archivo
self.nombre_archivo = Path(ruta_archivo).name
self.titulo = titulo or self._extraer_titulo_de_ruta()
self.contenido = contenido or ""
self.hash = self._generar_hash()
self.imagenes = imagenes or [] # Lista de imágenes asociadas a esta página
def _extraer_titulo_de_ruta(self):
"""Extrae el título a partir del nombre del archivo."""
nombre = Path(self.ruta_archivo).stem
# Limpia el nombre: reemplaza guiones y underscores por espacios
nombre = re.sub(r"[_-]", " ", nombre)
# Capitaliza cada palabra
nombre = " ".join(word.capitalize() for word in nombre.split())
return nombre
def _generar_hash(self):
"""Genera un hash único para la página."""
elementos_hash = [
self.nombre_archivo,
self.titulo,
(
self.contenido[:500] if self.contenido else ""
), # Usar solo los primeros 500 caracteres
]
texto_hash = "|".join(elementos_hash)
return hashlib.md5(texto_hash.encode()).hexdigest()
def to_markdown(self):
"""Convierte la página a formato Markdown."""
# Hash como comentario al principio
hash_line = f"<!-- {self.hash} -->\n\n"
# Título como encabezado
titulo_line = f"## {self.titulo}\n\n"
# Nombre del archivo original como referencia
origen_line = f"*Origen: {self.nombre_archivo}*\n\n"
# Contenido
md = f"{hash_line}{titulo_line}{origen_line}"
# Limpiar cualquier artefacto de asteriscos extra antes de agregar el contenido
contenido_limpio = re.sub(r"(\*{3,}|\-{3,}|_{3,})", "---", self.contenido)
md += contenido_limpio + "\n\n"
# Ya no se agregan imágenes al final - se asume que están en el contenido
md += "---\n\n"
return md
def get_index_entry(self):
"""Genera una entrada para el índice."""
# Formatear el título para el enlace de Markdown
slug = re.sub(r"[^\w\s-]", "", self.titulo.lower())
slug = re.sub(r"\s+", "-", slug)
return f"- [{self.titulo}](#{slug})"
def add_imagen(self, imagen):
"""Añade una referencia a una imagen asociada a esta página."""
if imagen not in self.imagenes:
self.imagenes.append(imagen)

View File

@ -0,0 +1,5 @@
beautifulsoup4==4.12.2
html2text==2020.1.16
requests==2.31.0
pypandoc==1.11
mammoth==1.6.0

View File

@ -0,0 +1,150 @@
# utils/docx_converter.py
import os
import re
import mammoth
from pathlib import Path
from models.pagina_html import PaginaHTML
def procesar_docx(ruta_archivo, dir_adjuntos):
"""
Procesa un archivo DOCX y lo convierte directamente a Markdown,
extrayendo las imágenes al directorio de adjuntos.
"""
# Asegurar que el directorio de adjuntos existe
os.makedirs(dir_adjuntos, exist_ok=True)
# Lista para almacenar imágenes procesadas
imagenes_procesadas = []
def manejar_imagen(image):
"""Procesa cada imagen encontrada en el documento DOCX."""
try:
# Generar nombre de archivo para la imagen
extension = image.content_type.split('/')[-1] if hasattr(image, 'content_type') else 'png'
if extension == 'jpeg':
extension = 'jpg'
# Usar alt_text si está disponible o generar nombre basado en índice
filename = (image.alt_text if hasattr(image, 'alt_text') and image.alt_text
else f"image-{len(imagenes_procesadas)+1}.{extension}")
# Asegurar que el nombre sea válido para el sistema de archivos
filename = re.sub(r'[<>:"/\\|?*]', "_", filename)
if not filename.endswith(f".{extension}"):
filename = f"{filename}.{extension}"
# Ruta completa para guardar la imagen
image_path = os.path.join(dir_adjuntos, filename)
# Verificar si el objeto imagen tiene el atributo 'content'
if hasattr(image, 'content') and image.content:
# Guardar la imagen
with open(image_path, 'wb') as f:
f.write(image.content)
# Agregar a la lista de imágenes procesadas
if filename not in imagenes_procesadas:
imagenes_procesadas.append(filename)
# Retornar el formato para Obsidian
return {"src": filename}
else:
# Si no hay contenido, registrar el problema
print(f"Advertencia: No se pudo extraer contenido de imagen '{filename}'")
# Retornar un marcador de posición
return {"src": "imagen_no_disponible.png"}
except Exception as e:
print(f"Error procesando imagen en DOCX: {str(e)}")
# En caso de error, retornar un marcador de texto
return {"alt": "Imagen no disponible"}
try:
# Configurar opciones de conversión personalizada para manejar casos problemáticos
options = {
"ignore_empty_paragraphs": True,
"style_map": "p[style-name='Heading 1'] => h1:fresh"
}
# Abrir el archivo DOCX
with open(ruta_archivo, "rb") as docx_file:
# Convertir con manejo de errores mejorado
result = mammoth.convert_to_markdown(
docx_file,
convert_image=mammoth.images.img_element(manejar_imagen),
options=options
)
# Extraer el título (primera línea como encabezado)
markdown_content = result.value
lines = markdown_content.strip().split('\n')
titulo = None
# Buscar el primer encabezado para usar como título
for line in lines:
if line.startswith('#'):
# Eliminar los símbolos # y espacios
titulo = re.sub(r'^#+\s*', '', line).strip()
break
# Si no hay encabezado, usar el nombre del archivo
if not titulo:
titulo = Path(ruta_archivo).stem
# Mostrar advertencias de la conversión
warnings = result.messages
if warnings:
print(f"Advertencias en la conversión:")
for warning in warnings:
print(f"- {warning}")
# Post-procesar para formato Obsidian
markdown_content = post_procesar_markdown_obsidian(markdown_content, imagenes_procesadas)
# Crear objeto PaginaHTML
return PaginaHTML(
ruta_archivo=ruta_archivo,
titulo=titulo,
contenido=markdown_content,
imagenes=imagenes_procesadas
)
except Exception as e:
print(f"Error procesando DOCX {ruta_archivo}: {str(e)}")
return PaginaHTML(
ruta_archivo=ruta_archivo,
contenido=f"Error al procesar: {str(e)}"
)
def post_procesar_markdown_obsidian(markdown, imagenes):
"""
Realiza ajustes adicionales al markdown para formato Obsidian.
"""
# 1. Convertir referencias de imágenes al formato Obsidian
for imagen in imagenes:
# Buscar referencias de imágenes en formato estándar
# y convertirlas al formato Obsidian
markdown = re.sub(
r'!\[(.*?)\]\(' + re.escape(imagen) + r'\)',
f'![[{imagen}]]',
markdown
)
# 2. Agregar salto de línea adicional después de cada encabezado
markdown = re.sub(r'(^|\n)(#+\s.*?)(\n(?!\n))', r'\1\2\n\3', markdown)
# 3. Arreglar listas mal formadas (asegurar espacio después de * o -)
markdown = re.sub(r'(^|\n)([*\-])([^\s])', r'\1\2 \3', markdown)
# 4. Mejorar formato de tablas
# Asegurar que hay línea en blanco antes y después de tablas
markdown = re.sub(r'([^\n])\n(\|[^\n]+\|)', r'\1\n\n\2', markdown)
markdown = re.sub(r'(\|[^\n]+\|)\n([^\n\|])', r'\1\n\n\2', markdown)
# 5. Normalizar fin de líneas
markdown = markdown.replace('\r\n', '\n')
# 6. Eliminar líneas en blanco consecutivas excesivas
markdown = re.sub(r'\n{3,}', '\n\n', markdown)
return markdown

View File

@ -0,0 +1,391 @@
# utils/html_parser.py
import os
import re
import hashlib
from pathlib import Path
from bs4 import BeautifulSoup
from urllib.parse import urlparse, unquote
import requests
import shutil
from models.pagina_html import PaginaHTML
# Importar pypandoc para la conversión mejorada
try:
import pypandoc
PANDOC_AVAILABLE = True
except ImportError:
print("⚠️ Advertencia: pypandoc no está instalado. Se utilizará html2text como fallback.")
import html2text
PANDOC_AVAILABLE = False
def procesar_html(ruta_archivo, dir_adjuntos):
"""
Procesa un archivo HTML y lo convierte a Markdown, descargando
las imágenes a la carpeta de adjuntos.
Esta función mantiene la misma firma que la versión original
para garantizar compatibilidad con el flujo existente.
"""
# Lista de encodings para intentar
encodings = ["utf-8", "latin-1", "iso-8859-1", "windows-1252", "cp1252"]
# Lista para almacenar las imágenes procesadas
imagenes_procesadas = []
for encoding in encodings:
try:
with open(ruta_archivo, "r", encoding=encoding) as f:
contenido = f.read()
soup = BeautifulSoup(contenido, "html.parser")
# Extraer título
titulo = None
if soup.title:
titulo = soup.title.string
elif soup.find("h1"):
titulo = soup.find("h1").get_text()
# Procesar imágenes: descargarlas y actualizar rutas
# Obtener la lista de imágenes procesadas
imagenes_procesadas = procesar_imagenes(soup, dir_adjuntos, ruta_archivo)
# Preprocesar elementos específicos de Word
preprocesar_elementos_word(soup)
# Convertir HTML a Markdown
if PANDOC_AVAILABLE:
# Usar Pandoc para una mejor conversión (especialmente tablas)
markdown = html_a_markdown_pandoc(str(soup), dir_adjuntos)
else:
# Fallback a html2text si Pandoc no está disponible
markdown = html_a_markdown_html2text(soup)
# Post-procesar el Markdown para formato Obsidian
markdown = post_procesar_markdown(markdown, imagenes_procesadas)
# Crear la página HTML con la lista de imágenes
return PaginaHTML(
ruta_archivo=ruta_archivo,
titulo=titulo,
contenido=markdown,
imagenes=imagenes_procesadas
)
except UnicodeDecodeError:
# Si falla la codificación, probar con la siguiente
continue
except Exception as e:
print(
f"Error procesando archivo HTML {ruta_archivo} con encoding {encoding}: {str(e)}"
)
# Continuar con el siguiente encoding si es un error de codificación
if isinstance(e, UnicodeError):
continue
# Para otros errores, devolver página con error
return PaginaHTML(
ruta_archivo=ruta_archivo, contenido=f"Error al procesar: {str(e)}"
)
# Si todos los encodings fallaron
return PaginaHTML(
ruta_archivo=ruta_archivo,
contenido=f"Error al procesar: No se pudo decodificar el archivo con ninguna codificación compatible",
)
def preprocesar_elementos_word(soup):
"""
Preprocesa elementos específicos de Word para mejorar la conversión.
"""
# Eliminar metadatos y scripts
for tag in soup.select('style, script, meta, link'):
tag.decompose()
# Intentar limpiar elementos Office específicos
for tag in soup.find_all(['o:p']):
if tag.string:
# Preservar solo el texto
tag.replace_with(soup.new_string(tag.get_text()))
else:
tag.decompose()
# Convertir elementos VML a elementos HTML estándar
for shape in soup.find_all(['v:shape', 'v:imagedata']):
# Extraer cualquier imagen dentro del shape
img_tags = shape.find_all('img')
if img_tags:
for img in img_tags:
# Mover la imagen fuera del shape
if shape.parent:
shape.parent.insert_before(img)
# Extraer cualquier texto significativo
text_content = shape.get_text().strip()
if text_content and shape.parent:
text_node = soup.new_tag('p')
text_node.string = text_content
shape.parent.insert_before(text_node)
def html_a_markdown_pandoc(html_content, dir_adjuntos):
"""
Convierte HTML a Markdown usando Pandoc.
Pandoc maneja mucho mejor tablas, listas y otros elementos complejos.
"""
try:
# Guardar HTML a un archivo temporal
temp_html_path = os.path.join(dir_adjuntos, "temp_conversion.html")
with open(temp_html_path, "w", encoding="utf-8") as f:
f.write(html_content)
# Convertir usando pypandoc (interfaz para Pandoc)
# Opciones de Pandoc para mejor manejo de tablas
extra_args = [
'--wrap=none', # No wrap lines
'--extract-media=' + dir_adjuntos, # Extract media to the specified directory
'--standalone' # Process as a standalone document
]
# Usar GitHub Flavored Markdown para mejor soporte de tablas
markdown = pypandoc.convert_file(
temp_html_path,
'gfm', # GitHub Flavored Markdown
extra_args=extra_args
)
# Limpiar archivo temporal
if os.path.exists(temp_html_path):
os.remove(temp_html_path)
return markdown
except Exception as e:
print(f"Error en conversión con Pandoc: {str(e)}")
# Fallback al método antiguo si Pandoc falla
if os.path.exists(temp_html_path):
with open(temp_html_path, "r", encoding="utf-8") as f:
soup = BeautifulSoup(f.read(), "html.parser")
os.remove(temp_html_path)
return html_a_markdown_html2text(soup)
else:
return f"Error en conversión: {str(e)}"
def html_a_markdown_html2text(soup):
"""
Método de fallback: convierte HTML a texto Markdown utilizando html2text.
"""
# Configurar el conversor html2text
h2t = html2text.HTML2Text()
h2t.body_width = 0 # No limitar el ancho del cuerpo del texto
h2t.ignore_links = False
h2t.ignore_images = False
h2t.ignore_emphasis = False
h2t.ignore_tables = False
h2t.bypass_tables = False
h2t.mark_code = True
h2t.unicode_snob = True
# Procesar tablas para prepararlas para conversión
for table in soup.find_all("table"):
# Agregar clase para mejor reconocimiento
table["class"] = table.get("class", []) + ["md-table"]
# Verificar si la tabla tiene encabezados
rows = table.find_all("tr")
if rows and not table.find("th"):
# Convertir primera fila a encabezados si no hay th
first_row_cells = rows[0].find_all("td")
for cell in first_row_cells:
new_th = soup.new_tag("th")
new_th.string = cell.get_text().strip()
cell.replace_with(new_th)
# Convertir a Markdown
html_content = str(soup)
markdown_content = h2t.handle(html_content)
return markdown_content
def post_procesar_markdown(markdown, imagenes_procesadas):
"""
Post-procesa el Markdown para adaptarlo a formato Obsidian y
corregir problemas comunes de conversión.
"""
# 1. Convertir referencias de imágenes al formato Obsidian
for imagen in imagenes_procesadas:
# Buscar referencias de imágenes en formato estándar
# y convertirlas al formato Obsidian
markdown = re.sub(
r'!\[(.*?)\]\((.*?' + re.escape(imagen) + r')\)',
f'![[{imagen}]]',
markdown
)
# 2. Limpiar líneas excesivas
markdown = re.sub(r'\n{3,}', '\n\n', markdown)
# 3. Arreglar listas mal formadas
markdown = re.sub(r'(^|\n)([*\-])([^\s])', r'\1\2 \3', markdown, flags=re.MULTILINE)
# 4. Mejorar formato de tablas
# Asegurar que hay línea en blanco antes y después de tablas
markdown = re.sub(r'([^\n])\n(\|[^\n]+\|)', r'\1\n\n\2', markdown)
markdown = re.sub(r'(\|[^\n]+\|)\n([^\n\|])', r'\1\n\n\2', markdown)
# 5. Eliminar caracteres de control
markdown = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', markdown)
# 6. Normalizar líneas horizontales
markdown = re.sub(r'(\*{3,}|\-{3,}|_{3,})', '---', markdown)
return markdown
def procesar_imagenes(soup, dir_adjuntos, ruta_archivo_html):
"""
Procesa todas las imágenes en el HTML, descargándolas al directorio
de adjuntos y actualizando las rutas.
Versión mejorada con manejo simplificado de rutas y mejor
detección de imágenes en documentos Word.
"""
# Crear directorio si no existe
os.makedirs(dir_adjuntos, exist_ok=True)
# Directorio base del archivo HTML
dir_base = os.path.dirname(os.path.abspath(ruta_archivo_html))
# Lista para almacenar imágenes procesadas
imagenes_procesadas = []
# Estadísticas
stats = {
"procesadas": 0,
"no_encontradas": 0,
"con_error": 0
}
# Función auxiliar para procesar una imagen
def procesar_imagen(src, img_tag=None):
if not src or src.startswith("data:"):
return None
try:
print(f"Procesando imagen: {src}")
# Determinar si es URL o ruta local
if src.startswith(("http://", "https://")):
# Es una URL remota
nombre_archivo = os.path.basename(urlparse(src).path)
nombre_archivo = unquote(nombre_archivo)
# Limpiar nombre de archivo
nombre_archivo = re.sub(r'[<>:"/\\|?*]', "_", nombre_archivo)
if not nombre_archivo or nombre_archivo == "_":
nombre_archivo = f"image_{hashlib.md5(src.encode()).hexdigest()[:8]}.jpg"
# Descargar imagen
ruta_img = os.path.join(dir_adjuntos, nombre_archivo)
try:
response = requests.get(src, stream=True, timeout=10)
if response.status_code == 200:
with open(ruta_img, "wb") as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
# Actualizar src en el tag HTML
if img_tag:
img_tag["src"] = nombre_archivo
return nombre_archivo
except Exception as e:
print(f"Error descargando imagen {src}: {str(e)}")
stats["con_error"] += 1
return None
else:
# Es una ruta local - intentar múltiples ubicaciones posibles
src_decoded = unquote(src)
posibles_rutas = [
os.path.join(dir_base, src_decoded), # Ruta completa
os.path.join(dir_base, os.path.basename(src_decoded)), # Solo el nombre
# Rutas comunes para archivos Word
os.path.join(
dir_base,
os.path.splitext(os.path.basename(ruta_archivo_html))[0] + "_archivos",
os.path.basename(src_decoded)
),
os.path.join(
dir_base,
os.path.splitext(os.path.basename(ruta_archivo_html))[0] + "_files",
os.path.basename(src_decoded)
),
os.path.join(dir_base, "image", os.path.basename(src_decoded))
]
# Probar cada ruta posible
for ruta in posibles_rutas:
if os.path.exists(ruta):
# Copiar imagen a adjuntos
nombre_archivo = os.path.basename(ruta)
nombre_archivo = re.sub(r'[<>:"/\\|?*]', "_", nombre_archivo)
ruta_img_destino = os.path.join(dir_adjuntos, nombre_archivo)
shutil.copy2(ruta, ruta_img_destino)
# Actualizar src en el tag HTML
if img_tag:
img_tag["src"] = nombre_archivo
return nombre_archivo
print(f"Imagen no encontrada en ninguna ruta: {src}")
stats["no_encontradas"] += 1
return None
except Exception as e:
print(f"Error procesando imagen {src}: {str(e)}")
stats["con_error"] += 1
return None
# 1. Procesar imágenes normales
for img in soup.find_all("img"):
src = img.get("src")
nombre_archivo = procesar_imagen(src, img)
if nombre_archivo and nombre_archivo not in imagenes_procesadas:
imagenes_procesadas.append(nombre_archivo)
stats["procesadas"] += 1
# 2. Procesar imágenes VML (Office)
for img_data in soup.find_all("v:imagedata"):
src = img_data.get("src")
nombre_archivo = procesar_imagen(src)
if nombre_archivo and nombre_archivo not in imagenes_procesadas:
imagenes_procesadas.append(nombre_archivo)
stats["procesadas"] += 1
# Crear un tag img estándar para asegurar que aparezca en Markdown
new_img = soup.new_tag("img", src=nombre_archivo)
parent = img_data.find_parent()
if parent:
parent.insert_after(new_img)
# 3. Procesar referencias de imágenes en estilos
style_tags = soup.find_all(["style", lambda tag: tag.has_attr("style")])
for tag in style_tags:
style_content = tag.get("style", "") if tag.name != "style" else tag.string or ""
img_refs = re.findall(r'url\(["\']?([^"\']+)["\']?\)', style_content)
for img_ref in img_refs:
nombre_archivo = procesar_imagen(img_ref)
if nombre_archivo and nombre_archivo not in imagenes_procesadas:
imagenes_procesadas.append(nombre_archivo)
stats["procesadas"] += 1
# Mostrar resumen
print(f"\nResumen de procesamiento de imágenes:")
print(f"- Imágenes procesadas con éxito: {stats['procesadas']}")
print(f"- Imágenes no encontradas: {stats['no_encontradas']}")
print(f"- Imágenes con error de procesamiento: {stats['con_error']}")
return imagenes_procesadas

View File

@ -0,0 +1,36 @@
# utils/markdown_handler.py
import os
import re
from models.pagina_html import PaginaHTML
def generar_indice(paginas):
"""
Genera un índice para las páginas HTML.
"""
indice = "# Índice de Contenido\n\n"
for pagina in paginas:
indice += pagina.get_index_entry() + "\n"
indice += "\n---\n\n"
return indice
def escribir_archivo_markdown(paginas, ruta_archivo):
"""
Escribe el archivo Markdown con el índice y todas las páginas.
"""
try:
with open(ruta_archivo, "w", encoding="utf-8") as f:
# Escribir el índice
f.write(generar_indice(paginas))
# Escribir el contenido de cada página
for pagina in paginas:
f.write(pagina.to_markdown())
return True
except Exception as e:
print(f"Error escribiendo archivo Markdown {ruta_archivo}: {str(e)}")
return False

View File

@ -0,0 +1,8 @@
{
"path": "C:\\Users\\migue\\Downloads\\Nueva carpeta (7)",
"history": [
"C:\\Users\\migue\\Downloads\\Nueva carpeta (7)",
"C:\\Trabajo\\SIDEL\\04 - E5.007299 - Modifica O&U - RNF032\\Entregar\\NEW\\Nueva carpeta",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\04 - E5.007299 - Modifica O&U - RNF032"
]
}

View File

@ -0,0 +1,170 @@
"""
Script para importar archivos HTML o DOCX y convertirlos a un archivo Markdown.
"""
import os
import sys
from pathlib import Path
import json
from utils.html_parser import procesar_html
from utils.markdown_handler import escribir_archivo_markdown
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# Verificar si la biblioteca mammoth está disponible para procesamiento DOCX
try:
from utils.docx_converter import procesar_docx
DOCX_SUPPORT = True
except ImportError:
DOCX_SUPPORT = False
print("Nota: Soporte para DOCX no disponible. Instale mammoth para habilitar esta función.")
# Forzar UTF-8 en la salida estándar
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
# Para versiones anteriores de Python que no tienen reconfigure
pass
# Definir símbolos de éxito/error sin caracteres especiales
SUCCESS_SYMBOL = "[OK]"
ERROR_SYMBOL = "[ERROR]"
def main():
# Cargar configuraciones del entorno
# configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
configs = load_configuration()
# Obtener working directory
working_directory = configs.get("working_directory", ".")
# Obtener configuraciones de nivel 2 (grupo)
group_config = configs.get("level2", {})
output_file = group_config.get("output_file", "contenido.md")
attachments_dir = group_config.get("attachments_dir", "adjuntos")
# Obtener directorio de salida (nivel 3)
work_config = configs.get("level3", {})
output_directory = work_config.get("output_directory", ".")
# Construir rutas absolutas
input_dir = (
working_directory # El directorio de trabajo es el directorio de entrada
)
output_path = os.path.join(output_directory, output_file)
attachments_path = os.path.join(output_directory, attachments_dir)
# Debug prints
print(f"Working directory: {working_directory}")
print(f"Input directory: {input_dir}")
print(f"Output directory: {output_directory}")
print(f"Output file: {output_path}")
print(f"Attachments directory: {attachments_path}")
# Asegurar que existen los directorios
os.makedirs(attachments_path, exist_ok=True)
# Verificar si el directorio de entrada existe
input_path = Path(input_dir)
if not input_path.exists():
print(f"Error: Input directory {input_path} does not exist")
return
# Buscar archivos HTML y DOCX
html_files = []
docx_files = []
# Buscar archivos HTML
for ext in ["*.html", "*.htm"]:
html_files.extend(list(input_path.glob(ext)))
# Buscar archivos DOCX si el soporte está disponible
if DOCX_SUPPORT:
for ext in ["*.docx", "*.doc"]:
docx_files.extend(list(input_path.glob(ext)))
print(f"Found {len(html_files)} HTML files")
if DOCX_SUPPORT:
print(f"Found {len(docx_files)} DOCX files")
if not html_files and not docx_files:
print("No compatible files found in the input directory.")
return
# Procesar archivos
paginas = []
total_files = len(html_files) + len(docx_files)
successful_files = 0
failed_files = 0
# Procesar archivos HTML
for i, archivo in enumerate(html_files, 1):
print(f"\nProcessing HTML [{i}/{total_files}] {archivo}")
try:
pagina = procesar_html(archivo, attachments_path)
if pagina:
paginas.append(pagina)
# Verificar si hubo error al procesar
if pagina.contenido.startswith("Error al procesar:"):
failed_files += 1
titulo_seguro = str(pagina.contenido).encode('ascii', 'replace').decode('ascii')
print(f"{ERROR_SYMBOL} Failed: {titulo_seguro}")
else:
successful_files += 1
# Usar codificación ascii para evitar problemas de caracteres
titulo_seguro = str(pagina.titulo).encode('ascii', 'replace').decode('ascii')
print(f"{SUCCESS_SYMBOL} Success: {titulo_seguro}")
except Exception as e:
failed_files += 1
print(f"{ERROR_SYMBOL} Error processing HTML file: {str(e)}")
# Procesar archivos DOCX si el soporte está disponible
if DOCX_SUPPORT:
for i, archivo in enumerate(docx_files, len(html_files) + 1):
print(f"\nProcessing DOCX [{i}/{total_files}] {archivo}")
try:
pagina = procesar_docx(archivo, attachments_path)
if pagina:
paginas.append(pagina)
if pagina.contenido.startswith("Error al procesar:"):
failed_files += 1
titulo_seguro = str(pagina.contenido).encode('ascii', 'replace').decode('ascii')
print(f"{ERROR_SYMBOL} Failed: {titulo_seguro}")
else:
successful_files += 1
titulo_seguro = str(pagina.titulo).encode('ascii', 'replace').decode('ascii')
print(f"{SUCCESS_SYMBOL} Success: {titulo_seguro}")
except Exception as e:
failed_files += 1
error_msg = str(e).encode('ascii', 'replace').decode('ascii')
print(f"{ERROR_SYMBOL} Error processing DOCX file: {error_msg}")
# Crear página con error
from models.pagina_html import PaginaHTML
error_pagina = PaginaHTML(
ruta_archivo=archivo,
contenido=f"Error al procesar DOCX: {str(e)}"
)
paginas.append(error_pagina)
# Escribir el archivo Markdown
if paginas:
print(f"\nSummary:")
print(f"- Total files: {total_files}")
print(f"- Successfully processed: {successful_files}")
print(f"- Failed: {failed_files}")
print(f"\nWriting {len(paginas)} pages to {output_path}")
if escribir_archivo_markdown(paginas, output_path):
print("Markdown file created successfully.")
else:
print("Error creating Markdown file.")
else:
print("No pages to write.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -0,0 +1,38 @@
--- Log de Ejecución: x2.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-02 23:34:21
Fin: 2025-05-02 23:36:20
Duración: 0:01:58.373747
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- TIA Portal Project CAx Exporter and Analyzer ---
Selected Project: C:/Trabajo/SIDEL/06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)/InLavoro/PLC/SAE196_c0.2/SAE196_c0.2.ap18
Using Output Directory (Working Directory): C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Will export CAx data to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Will generate summary to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Summary.md
Export log file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.log
Connecting to TIA Portal V18.0...
2025-05-02 23:34:30,132 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-02 23:34:30,155 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
Connected.
Opening project: SAE196_c0.2.ap18...
2025-05-02 23:35:01,950 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\InLavoro\PLC\SAE196_c0.2\SAE196_c0.2.ap18
Project opened.
Exporting CAx data for the project to C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml...
CAx data exported successfully.
Closing TIA Portal...
2025-05-02 23:36:15,947 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
TIA Portal closed.
Parsing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Summary.md
Script finished.
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -0,0 +1,48 @@
--- Log de Ejecución: x3.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-02 23:43:07
Fin: 2025-05-02 23:43:12
Duración: 0:00:05.235415
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v28 - Working Directory Integration) ---
Using Working Directory for Output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
Output Main Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md
Output IO Debug Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
Processing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml
Pass 1: Found 203 InternalElement(s). Populating device dictionary...
Pass 2: Identifying PLCs and Networks (Refined v2)...
Identified Network: PROFIBUS_1 (bcc6f2bd-3d71-4407-90f2-bccff6064051) Type: Profibus
Identified Network: ETHERNET_1 (c6d49787-a076-4592-994d-876eea123dfd) Type: Ethernet/Profinet
Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0
Pass 3: Processing InternalLinks (Robust Network Mapping & IO)...
Found 118 InternalLink(s).
Mapping Device/Node 'E1' (NodeID:1643b51f-7067-4565-8f8e-109a1a775fed, Addr:10.1.33.11) to Network 'ETHERNET_1'
--> Associating Network 'ETHERNET_1' with PLC 'PLC' (via Node 'E1' Addr: 10.1.33.11)
Mapping Device/Node 'P1' (NodeID:5aff409b-2573-485f-82bf-0e08c9200086, Addr:1) to Network 'PROFIBUS_1'
--> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1)
Mapping Device/Node 'PB1' (NodeID:c796e175-c770-43f0-8191-fc91996c0147, Addr:12) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:0b44f55a-63c1-49e8-beea-24dc5d3226e3, Addr:20) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:25cfc251-f946-40c5-992d-ad6387677acb, Addr:21) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:57999375-ec72-46ef-8ec2-6c3178e8acf8, Addr:22) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:54e8db6a-9443-41a4-a85b-cf0722c1d299, Addr:10) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:4786bab6-4097-4651-ac19-6cadfc7ea735, Addr:8) to Network 'PROFIBUS_1'
Mapping Device/Node 'PB1' (NodeID:1f08afcb-111f-428f-915e-69363af1b09a, Addr:40) to Network 'PROFIBUS_1'
Data extraction and structuring complete.
Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
JSON data written successfully.
Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md
IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
Script finished.
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -0,0 +1,9 @@
{
"level1": {
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo"
},
"level2": {},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
}

View File

@ -0,0 +1,6 @@
{
"path": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport",
"history": [
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
]
}

View File

@ -0,0 +1,325 @@
"""
export_logic_from_tia :
Script para exportar el software de un PLC desde TIA Portal en archivos XML y SCL.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
TIA_PORTAL_VERSION = "18.0" # Target TIA Portal version (e.g., "18.0")
EXPORT_OPTIONS = None # Use default export options
KEEP_FOLDER_STRUCTURE = (
True # Replicate TIA project folder structure in export directory
)
# --- TIA Scripting Import Handling ---
# Check if the TIA_SCRIPTING environment variable is set
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
# Optional: Define a fallback path if the environment variable isn't set
# fallback_path = "C:\\path\\to\\your\\TIA_Scripting_binaries"
# if os.path.exists(fallback_path):
# sys.path.append(fallback_path)
pass # Allow import to fail if not found
try:
import siemens_tia_scripting as ts
EXPORT_OPTIONS = (
ts.Enums.ExportOptions.WithDefaults
) # Set default options now that 'ts' is imported
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure:")
print(f"1. TIA Portal Openness for V{TIA_PORTAL_VERSION} is installed.")
print(
"2. The 'siemens_tia_scripting' Python module is installed (pip install ...) or"
)
print(
" the path to its binaries is set in the 'TIA_SCRIPTING' environment variable."
)
print(
"3. You are using a compatible Python version (e.g., 3.12.X as per documentation)."
)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=[
(
f"TIA Portal V{TIA_PORTAL_VERSION} Projects",
f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}",
)
], # e.g. *.ap18
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_export_directory():
"""Opens a dialog to select the export directory."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
dir_path = filedialog.askdirectory(title="Select Export Directory")
root.destroy()
if not dir_path:
print("No export directory selected. Exiting.")
sys.exit(0)
return dir_path
def export_plc_data(plc, export_base_dir):
"""Exports Blocks, UDTs, and Tag Tables from a given PLC."""
plc_name = plc.get_name()
print(f"\n--- Processing PLC: {plc_name} ---")
# Define base export path for this PLC
plc_export_dir = os.path.join(export_base_dir, plc_name)
os.makedirs(plc_export_dir, exist_ok=True)
# --- Export Program Blocks ---
blocks_exported = 0
blocks_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting Program Blocks...")
xml_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_XML")
scl_blocks_path = os.path.join(plc_export_dir, "ProgramBlocks_SCL")
os.makedirs(xml_blocks_path, exist_ok=True)
os.makedirs(scl_blocks_path, exist_ok=True)
print(f" XML Target: {xml_blocks_path}")
print(f" SCL Target: {scl_blocks_path}")
try:
program_blocks = plc.get_program_blocks() #
print(f" Found {len(program_blocks)} program blocks.")
for block in program_blocks:
block_name = block.get_name() # Assuming get_name() exists
print(f" Processing block: {block_name}...")
try:
if not block.is_consistent(): #
print(f" Compiling block {block_name}...")
block.compile() #
if not block.is_consistent():
print(
f" WARNING: Block {block_name} inconsistent after compile. Skipping."
)
blocks_skipped += 1
continue
print(f" Exporting {block_name} as XML...")
block.export(
target_directory_path=xml_blocks_path, #
export_options=EXPORT_OPTIONS, #
export_format=ts.Enums.ExportFormats.SimaticML, #
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
try:
prog_language = block.get_property(name="ProgrammingLanguage")
if prog_language == "SCL":
print(f" Exporting {block_name} as SCL...")
block.export(
target_directory_path=scl_blocks_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.ExternalSource, #
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
except Exception as prop_ex:
print(
f" Could not get ProgrammingLanguage for {block_name}. Skipping SCL. Error: {prop_ex}"
)
blocks_exported += 1
except Exception as block_ex:
print(f" ERROR exporting block {block_name}: {block_ex}")
blocks_skipped += 1
print(
f" Program Blocks Export Summary: Exported={blocks_exported}, Skipped/Errors={blocks_skipped}"
)
except Exception as e:
print(f" ERROR processing Program Blocks: {e}")
traceback.print_exc()
# --- Export PLC Data Types (UDTs) ---
udts_exported = 0
udts_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Data Types (UDTs)...")
udt_export_path = os.path.join(plc_export_dir, "PlcDataTypes")
os.makedirs(udt_export_path, exist_ok=True)
print(f" Target: {udt_export_path}")
try:
udts = plc.get_user_data_types() #
print(f" Found {len(udts)} UDTs.")
for udt in udts:
udt_name = udt.get_name() #
print(f" Processing UDT: {udt_name}...")
try:
if not udt.is_consistent(): #
print(f" Compiling UDT {udt_name}...")
udt.compile() #
if not udt.is_consistent():
print(
f" WARNING: UDT {udt_name} inconsistent after compile. Skipping."
)
udts_skipped += 1
continue
print(f" Exporting {udt_name}...")
udt.export(
target_directory_path=udt_export_path, #
export_options=EXPORT_OPTIONS, #
# export_format defaults to SimaticML for UDTs
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
udts_exported += 1
except Exception as udt_ex:
print(f" ERROR exporting UDT {udt_name}: {udt_ex}")
udts_skipped += 1
print(
f" UDT Export Summary: Exported={udts_exported}, Skipped/Errors={udts_skipped}"
)
except Exception as e:
print(f" ERROR processing UDTs: {e}")
traceback.print_exc()
# --- Export PLC Tag Tables ---
tags_exported = 0
tags_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Tag Tables...")
tags_export_path = os.path.join(plc_export_dir, "PlcTags")
os.makedirs(tags_export_path, exist_ok=True)
print(f" Target: {tags_export_path}")
try:
tag_tables = plc.get_plc_tag_tables() #
print(f" Found {len(tag_tables)} Tag Tables.")
for table in tag_tables:
table_name = table.get_name() #
print(f" Processing Tag Table: {table_name}...")
try:
# Note: Consistency check might not be available/needed for tag tables like blocks/UDTs
print(f" Exporting {table_name}...")
table.export(
target_directory_path=tags_export_path, #
export_options=EXPORT_OPTIONS, #
# export_format defaults to SimaticML for Tag Tables
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
) #
tags_exported += 1
except Exception as table_ex:
print(f" ERROR exporting Tag Table {table_name}: {table_ex}")
tags_skipped += 1
print(
f" Tag Table Export Summary: Exported={tags_exported}, Skipped/Errors={tags_skipped}"
)
except Exception as e:
print(f" ERROR processing Tag Tables: {e}")
traceback.print_exc()
print(f"\n--- Finished processing PLC: {plc_name} ---")
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal Data Exporter (Blocks, UDTs, Tags) ---")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Export Directory comes from config
project_file = select_project_file()
export_dir = working_directory # Use working directory from config
print(f"\nSelected Project: {project_file}")
print(f"Using Export Directory (Working Directory): {export_dir}")
portal_instance = None
project_object = None
try:
# 2. Connect to TIA Portal
print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...")
portal_instance = ts.open_portal(
version=TIA_PORTAL_VERSION,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected to TIA Portal.")
print(f"Portal Process ID: {portal_instance.get_process_id()}") #
# 3. Open Project
print(f"Opening project: {os.path.basename(project_file)}...")
project_object = portal_instance.open_project(project_file_path=project_file) #
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project() #
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened successfully.")
# 4. Get PLCs
plcs = project_object.get_plcs() #
if not plcs:
print("No PLC devices found in the project.")
else:
print(f"Found {len(plcs)} PLC(s). Starting export process...")
# 5. Iterate and Export Data for each PLC
for plc_device in plcs:
export_plc_data(
plc=plc_device, export_base_dir=export_dir
) # Pass export_dir
print("\nExport process completed.")
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred: {e}")
traceback.print_exc()
finally:
# 6. Cleanup
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal() #
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
print("\nScript finished.")

View File

@ -0,0 +1,339 @@
"""
export_CAx_from_tia :
Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
import xml.etree.ElementTree as ET # Library to parse XML (AML)
from pathlib import Path # Import Path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
TIA_PORTAL_VERSION = "18.0" # Target TIA Portal version
# --- TIA Scripting Import Handling ---
# (Same import handling as the previous script)
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
pass
try:
import siemens_tia_scripting as ts
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure TIA Openness, the module, and Python 3.12.X are set up.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw()
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=[
(
f"TIA Portal V{TIA_PORTAL_VERSION} Projects",
f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}",
)
],
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_output_directory():
"""Opens a dialog to select the output directory."""
root = tk.Tk()
root.withdraw()
dir_path = filedialog.askdirectory(
title="Select Output Directory for AML and MD files"
)
root.destroy()
if not dir_path:
print("No output directory selected. Exiting.")
sys.exit(0)
return dir_path
def find_elements(element, path):
"""Helper to find elements using namespaces commonly found in AML."""
# AutomationML namespaces often vary slightly or might be default
# This basic approach tries common prefixes or no prefix
namespaces = {
"": (
element.tag.split("}")[0][1:] if "}" in element.tag else ""
), # Default namespace if present
"caex": "http://www.dke.de/CAEX", # Common CAEX namespace
# Add other potential namespaces if needed based on file inspection
}
# Try finding with common prefixes or the default namespace
for prefix, uri in namespaces.items():
# Construct path with namespace URI if prefix is defined
namespaced_path = path
if prefix:
parts = path.split("/")
namespaced_parts = [
f"{{{uri}}}{part}" if part != "." else part for part in parts
]
namespaced_path = "/".join(namespaced_parts)
# Try findall with the constructed path
found = element.findall(namespaced_path)
if found:
return found # Return first successful find
# Fallback: try finding without explicit namespace (might work if default ns is used throughout)
# This might require adjusting the path string itself depending on the XML structure
try:
# Simple attempt without namespace handling if the above fails
return element.findall(path)
except (
SyntaxError
): # Handle potential errors if path isn't valid without namespaces
return []
def parse_aml_to_markdown(aml_file_path, md_file_path):
"""Parses the AML file and generates a Markdown summary."""
print(f"Parsing AML file: {aml_file_path}")
try:
tree = ET.parse(aml_file_path)
root = tree.getroot()
markdown_lines = ["# Project CAx Data Summary (AutomationML)", ""]
# Find InstanceHierarchy - usually contains the project structure
# Note: Namespace handling in ElementTree can be tricky. Adjust '{...}' part if needed.
# We will use a helper function 'find_elements' to try common patterns
instance_hierarchies = find_elements(
root, ".//InstanceHierarchy"
) # Common CAEX tag
if not instance_hierarchies:
markdown_lines.append("Could not find InstanceHierarchy in the AML file.")
print("Warning: Could not find InstanceHierarchy element.")
else:
# Assuming the first InstanceHierarchy is the main one
ih = instance_hierarchies[0]
markdown_lines.append(f"## Instance Hierarchy: {ih.get('Name', 'N/A')}")
markdown_lines.append("")
# Look for InternalElements which represent devices/components
internal_elements = find_elements(
ih, ".//InternalElement"
) # Common CAEX tag
if not internal_elements:
markdown_lines.append(
"No devices (InternalElement) found in InstanceHierarchy."
)
print("Info: No InternalElement tags found under InstanceHierarchy.")
else:
markdown_lines.append(
f"Found {len(internal_elements)} device(s)/component(s):"
)
markdown_lines.append("")
markdown_lines.append(
"| Name | SystemUnitClass | RefBaseSystemUnitPath | Attributes |"
)
markdown_lines.append("|---|---|---|---|")
for elem in internal_elements:
name = elem.get("Name", "N/A")
ref_path = elem.get(
"RefBaseSystemUnitPath", "N/A"
) # Path to class definition
# Try to get the class name from the RefBaseSystemUnitPath or SystemUnitClassLib
su_class_path = find_elements(
elem, ".//SystemUnitClass"
) # Check direct child first
su_class = (
su_class_path[0].get("Path", "N/A")
if su_class_path
else ref_path.split("/")[-1]
) # Fallback to last part of path
attributes_md = ""
attributes = find_elements(elem, ".//Attribute") # Find attributes
attr_list = []
for attr in attributes:
attr_name = attr.get("Name", "")
attr_value_elem = find_elements(
attr, ".//Value"
) # Get Value element
attr_value = (
attr_value_elem[0].text
if attr_value_elem and attr_value_elem[0].text
else "N/A"
)
# Look for potential IP addresses (common attribute names)
if "Address" in attr_name or "IP" in attr_name:
attr_list.append(f"**{attr_name}**: {attr_value}")
else:
attr_list.append(f"{attr_name}: {attr_value}")
attributes_md = "<br>".join(attr_list) if attr_list else "None"
markdown_lines.append(
f"| {name} | {su_class} | `{ref_path}` | {attributes_md} |"
)
# Write to Markdown file
with open(md_file_path, "w", encoding="utf-8") as f:
f.write("\n".join(markdown_lines))
print(f"Markdown summary written to: {md_file_path}")
except ET.ParseError as xml_err:
print(f"ERROR parsing XML file {aml_file_path}: {xml_err}")
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nFailed to parse AML file: {os.path.basename(aml_file_path)}\n\nError: {xml_err}"
)
except Exception as e:
print(f"ERROR processing AML file {aml_file_path}: {e}")
traceback.print_exc()
with open(md_file_path, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nAn unexpected error occurred while processing AML file: {os.path.basename(aml_file_path)}\n\nError: {e}"
)
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal Project CAx Exporter and Analyzer ---")
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Output Directory comes from config
project_file = select_project_file()
output_dir = Path(
working_directory
) # Use working directory from config, ensure it's a Path object
print(f"\nSelected Project: {project_file}")
print(f"Using Output Directory (Working Directory): {output_dir}")
# Define output file names using Path object
project_path = Path(project_file)
project_base_name = project_path.stem # Get filename without extension
aml_file = output_dir / f"{project_base_name}_CAx_Export.aml"
md_file = output_dir / f"{project_base_name}_CAx_Summary.md"
log_file = (
output_dir / f"{project_base_name}_CAx_Export.log"
) # Log file for the export process
print(f"Will export CAx data to: {aml_file}")
print(f"Will generate summary to: {md_file}")
print(f"Export log file: {log_file}")
portal_instance = None
project_object = None
cax_export_successful = False
try:
# 2. Connect to TIA Portal
print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...")
portal_instance = ts.open_portal(
version=TIA_PORTAL_VERSION,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected.")
# 3. Open Project
print(
f"Opening project: {project_path.name}..."
) # Use Path object's name attribute
project_object = portal_instance.open_project(
project_file_path=str(project_path)
) # Pass path as string
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project()
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened.")
# 4. Export CAx Data (Project Level)
print(f"Exporting CAx data for the project to {aml_file}...")
# Ensure output directory exists (Path.mkdir handles this implicitly if needed later, but good practice)
output_dir.mkdir(parents=True, exist_ok=True)
# Pass paths as strings to the TIA function
export_result = project_object.export_cax_data(
export_file_path=str(aml_file), log_file_path=str(log_file)
)
if export_result:
print("CAx data exported successfully.")
cax_export_successful = True
else:
print("CAx data export failed. Check the log file for details:")
print(f" Log file: {log_file}")
# Write basic error message to MD file if export fails
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nCAx data export failed. Check log file: {log_file}"
)
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred during TIA interaction: {e}")
traceback.print_exc()
finally:
# Close TIA Portal before processing the file (or detach)
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal()
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
# 5. Parse AML and Generate Markdown (only if export was successful)
if cax_export_successful:
if aml_file.exists(): # Use Path object's exists() method
parse_aml_to_markdown(aml_file, md_file)
else:
print(
f"ERROR: Export was reported successful, but AML file not found at {aml_file}"
)
with open(md_file, "w", encoding="utf-8") as f:
f.write(
f"# Error\n\nExport was reported successful, but AML file not found:\n{aml_file}"
)
print("\nScript finished.")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -0,0 +1,4 @@
{
"type": "object",
"properties": {}
}

View File

@ -0,0 +1,28 @@
# generators/generate_md_tag_table.py
# -*- coding: utf-8 -*-
def generate_tag_table_markdown(data):
"""Genera contenido Markdown para una tabla de tags."""
md_lines = []
table_name = data.get("block_name", "UnknownTagTable")
tags = data.get("tags", [])
md_lines.append(f"# Tag Table: {table_name}")
md_lines.append("")
if tags:
md_lines.append("| Name | Datatype | Address | Comment |")
md_lines.append("|---|---|---|---|")
for tag in tags:
name = tag.get("name", "N/A")
datatype = tag.get("datatype", "N/A")
address = tag.get("address", "N/A") or " "
comment_raw = tag.get("comment")
comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else ""
md_lines.append(f"| `{name}` | `{datatype}` | `{address}` | {comment} |")
md_lines.append("")
else:
md_lines.append("No tags found in this table.")
md_lines.append("")
return md_lines

View File

@ -0,0 +1,46 @@
# generators/generate_md_udt.py
# -*- coding: utf-8 -*-
import re
from .generator_utils import format_scl_start_value # Importar utilidad necesaria
def generate_markdown_member_rows(members, level=0):
"""Genera filas Markdown para miembros de UDT (recursivo)."""
md_rows = []; prefix = "&nbsp;&nbsp;&nbsp;&nbsp;" * level
for member in members:
name = member.get("name", "N/A"); datatype = member.get("datatype", "N/A")
start_value_raw = member.get("start_value")
start_value_fmt = format_scl_start_value(start_value_raw, datatype) if start_value_raw is not None else ""
comment_raw = member.get("comment"); comment = comment_raw.replace('|', '\|').replace('\n', ' ') if comment_raw else ""
md_rows.append(f"| {prefix}`{name}` | `{datatype}` | `{start_value_fmt}` | {comment} |")
children = member.get("children")
if children: md_rows.extend(generate_markdown_member_rows(children, level + 1))
array_elements = member.get("array_elements")
if array_elements:
base_type_for_init = datatype
if isinstance(datatype, str) and datatype.lower().startswith("array["):
match = re.match(r"(Array\[.*\]\s+of\s+)(.*)", datatype, re.IGNORECASE)
if match: base_type_for_init = match.group(2).strip()
md_rows.append(f"| {prefix}&nbsp;&nbsp;*(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}&nbsp;&nbsp;`[{idx_str}]` | | `{val_fmt}` | |")
return md_rows
def generate_udt_markdown(data):
"""Genera contenido Markdown para un UDT."""
md_lines = []; udt_name = data.get("block_name", "UnknownUDT"); udt_comment = data.get("block_comment", "")
md_lines.append(f"# UDT: {udt_name}"); md_lines.append("")
if udt_comment: md_lines.append(f"**Comment:**"); [md_lines.append(f"> {line}") for line in udt_comment.splitlines()]; md_lines.append("")
members = data.get("interface", {}).get("None", [])
if members:
md_lines.append("## Members"); md_lines.append("")
md_lines.append("| Name | Datatype | Start Value | Comment |"); md_lines.append("|---|---|---|---|")
md_lines.extend(generate_markdown_member_rows(members))
md_lines.append("")
else: md_lines.append("No members found in the UDT interface."); md_lines.append("")
return md_lines

View File

@ -0,0 +1,285 @@
# ToUpload/generators/generate_scl_code_block.py
# -*- coding: utf-8 -*-
import re
import os # Importar os
from .generator_utils import format_variable_name, generate_scl_declarations
SCL_SUFFIX = "_sympy_processed"
# ... (_generate_scl_header sin cambios)...
def _generate_scl_header(data, scl_block_name):
scl_output = []
block_type = data.get("block_type", "Unknown")
block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number")
block_comment = data.get("block_comment", "")
scl_block_keyword = "FUNCTION_BLOCK"
if block_type == "FC":
scl_block_keyword = "FUNCTION"
elif block_type == "OB":
scl_block_keyword = "ORGANIZATION_BLOCK"
scl_output.append(f"// Block Type: {block_type}")
if block_name != scl_block_name:
scl_output.append(f"// Block Name (Original): {block_name}")
if block_number:
scl_output.append(f"// Block Number: {block_number}")
original_net_langs = set(
n.get("language", "Unknown") for n in data.get("networks", [])
)
scl_output.append(
f"// Original Network Languages: {', '.join(l for l in original_net_langs if l != 'Unknown')}"
)
if block_comment:
scl_output.append(f"// Block Comment:")
[scl_output.append(f"// {line}") for line in block_comment.splitlines()]
scl_output.append("")
if block_type == "FC":
return_type = "Void"
interface_data = data.get("interface", {})
if interface_data.get("Return"):
return_member = interface_data["Return"][0]
return_type_raw = return_member.get("datatype", "Void")
return_type = (
return_type_raw[1:-1]
if isinstance(return_type_raw, str)
and return_type_raw.startswith('"')
and return_type_raw.endswith('"')
else return_type_raw
)
if return_type != return_type_raw and not (
isinstance(return_type_raw, str)
and return_type_raw.lower().startswith("array")
):
return_type = f'"{return_type}"'
else:
return_type = return_type_raw
scl_output.append(f'{scl_block_keyword} "{scl_block_name}" : {return_type}')
else:
scl_output.append(f'{scl_block_keyword} "{scl_block_name}"')
scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
scl_output.append("VERSION : 0.1")
scl_output.append("")
return scl_output
# Modificar _generate_scl_interface para pasar project_root_dir
def _generate_scl_interface(interface_data, project_root_dir): # <-- Nuevo argumento
"""Genera las secciones VAR_* de la interfaz SCL para FC/FB/OB."""
scl_output = []
section_order = [
"Input",
"Output",
"InOut",
"Static",
"Temp",
"Constant",
"Return",
] # Incluir Return
declared_temps = set() # Para _generate_scl_temp_vars
for section_name in section_order:
vars_in_section = interface_data.get(section_name, [])
if vars_in_section:
scl_section_keyword = f"VAR_{section_name.upper()}"
end_keyword = "END_VAR"
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"
end_keyword = "END_CONSTANT"
if section_name == "Return":
scl_section_keyword = "VAR_OUTPUT"
# Retorno va en Output para FB/OB, implícito en FC
# Para FC, la sección Return no se declara explícitamente aquí
if (
interface_data.get("parent_block_type") == "FC"
and section_name == "Return"
):
continue
scl_output.append(scl_section_keyword)
# Pasar project_root_dir a generate_scl_declarations
scl_output.extend(
generate_scl_declarations(
vars_in_section, indent_level=1, project_root_dir=project_root_dir
)
) # <-- Pasar ruta raíz
scl_output.append(end_keyword)
scl_output.append("")
if section_name == "Temp":
declared_temps.update(
format_variable_name(v.get("name"))
for v in vars_in_section
if v.get("name")
)
return scl_output, declared_temps
# ... (_generate_scl_temp_vars y _generate_scl_body sin cambios) ...
def _generate_scl_temp_vars(data, declared_temps):
scl_output = []
temp_vars_detected = set()
temp_pattern = re.compile(r'"?(#\w+)"?')
for network in data.get("networks", []):
for instruction in network.get("logic", []):
scl_code = instruction.get("scl", "")
edge_update_code = instruction.get("_edge_mem_update_scl", "")
code_to_scan = (
(scl_code if scl_code else "")
+ "\n"
+ (edge_update_code if edge_update_code else "")
)
if code_to_scan:
found_temps = temp_pattern.findall(code_to_scan)
[temp_vars_detected.add(t) for t in found_temps if t]
additional_temps = sorted(list(temp_vars_detected - declared_temps))
if additional_temps:
print(f"INFO: Detectadas {len(additional_temps)} VAR_TEMP adicionales.")
temp_section_exists = any(
"VAR_TEMP" in s for s in data.get("generated_scl", [])
) # Check if VAR_TEMP already exists
if not temp_section_exists and not declared_temps:
scl_output.append("VAR_TEMP") # Only add if no temps were declared before
for temp_name in additional_temps:
scl_name = format_variable_name(temp_name)
inferred_type = "Bool"
scl_output.append(
f" {scl_name} : {inferred_type}; // Auto-generated temporary"
)
if not temp_section_exists and not declared_temps:
scl_output.append("END_VAR")
scl_output.append("")
return scl_output
def _generate_scl_body(networks):
scl_output = ["BEGIN", ""]
network_logic_added = False
for i, network in enumerate(networks):
network_title = network.get("title", f'Network {network.get("id", i+1)}')
network_comment = network.get("comment", "")
network_lang = network.get("language", "LAD")
scl_output.append(
f" // Network {i+1}: {network_title} (Original Language: {network_lang})"
)
if network_comment:
[
scl_output.append(f" // {line}")
for line in network_comment.splitlines()
]
scl_output.append("")
network_has_code = False
logic_in_network = network.get("logic", [])
if not logic_in_network:
scl_output.append(f" // Network {i+1} has no logic elements.")
scl_output.append("")
continue
if network_lang == "STL":
if logic_in_network and logic_in_network[0].get("type") == "RAW_STL_CHUNK":
network_has_code = True
raw_stl_code = logic_in_network[0].get(
"stl", "// ERROR: STL code missing"
)
scl_output.append(f" // --- BEGIN STL Network {i+1} ---")
scl_output.append(f" ```stl ")
[
scl_output.append(f" {stl_line}") # scl_output.append(f" // {stl_line}")
for stl_line in raw_stl_code.splitlines()
]
scl_output.append(f" ``` ")
scl_output.append(f" // --- END STL Network {i+1} ---")
scl_output.append("")
else:
scl_output.append(
f" // ERROR: Contenido STL inesperado en Network {i+1}."
)
scl_output.append("")
else:
for instruction in logic_in_network:
instruction_type = instruction.get("type", "")
scl_code = instruction.get("scl", "")
is_grouped = instruction.get("grouped", False)
edge_update_scl = instruction.get("_edge_mem_update_scl", "")
if is_grouped:
continue
code_to_print = []
if scl_code:
code_to_print.extend(scl_code.splitlines())
if edge_update_scl:
code_to_print.extend(
edge_update_scl.splitlines()
) # Append edge update SCL
if code_to_print:
is_only_comment = all(
line.strip().startswith("//")
for line in code_to_print
if line.strip()
)
is_if_block = any(
line.strip().startswith("IF") for line in code_to_print
)
if (
not is_only_comment
or is_if_block
or "_error" in instruction_type
or instruction_type
in [
"UNSUPPORTED_LANG",
"UNSUPPORTED_CONTENT",
"PARSING_ERROR",
"RAW_SCL_CHUNK",
]
): # Print RAW_SCL chunks too
network_has_code = True
[scl_output.append(f" {line}") for line in code_to_print]
scl_output.append("")
if not network_has_code and network_lang != "STL":
scl_output.append(f" // Network {i+1} did not produce printable SCL code.")
scl_output.append("")
if network_has_code:
network_logic_added = True # Mark if any network had code
# Add a default comment if no logic was generated at all
if not network_logic_added:
scl_output.append(" // No executable logic generated by script.")
scl_output.append("")
return scl_output
# Modificar generate_scl_for_code_block para aceptar y pasar project_root_dir
def generate_scl_for_code_block(data, project_root_dir): # <-- Nuevo argumento
"""Genera el contenido SCL completo para un FC/FB/OB."""
scl_output = []
block_type = data.get("block_type", "Unknown")
scl_block_name = format_variable_name(data.get("block_name", "UnknownBlock"))
scl_block_keyword = "FUNCTION_BLOCK" # Default for FB
if block_type == "FC":
scl_block_keyword = "FUNCTION"
elif block_type == "OB":
scl_block_keyword = "ORGANIZATION_BLOCK"
scl_output.extend(_generate_scl_header(data, scl_block_name))
interface_data = data.get("interface", {})
interface_data["parent_block_type"] = block_type # Ayuda a _generate_scl_interface
# Pasar project_root_dir a _generate_scl_interface
interface_lines, declared_temps = _generate_scl_interface(
interface_data, project_root_dir
) # <-- Pasar ruta raíz
scl_output.extend(interface_lines)
# Generar VAR_TEMP adicionales (no necesita project_root_dir)
scl_output.extend(_generate_scl_temp_vars(data, declared_temps))
# Generar cuerpo (no necesita project_root_dir)
scl_output.extend(_generate_scl_body(data.get("networks", [])))
scl_output.append(f"END_{scl_block_keyword}")
# Guardar SCL generado en data para _generate_scl_temp_vars
data["generated_scl"] = scl_output
return scl_output

View File

@ -0,0 +1,54 @@
# ToUpload/generators/generate_scl_db.py
# -*- coding: utf-8 -*-
# No necesita importar json/os aquí, lo hará generate_scl_declarations
from .generator_utils import format_variable_name, generate_scl_declarations
# Modificar _generate_scl_header si es necesario, pero parece ok
def _generate_scl_header(data, scl_block_name):
# ... (código sin cambios) ...
scl_output = []
block_type = data.get("block_type", "Unknown")
block_name = data.get("block_name", "UnknownBlock")
block_number = data.get("block_number")
block_comment = data.get("block_comment", "")
scl_output.append(f"// Block Type: {block_type}")
if block_name != scl_block_name: scl_output.append(f"// Block Name (Original): {block_name}")
if block_number: scl_output.append(f"// Block Number: {block_number}")
if block_comment: scl_output.append(f"// Block Comment:"); [scl_output.append(f"// {line}") for line in block_comment.splitlines()]
scl_output.append(""); scl_output.append(f'DATA_BLOCK "{scl_block_name}"'); scl_output.append("{ S7_Optimized_Access := 'TRUE' }")
scl_output.append("VERSION : 0.1"); scl_output.append("")
return scl_output
# Modificar _generate_scl_interface para pasar project_root_dir
def _generate_scl_interface(interface_data, project_root_dir): # <-- Nuevo argumento
"""Genera la sección VAR para DB (basada en 'Static')."""
scl_output = []
static_vars = interface_data.get("Static", [])
if static_vars:
scl_output.append("VAR")
# Pasar project_root_dir a generate_scl_declarations
scl_output.extend(generate_scl_declarations(static_vars, indent_level=1, project_root_dir=project_root_dir)) # <-- Pasar ruta raíz
scl_output.append("END_VAR")
else:
print("Advertencia: No se encontró sección 'Static' o está vacía en la interfaz del DB.")
scl_output.append("VAR\nEND_VAR") # Añadir vacío
scl_output.append("")
return scl_output
# Modificar generate_scl_for_db para aceptar y pasar project_root_dir
def generate_scl_for_db(data, project_root_dir): # <-- Nuevo argumento
"""Genera el contenido SCL completo para un DATA_BLOCK."""
scl_output = []
scl_block_name = format_variable_name(data.get("block_name", "UnknownDB"))
scl_output.extend(_generate_scl_header(data, scl_block_name))
interface_data = data.get("interface", {})
# Pasar project_root_dir a _generate_scl_interface
scl_output.extend(_generate_scl_interface(interface_data, project_root_dir)) # <-- Pasar ruta raíz
scl_output.append("BEGIN")
scl_output.append(" // Data Blocks have no executable code")
scl_output.append("END_DATA_BLOCK")
return scl_output

View File

@ -0,0 +1,278 @@
# ToUpload/generators/generator_utils.py
# -*- coding: utf-8 -*-
import re
import os
import json
import traceback # Para depuración si es necesario
import sys
# --- Importar format_variable_name desde processors ---
try:
# Asumiendo que este script está en 'generators' y 'processors' está al mismo nivel
current_dir = os.path.dirname(os.path.abspath(__file__))
project_base_dir = os.path.dirname(current_dir)
processors_dir = os.path.join(project_base_dir, 'processors')
if processors_dir not in sys.path:
sys.path.insert(0, processors_dir) # Añadir al path si no está
from processor_utils import format_variable_name
except ImportError:
print("Advertencia: No se pudo importar 'format_variable_name' desde processors.processor_utils.")
print("Usando una implementación local básica.")
def format_variable_name(name): # Fallback
if not name: return "_INVALID_NAME_"
if name.startswith('"') and name.endswith('"'): return name
prefix = "#" if name.startswith("#") else ""
if prefix: name = name[1:]
if name and name[0].isdigit(): name = "_" + name
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
return prefix + name
# --- Fin Fallback ---
# --- format_scl_start_value (Sin cambios respecto a la versión anterior) ---
def format_scl_start_value(value, datatype):
if value is None: return None
# Convertir complex dict a string para procesar
if isinstance(value, dict):
# Si tiene 'value', usar ese. Si no, representar el dict como comentario
value_to_process = value.get('value')
if value_to_process is None:
return f"/* Init: {json.dumps(value)} */" # Representar dict como comentario
value = value_to_process # Usar el valor interno
datatype_lower = datatype.lower() if isinstance(datatype, str) else ""
value_str = str(value)
# Determinar si es tipo complejo (no estrictamente básico)
is_complex_type = (
('"' in datatype_lower) or ('array' in datatype_lower) or ('struct' in datatype_lower) or
datatype_lower not in {
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
"s5time", "date", "dt", "dtl", "tod", "string", "char", "wstring", "wchar", "variant",
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer" # Añadir otros tipos IEC comunes
}
)
if is_complex_type:
# Para tipos complejos, solo permitir constantes simbólicas o inicializadores básicos (0, FALSE, '')
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str): return value_str # Constante simbólica
if value_str == '0': return '0' # Cero numérico
if value_str.lower() == 'false': return 'FALSE' # Booleano Falso
if value_str == "''" or value_str == "": return "''" # String vacío
# Ignorar otros valores iniciales para tipos complejos (incluye JSON de arrays)
# print(f"INFO: Start value '{value_str}' for complex type '{datatype}' skipped.")
return None
# Quitar comillas simples/dobles externas si las hay
value_str_unquoted = value_str
if len(value_str) > 1:
if value_str.startswith('"') and value_str.endswith('"'): value_str_unquoted = value_str[1:-1]
elif value_str.startswith("'") and value_str.endswith("'"): value_str_unquoted = value_str[1:-1]
# Formateo por tipo básico
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: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
elif "bool" in datatype_lower:
val_low = value_str_unquoted.lower();
if val_low in ['true', '1']: return "TRUE"
elif val_low in ['false', '0']: return "FALSE"
else: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else "FALSE" # Default FALSE
elif "string" in datatype_lower or "char" in datatype_lower:
escaped_value = value_str_unquoted.replace("'", "''") # Escapar comillas simples
prefix = "WSTRING#" if "wstring" in datatype_lower else ("WCHAR#" if "wchar" in datatype_lower else "")
return f"{prefix}'{escaped_value}'" # Usar comillas simples SCL
elif "real" in datatype_lower or "lreal" in datatype_lower:
try:
f_val = float(value_str_unquoted)
s_val = "{:.7g}".format(f_val) # Notación científica si es necesario, precisión limitada
return s_val + (".0" if "." not in s_val and "e" not in s_val.lower() else "") # Añadir .0 si es entero
except ValueError: return value_str_unquoted if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted) else None # Permitir constante simbólica
elif "time" in datatype_lower: # Incluye TIME, LTIME, S5TIME
prefix, val_to_use = "", value_str_unquoted
# Extraer prefijo si ya existe (T#, LT#, S5T#)
match_prefix = re.match(r"^(T#|LT#|S5T#)(.*)", val_to_use, re.IGNORECASE)
if match_prefix: prefix, val_to_use = match_prefix.groups()
# Validar formato del valor de tiempo (simplificado)
if re.match(r'^-?(\d+d_)?(\d+h_)?(\d+m_)?(\d+s_)?(\d+ms)?$', val_to_use, re.IGNORECASE):
target_prefix = "S5T#" if "s5time" in datatype_lower else ("LT#" if "ltime" in datatype_lower else "T#")
return f"{target_prefix}{val_to_use}"
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
else: return None # Formato inválido
elif any(t in datatype_lower for t in ["date", "dtl", "dt", "tod", "time_of_day"]):
val_to_use = value_str_unquoted; prefix = ""
# Extraer prefijo si ya existe (DTL#, D#, DT#, TOD#)
match_prefix = re.match(r"^(DTL#|D#|DT#|TOD#)(.*)", val_to_use, re.IGNORECASE)
if match_prefix: prefix, val_to_use = match_prefix.groups()
# Determinar prefijo SCL correcto
target_prefix="DTL#" if "dtl" in datatype_lower or "date_and_time" in datatype_lower else ("DT#" if "dt" in datatype_lower else ("TOD#" if "tod" in datatype_lower or "time_of_day" in datatype_lower else "D#"))
# Validar formato (simplificado)
if re.match(r'^\d{4}-\d{2}-\d{2}(-\d{2}:\d{2}:\d{2}(\.\d+)?)?$', val_to_use) or re.match(r'^\d{2}:\d{2}:\d{2}(\.\d+)?$', val_to_use):
return f"{target_prefix}{val_to_use}"
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str_unquoted): return value_str_unquoted # Constante simbólica
else: return None # Formato inválido
else: # Otros tipos o desconocidos
return value_str if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', value_str) else None # Solo permitir constantes simbólicas
# <-- MODIFICADO: generate_scl_declarations -->
def generate_scl_declarations(variables, indent_level=1, project_root_dir=None):
"""
Genera líneas SCL para declarar variables, manejando UDTs, FBs (InstanceOfName),
Arrays y Structs.
"""
scl_lines = []
indent = " " * indent_level
# Lista de tipos básicos simples (en minúsculas) - ampliada
basic_types = {
"bool", "int", "dint", "sint", "usint", "uint", "udint", "lint", "ulint",
"byte", "word", "dword", "lword", "real", "lreal", "time", "ltime",
"s5time", "date", "dt", "dtl", "tod", "time_of_day", # TOD sinónimos
"char", "wchar", "variant",
# Tipos IEC comunes
"timer", "counter", "iec_timer", "iec_counter", "iec_sfc", "iec_ld_timer"
}
# Patrones para tipos básicos parametrizados (ignorando mayúsculas/minúsculas)
string_pattern = re.compile(r"^(W?STRING)(\[\s*\d+\s*\])?$", re.IGNORECASE)
array_pattern = re.compile(r'^(Array\[.*\]\s+of\s+)(.*)', re.IGNORECASE)
for var in variables:
var_name_scl = format_variable_name(var.get("name"))
var_dtype_raw = var.get("datatype", "VARIANT")
# <-- NUEVO: Obtener instance_of_name -->
instance_of_name = var.get("instance_of_name") # Puede ser None
# <-- FIN NUEVO -->
var_comment = var.get("comment")
start_value_raw = var.get("start_value")
children = var.get("children") # Para STRUCT anidados
array_elements = var.get("array_elements") # Para inicialización de ARRAY
declaration_dtype = var_dtype_raw # Tipo a usar en la declaración SCL
base_type_for_init = var_dtype_raw # Tipo base para formatear valor inicial
is_array = False
is_struct_inline = bool(children) # Es un STRUCT definido inline
is_potential_udt_or_fb = False # Flag para comprobar si buscar archivo .json
type_to_check = None # Nombre limpio del tipo a buscar (UDT o FB)
# --- Lógica Principal de Determinación de Tipo ---
if is_struct_inline:
# Si tiene hijos, se declara como STRUCT ... END_STRUCT
declaration_dtype = "STRUCT"
base_type_for_init = "STRUCT" # Valor inicial no aplica a STRUCT directamente
elif isinstance(var_dtype_raw, str):
# 1. Comprobar si es FB Instance usando InstanceOfName
if instance_of_name:
# Si InstanceOfName existe, usarlo como tipo (entre comillas)
declaration_dtype = f'"{instance_of_name}"'
base_type_for_init = instance_of_name # Usar nombre limpio para init/check
is_potential_udt_or_fb = True # Marcar para buscar archivo FB
type_to_check = instance_of_name
else:
# 2. No es FB Instance directo, comprobar si es Array
array_match = array_pattern.match(var_dtype_raw)
if array_match:
is_array = True
array_prefix_for_decl = array_match.group(1)
base_type_raw = array_match.group(2).strip()
base_type_for_init = base_type_raw # Tipo base para init/check
# Limpiar tipo base para comprobar si es básico/UDT/String
base_type_clean = base_type_raw[1:-1] if base_type_raw.startswith('"') and base_type_raw.endswith('"') else base_type_raw
base_type_lower = base_type_clean.lower()
# ¿El tipo base es UDT/FB conocido o un tipo básico/paramétrico?
if (base_type_lower not in basic_types and
not string_pattern.match(base_type_clean)):
# Asumir UDT/FB si no es básico ni String[N]/Char
declaration_dtype = f'{array_prefix_for_decl}"{base_type_clean}"' # Poner comillas
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
type_to_check = base_type_clean
else:
# Es básico o String[N]/Char
declaration_dtype = f'{array_prefix_for_decl}{base_type_raw}' # Usar como viene (puede tener comillas si era así)
else:
# 3. No es FB ni Array, ¿es UDT, String, Char o Básico?
base_type_clean = var_dtype_raw[1:-1] if var_dtype_raw.startswith('"') and var_dtype_raw.endswith('"') else var_dtype_raw
base_type_lower = base_type_clean.lower()
base_type_for_init = base_type_clean # Tipo base para init/check
if (base_type_lower not in basic_types and
not string_pattern.match(base_type_clean)):
# Asumir UDT/FB si no es básico ni String[N]/Char
declaration_dtype = f'"{base_type_clean}"' # Poner comillas
is_potential_udt_or_fb = True # Marcar para buscar archivo UDT/FB
type_to_check = base_type_clean
else:
# Es básico o String[N]/Char
declaration_dtype = var_dtype_raw # Usar como viene
# --- Búsqueda Opcional de Archivo de Definición (UDT o FB) ---
if is_potential_udt_or_fb and type_to_check and project_root_dir:
# Buscar tanto en 'PLC data types' como en 'Program blocks'
found_path = None
type_scl_name = format_variable_name(type_to_check)
possible_paths = [
os.path.join(project_root_dir, 'PLC data types', 'parsing', f'{type_scl_name}_processed.json'),
os.path.join(project_root_dir, 'Program blocks', 'parsing', f'{type_scl_name}_processed.json')
# Añadir más rutas si la estructura del proyecto varía
]
for path in possible_paths:
if os.path.exists(path):
found_path = path
break
if found_path:
print(f" INFO: Definición '{type_to_check}' localizada en: '{os.path.relpath(found_path, project_root_dir)}'")
else:
print(f" WARNING: No se encontró definición para '{type_to_check}'. Se buscó en directorios estándar.")
# --- Construir Línea de Declaración SCL ---
declaration_line = f"{indent}{var_name_scl} : {declaration_dtype}"
init_value_scl_part = ""
if is_struct_inline:
# Generar STRUCT anidado
scl_lines.append(declaration_line) # Añade "VarName : STRUCT"
# Llamada recursiva para los hijos
scl_lines.extend(generate_scl_declarations(children, indent_level + 1, project_root_dir))
scl_lines.append(f"{indent}END_STRUCT;")
# Añadir comentario al END_STRUCT si existe
if var_comment: scl_lines[-1] += f" // {var_comment}"
scl_lines.append("") # Línea en blanco después del struct
continue # Pasar a la siguiente variable del nivel actual
# --- Manejo de Valor Inicial (para no-STRUCTs) ---
init_value_scl = None
if is_array and array_elements:
# Inicialización de Array
init_values = []
try: # Intentar ordenar índices numéricamente
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: # Ordenar como strings si no son numéricos
print(f"Advertencia: Índices array no numéricos para '{var_name_scl}', ordenando como strings.")
sorted_indices_str = sorted(array_elements.keys())
for idx_str in sorted_indices_str:
val_info = array_elements[idx_str] # val_info puede ser dict o valor directo
# Formatear valor usando el tipo base del array
formatted_val = format_scl_start_value(val_info, base_type_for_init)
# Usar 'NULL' o comentario si el formateo falla o es complejo
init_values.append(formatted_val if formatted_val is not None else f"/* Array[{idx_str}] unsupported init */")
if init_values: init_value_scl = f"[{', '.join(init_values)}]"
elif not is_array and not is_struct_inline and start_value_raw is not None:
# Inicialización de variable simple
init_value_scl = format_scl_start_value(start_value_raw, base_type_for_init)
# Añadir parte del valor inicial si existe
if init_value_scl is not None:
init_value_scl_part = f" := {init_value_scl}"
# Combinar todo para la línea final
declaration_line += f"{init_value_scl_part};"
if var_comment: declaration_line += f" // {var_comment}"
scl_lines.append(declaration_line)
return scl_lines

View 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
}

View File

@ -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
}

View File

@ -0,0 +1,526 @@
# ToUpload/parsers/parse_stl.py
# -*- coding: utf-8 -*-
from lxml import etree
import traceback
import re # Needed for substitutions in get_access_text_stl
# Importar desde las utilidades del parser
# ns y get_multilingual_text son necesarios
from .parser_utils import ns, get_multilingual_text
# --- Funciones Auxiliares de Reconstrucción STL ---
def get_access_text_stl(access_element):
"""
Reconstruye una representación textual simple de un Access en STL.
Intenta manejar los diferentes tipos de acceso definidos en el XSD.
"""
if access_element is None:
return "_ERR_ACCESS_"
# --- Símbolo (Variable, Constante Simbólica) ---
# Busca <Symbol> dentro del <Access> usando el namespace stl
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 (puede estar en el Access o Componente, priorizar Componente)
has_quotes_comp = comp.get("HasQuotes", "false").lower() == "true"
has_quotes_access = False
access_parent = comp.xpath("ancestor::stl:Access[1]", namespaces=ns)
if access_parent:
has_quotes_attr = access_parent[0].xpath(
"./stl: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("#")
if i > 0:
parts.append(".") # Separador para estructuras
# Aplicar comillas si es necesario
if has_quotes or (
i == 0 and not is_temp and '"' not in name and "." not in name
):
# Añadir comillas si HasQuotes es true, o si es el primer componente,
# no es temporal, no tiene ya comillas, y no es parte de una DB (ej. DB10.DBX0.0)
parts.append(f'"{name}"')
else:
parts.append(name)
# Índices de Array (Access anidado dentro de Component)
index_access = comp.xpath("./stl:Access", namespaces=ns)
if index_access:
indices = [get_access_text_stl(ia) for ia in index_access]
# Limpiar índices (quitar saltos de línea, etc.)
indices_cleaned = [idx.replace("\n", "").strip() for idx in indices]
parts.append(f"[{','.join(indices_cleaned)}]")
return "".join(parts)
# --- Constante Literal ---
# Busca <Constant> dentro del <Access> usando el namespace stl
constant_elem = access_element.xpath("./stl:Constant", namespaces=ns)
if constant_elem:
# Obtener valor y tipo
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 == "ltime":
return f"LT#{const_val}" # Añadido LTIME
if const_type == "dtl":
return f"DTL#{const_val}" # Añadido DTL
# Strings y Chars (Escapar comillas simples internas)
if const_type == "string":
replaced_val = const_val.replace("'", "''")
return f"'{replaced_val}'"
if const_type == "char":
replaced_val = const_val.replace("'", "''")
return f"'{replaced_val}'"
if const_type == "wstring":
replaced_val = const_val.replace("'", "''")
return f"WSTRING#'{replaced_val}'"
if const_type == "wchar":
replaced_val = const_val.replace("'", "''")
return f"WCHAR#'{replaced_val}'"
# Tipos numéricos con prefijo opcional (Hexadecimal)
if const_val.startswith("16#"):
if const_type == "byte":
return f"B#{const_val}"
if const_type == "word":
return f"W#{const_val}"
if const_type == "dword":
return f"DW#{const_val}"
if const_type == "lword":
return f"LW#{const_val}" # Añadido LWORD
# Formato Real (añadir .0 si es necesario)
if (
const_type in ["real", "lreal"]
and "." not in const_val
and "e" not in const_val.lower()
):
# Verificar si es un número antes de añadir .0
try:
float(const_val) # Intenta convertir a float
return f"{const_val}.0"
except ValueError:
return const_val # No es número, devolver tal cual
# Otros tipos numéricos o desconocidos
return const_val # Valor por defecto
# --- Etiqueta (Label) ---
# Busca <Label> dentro del <Access> usando el namespace stl
label_elem = access_element.xpath("./stl:Label", namespaces=ns)
if label_elem:
return label_elem[0].get("Name", "_ERR_LABEL_")
# --- Acceso Indirecto (Punteros) ---
# Busca <Indirect> dentro del <Access> usando el namespace stl
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, Long
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",
} # Mapeo XSD a STL
width_char = width_map.get(
width, width[0] if width else "?"
) # Usar primera letra como fallback
# Área: DB, DI, L son comunes. Otras podrían necesitar mapeo.
area_char = (
area[0] if area else "?"
) # Usar primera letra (I, O, M, L, T, C, DB, DI...)
# Formato: AREAREG[puntero], ej. DBX[AR1,P#0.0] o LX[AR2,P#10.5]
return f"{area}{width_char}[{reg},{p_format_offset}]"
# --- Dirección Absoluta ---
# Busca <Address> dentro del <Access> usando el namespace stl
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")
# El tipo (Type) del Address define el ancho por defecto
addr_type_str = address_elem[0].get(
"Type", "Bool"
) # Bool, Byte, Word, DWord, Int, DInt, Real...
block_num_str = address_elem[0].get(
"BlockNumber"
) # Para DB10.DBX0.0 o DI5.DIW2
try:
bit_offset = int(bit_offset_str)
byte_offset = bit_offset // 8
bit_in_byte = bit_offset % 8
# Determinar ancho (X, B, W, D) basado en Type
addr_width = "X" # Default bit (Bool)
type_lower = addr_type_str.lower()
if type_lower in ["byte", "sint", "usint"]:
addr_width = "B"
elif type_lower in ["word", "int", "uint", "timer", "counter"]:
addr_width = "W" # T y C usan W para direccionamiento base
elif type_lower in [
"dword",
"dint",
"udint",
"real",
"time",
"dt",
"tod",
"date_and_time",
]:
addr_width = "D"
elif type_lower in [
"lreal",
"ltime",
"lword",
"lint",
"ulint",
"ltod",
"ldt",
"date_and_ltime",
]:
addr_width = "D" # Asumir que direccionamiento base usa D para 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) # Usar nombre XML si no está en el mapa
if stl_area in ["T", "C"]:
# Temporizadores y Contadores usan solo el número (offset de byte)
return f"{stl_area}{byte_offset}" # T 5, C 10
elif stl_area in ["DB", "DI"]:
block_num = (
block_num_str if block_num_str else ""
) # Número de bloque si existe
# Formato: DBNum.DBAnchoByte.Bit o DINum.DIAnchoByte.Bit o DBAnchoByte.Bit (si BlockNum es None)
db_prefix = f"{stl_area}{block_num}." if block_num else ""
return f"{db_prefix}{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
else: # I, Q, M, L, PI, PQ
# Formato: AreaAnchoByte.Bit (ej: M B 10 . 1 -> MB10.1 ; I W 0 . 0 -> IW0.0)
# Corrección: No añadir bit si el ancho no es X
if addr_width == "X":
return f"{stl_area}{addr_width}{byte_offset}.{bit_in_byte}"
else:
return f"{stl_area}{addr_width}{byte_offset}" # ej: MB10, IW0, QW4
except ValueError:
return f"{area}?{bit_offset_str}?" # Error de formato
# --- CallInfo (para operando de CALL) ---
# Busca <CallInfo> dentro del <Access> usando el namespace stl
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
# El operando de CALL depende del tipo de bloque
if btype == "FB":
# Para CALL FB, el operando es el DB de instancia
instance_node = call_info_elem[0].xpath(
".//stl:Component/@Name", namespaces=ns
) # Buscar nombre dentro de Instance/Component
if instance_node:
db_name_raw = instance_node[0]
# Añadir comillas si no las tiene
return f'"{db_name_raw}"' if '"' not in db_name_raw else db_name_raw
else:
return f'"_ERR_FB_INSTANCE_NAME_({name})_"'
else: # FC o desconocido
# Para CALL FC, el operando es el nombre del FC
# Añadir comillas si no las tiene
return f'"{name}"' if '"' not in name else name
# Fallback si no se reconoce el tipo de Access
scope = access_element.get("Scope", "UnknownScope")
return f"_{scope}_?"
def get_comment_text_stl(comment_element):
"""
Extrae texto de un LineComment o Comment para STL usando get_multilingual_text.
Se asume que get_multilingual_text ya está importado y maneja <Comment> y <LineComment>.
"""
return get_multilingual_text(comment_element) if comment_element is not None else ""
def reconstruct_stl_from_statementlist(statement_list_node):
"""
Reconstruye el código STL como una cadena de texto desde <StatementList>.
Usa las funciones auxiliares get_access_text_stl y get_comment_text_stl.
"""
if statement_list_node is None:
return "// Error: StatementList node not found.\n"
stl_lines = []
# Buscar todos los StlStatement hijos usando el namespace 'stl'
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 //)
# Buscar <Comment> o <LineComment> que sean hijos directos de StlStatement
# y NO tengan el atributo Inserted="true" (o no tengan Inserted)
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) # Usa la función auxiliar
if comment_text:
for comment_line in comment_text.splitlines():
stl_lines.append(
f"// {comment_line.strip()}"
) # Añadir como comentario SCL
# 2. Etiqueta (LabelDeclaration)
# Buscar <LabelDeclaration> hijo directo
label_decl = stmt.xpath("./stl:LabelDeclaration", namespaces=ns)
label_str = ""
if label_decl:
label_name_node = label_decl[0].xpath("./stl:Label/@Name", namespaces=ns)
if label_name_node:
label_str = f"{label_name_node[0]}:" # Añadir dos puntos
# Comentarios después de la etiqueta (inline) - Tienen Inserted="true"
label_comments = label_decl[0].xpath(
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
namespaces=ns,
)
for lcomm in label_comments:
inline_comment += f" // {get_comment_text_stl(lcomm).strip()}" # Acumular comentarios inline
if label_str:
line_parts.append(
label_str
) # Añadir etiqueta (si existe) a las partes de la línea
# 3. Instrucción (StlToken)
# Buscar <StlToken> hijo directo
instruction_token = stmt.xpath("./stl:StlToken", namespaces=ns)
instruction_str = ""
if instruction_token:
token_text = instruction_token[0].get("Text", "_ERR_TOKEN_")
# Manejar casos especiales definidos en el XSD
if token_text == "EMPTY_LINE":
if (
not stl_lines or stl_lines[-1]
): # Evitar múltiples líneas vacías seguidas
stl_lines.append("") # Añadir línea vacía
continue # Saltar resto del statement (no hay instrucción ni operando)
elif token_text == "COMMENT":
# Ya manejado por initial_comments. Si hubiera comentarios SÓLO aquí, se necesitaría extraerlos.
pass # Asumir manejado antes
elif token_text == "Assign":
instruction_str = "=" # Mapear Assign a '='
elif token_text == "OPEN_DB":
instruction_str = "AUF" # Mapear OPEN_DB a AUF
elif token_text == "OPEN_DI":
instruction_str = "AUF DI" # Mapear OPEN_DI a AUF DI
# Añadir más mapeos si son necesarios (ej. EQ_I a ==I)
else:
instruction_str = token_text # Usar el texto del token como instrucción
# Comentarios asociados al token (inline) - Tienen Inserted="true"
token_comments = instruction_token[0].xpath(
"child::stl:Comment[@Inserted='true'] | child::stl:LineComment[@Inserted='true']",
namespaces=ns,
)
for tcomm in token_comments:
inline_comment += f" // {get_comment_text_stl(tcomm).strip()}"
if instruction_str:
# Añadir tabulación si hubo etiqueta para alinear instrucciones
line_parts.append("\t" + instruction_str if label_str else instruction_str)
# 4. Operando (Access)
# Buscar <Access> hijo directo
access_elem = stmt.xpath("./stl:Access", namespaces=ns)
access_str = ""
if access_elem:
# Usar la función auxiliar para reconstruir el texto del operando
access_text = get_access_text_stl(access_elem[0])
access_str = access_text
# Comentarios asociados al Access (inline) - Tienen Inserted="true"
# Buscar DENTRO del Access
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).strip()}"
if access_str:
line_parts.append(access_str) # Añadir operando (si existe)
# Construir línea final si hay partes (etiqueta, instrucción u operando)
if line_parts:
# Unir partes con tabulación si hay más de una (etiqueta+instrucción o instrucción+operando)
# Ajustar espacios/tabulaciones para legibilidad
if len(line_parts) > 1:
# Caso Etiqueta + Instrucción + (Operando opcional)
if label_str and instruction_str:
current_line = f"{line_parts[0]:<8}\t{line_parts[1]}" # Etiqueta alineada, tab, instrucción
if access_str:
current_line += f"\t{line_parts[2]}" # Tab, operando
# Caso Instrucción + Operando (sin etiqueta)
elif instruction_str and access_str:
current_line = f"\t{line_parts[0]}\t{line_parts[1]}" # Tab, instrucción, tab, operando
# Caso solo Instrucción (sin etiqueta ni operando)
elif instruction_str:
current_line = f"\t{line_parts[0]}" # Tab, instrucción
else: # Otros casos (solo etiqueta, solo operando? improbable)
current_line = "\t".join(line_parts)
else: # Solo una parte (instrucción sin operando o solo etiqueta?)
current_line = line_parts[0] if label_str else f"\t{line_parts[0]}"
# Añadir comentario inline al final si existe, con tabulación
if inline_comment:
current_line += f"\t{inline_comment.strip()}"
# Añadir la línea construida si no está vacía
if current_line.strip():
stl_lines.append(current_line.rstrip()) # Quitar espacios finales
# Añadir BE al final si es necesario (lógica específica del bloque, no generalizable aquí)
# stl_lines.append("BE") # Ejemplo - QUITAR O ADAPTAR
return "\n".join(stl_lines)
# --- Función Principal del Parser STL (Corregida v4) ---
def parse_stl_network(network_element):
"""
Parsea una red STL extrayendo el código fuente reconstruido. (v4)
Devuelve un diccionario representando la red para el JSON.
"""
network_id = network_element.get("ID", "UnknownSTL_ID")
network_lang = "STL"
reconstructed_stl = "// STL extraction failed: Reason unknown.\n" # Default error
parsing_error_msg = None
network_title = f"Network {network_id}" # Default title
network_comment = "" # Default comment
try:
# Buscar NetworkSource usando local-name()
network_source_node_list = network_element.xpath(
".//*[local-name()='NetworkSource']"
)
statement_list_node = None
if network_source_node_list:
network_source_node = network_source_node_list[0]
# Buscar StatementList dentro del NetworkSource encontrado, usando local-name()
statement_list_node_list = network_source_node.xpath(
".//*[local-name()='StatementList']"
)
if statement_list_node_list:
statement_list_node = statement_list_node_list[0]
else:
parsing_error_msg = "StatementList node not found inside NetworkSource."
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
else:
parsing_error_msg = "NetworkSource node not found using local-name()."
print(f"Advertencia: {parsing_error_msg} (Red ID={network_id})")
# Intentar reconstruir SOLO si encontramos el nodo StatementList
if statement_list_node is not None:
# La función reconstruct_stl_from_statementlist debe estar definida arriba
reconstructed_stl = reconstruct_stl_from_statementlist(statement_list_node)
elif parsing_error_msg:
reconstructed_stl = f"// STL extraction failed: {parsing_error_msg}\n"
except Exception as e_parse:
parsing_error_msg = f"Exception during STL network parsing: {e_parse}"
print(f" ERROR parseando Red {network_id} (STL): {parsing_error_msg}")
traceback.print_exc()
reconstructed_stl = f"// ERROR durante el parseo de STL: {e_parse}\n"
# Crear la estructura de datos para la red
parsed_network_data = {
"id": network_id,
"language": network_lang,
"title": network_title,
"comment": network_comment,
"logic": [
{
"instruction_uid": f"STL_{network_id}",
"type": "RAW_STL_CHUNK",
"stl": reconstructed_stl,
}
],
}
if parsing_error_msg:
parsed_network_data["error"] = f"Parser failed: {parsing_error_msg}"
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"], "parser_func": parse_stl_network}

View File

@ -0,0 +1,467 @@
# ToUpload/parsers/parser_utils.py
# -*- coding: utf-8 -*-
from lxml import etree
import traceback
# Definición de 'ns' (asegúrate de que esté definida correctamente en tu archivo)
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",
# Añade otros namespaces si son necesarios
}
# --- Funciones Comunes de Extracción de Texto y Nodos ---
def get_multilingual_text(element, default_lang="en-US-it-IT", fallback_lang=None):
"""
Extrae texto multilingüe de un elemento XML. (v5.2 - DEBUG + XPath ObjectList)
"""
# print(f"--- DEBUG get_multilingual_text v5.2: Iniciando para elemento {element.tag if element is not None else 'None'}, default='{default_lang}' ---")
if element is None: return ""
combined_texts = []
languages_to_try = []
# --- Lógica Combinada ---
is_combined_mode = default_lang and '-' in default_lang and len(default_lang.split('-')) >= 2
if is_combined_mode:
# print(f"--- DEBUG v5.2: Detectado modo combinado: '{default_lang}' ---")
parts = default_lang.split('-')
target_langs = []
if len(parts) % 2 == 0:
for i in range(0, len(parts), 2): target_langs.append(f"{parts[i]}-{parts[i+1]}")
else: target_langs = []
if target_langs:
# print(f"--- DEBUG v5.2: Culturas combinadas a buscar: {target_langs} ---")
try:
for lang in target_langs:
# --- CORRECCIÓN XPath v5.2: Añadir ObjectList ---
xpath_find_item = f"./ObjectList/MultilingualTextItem[AttributeList/Culture='{lang}']"
found_items = element.xpath(xpath_find_item, namespaces=ns)
# print(f" DEBUG Combinado v5.2: Items encontrados para '{lang}': {len(found_items)}")
if found_items:
xpath_get_text = "./AttributeList/Text/text()"
text_nodes = found_items[0].xpath(xpath_get_text, namespaces=ns)
# print(f" DEBUG Combinado v5.2: Nodos de texto encontrados: {len(text_nodes)}")
if text_nodes:
text_content = text_nodes[0].strip()
# print(f" DEBUG Combinado v5.2: Texto encontrado para '{lang}': '{text_content[:50]}...'")
if text_content: combined_texts.append(text_content)
# --- FIN CORRECCIÓN XPath v5.2 ---
if combined_texts:
# print(f"--- DEBUG v5.2: Modo combinado retornando: '{' - '.join(combined_texts)}' ---")
return " - ".join(combined_texts)
else:
# print(f"--- DEBUG v5.2: Modo combinado no encontró textos. Intentando fallback... ---")
default_lang = None
except Exception as e_comb:
print(f" Advertencia: Error procesando modo combinado '{default_lang}': {e_comb}")
default_lang = None
else: default_lang = None
# --- Fin Lógica Combinada ---
# --- Lógica Normal / Fallback ---
# print("--- DEBUG v5.2: Iniciando lógica Normal/Fallback ---")
if default_lang: languages_to_try.append(default_lang)
if fallback_lang: languages_to_try.append(fallback_lang)
# print(f" DEBUG v5.2: Idiomas específicos a probar: {languages_to_try}")
try:
if languages_to_try:
for lang in languages_to_try:
# --- CORRECCIÓN XPath v5.2: Añadir ObjectList ---
xpath_find_item = f"./ObjectList/MultilingualTextItem[AttributeList/Culture='{lang}']"
found_items = element.xpath(xpath_find_item, namespaces=ns)
# print(f" DEBUG Fallback v5.2: Items encontrados para '{lang}': {len(found_items)}")
if found_items:
xpath_get_text = "./AttributeList/Text/text()"
text_nodes = found_items[0].xpath(xpath_get_text, namespaces=ns)
# print(f" DEBUG Fallback v5.2: Nodos de texto encontrados: {len(text_nodes)}")
if text_nodes:
text_content = text_nodes[0].strip()
# print(f" DEBUG Fallback v5.2: Texto encontrado para '{lang}': '{text_content[:50]}...'")
if text_content:
# print(f"--- DEBUG v5.2: Fallback retornando texto de '{lang}' ---")
return text_content
# --- FIN CORRECCIÓN XPath v5.2 ---
# Fallback final: buscar cualquier texto no vacío
# print(" DEBUG v5.2: Probando cualquier idioma con texto no vacío...")
xpath_any_text = "./ObjectList/MultilingualTextItem/AttributeList/Text/text()" # .// busca en cualquier nivel
all_text_nodes = element.xpath(xpath_any_text, namespaces=ns)
# print(f" DEBUG Fallback Any v5.2: Nodos de texto encontrados: {len(all_text_nodes)}")
for text_content_raw in all_text_nodes:
text_content = text_content_raw.strip()
if text_content:
# print(f"--- DEBUG v5.2: Fallback 'Any' retornando texto: '{text_content}' ---")
return text_content
# print("--- DEBUG v5.2: No se encontró ningún texto no vacío. Retornando '' ---")
return ""
except Exception as e:
# print(f"--- DEBUG v5.2: EXCEPCIÓN en lógica Normal/Fallback: {e} ---")
# traceback.print_exc()
return ""
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, incluyendo InstanceOfName."""
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")
# <-- NUEVO: Obtener InstanceOfName -->
member_instance_of = member.get("InstanceOfName") # Puede ser None
# <-- FIN NUEVO -->
if not member_name or not member_dtype_raw:
print("Advertencia: Miembro sin nombre o tipo de dato. Saltando.")
continue
# Combinar tipo y versión si existe versión
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": {},
}
# <-- NUEVO: Añadir instance_of_name si existe -->
if member_instance_of:
member_info["instance_of_name"] = member_instance_of
# <-- FIN NUEVO -->
# Extraer comentario
comment_node = member.xpath("./iface:Comment", namespaces=ns)
if comment_node:
member_info["comment"] = get_multilingual_text(comment_node[0])
# Extraer valor inicial
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
)
)
# 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)
# Procesar 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") # Índice del array, ej "0", "1"
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
)
)
# Guardar valor y comentario del subelemento
sub_comment_node = sub.xpath("./iface:Comment", namespaces=ns)
sub_comment_text = (
get_multilingual_text(sub_comment_node[0])
if sub_comment_node
else None
)
if sub_comment_text:
member_info["array_elements"][path] = {
"value": value,
"comment": sub_comment_text,
}
else:
member_info["array_elements"][path] = {
"value": value
} # Guardar como dict simple si no hay comment
members_data.append(member_info)
return members_data

Some files were not shown because too many files have changed in this diff Show More