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 dataclasses import dataclass
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
import difflib 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 # Forzar UTF-8 en la salida estándar
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
@ -257,7 +262,8 @@ class CSharpCodeMerger:
return ''.join(diff) return ''.join(diff)
def main(): 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", ".") working_directory = configs.get("working_directory", ".")
work_config = configs.get("level3", {}) 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.markdown_handler import cargar_cronologia_existente
from utils.beautify import BeautifyProcessor from utils.beautify import BeautifyProcessor
import json 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 # Forzar UTF-8 en la salida estándar
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
@ -29,7 +34,8 @@ def generar_indice(mensajes):
def main(): def main():
# Cargar configuraciones del entorno # 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 # Obtener working directory
working_directory = configs.get("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