torres.electronico
Well-known-Alfil
Bueno, acá de nuevo con un nuevo proyecto que de seguro, cuando este terminado a muchos de la vieja escuela que nunca aprendieron a programar microcontroladores pero si saben sobre automatización con lógica de contactos, o bien entienden el lenguaje de escalera (ladder), les va a servir mucho para hacer sus propios automatismos de bajo coste...
Estoy haciendo un curso de Python online enfocado a todo lo referido al diseño de GUI profesionales. Como es libre, ya prácticamente me comí todos los apuntes y algunos videos de referencia, mas otros tantos que busque para satisfacer mi sed. Estoy entrando en el punto de hacer un ejercicio, y se me ocurrio hacer una mini aplicacion que nos permita programar un Arduino Nano en lenguaje LADDER... Como ya tengo un hardware armado hace ya un tiempo, estoy orientando este software a una especie de microPLC que tiene las siguientes caracteristicas:
El software si bien en esta primera etapa beta tiene un set pequeño de funciones, creo que son las mas básicas e indispensables para arrancar:
Contacto NA - Contacto NC - Bobina NC - Bobina NA - PWM - ADC - Comparadores "Igual que"/"Mayor que"/Menor que" - Bobina de salida - Bobina de memoria - Memoria 0 al 9 - Variable 0 al 9 - Timer Ton - Timer Toff - Funciones lógicas "AND"/"OR"/"NOT"
Cuando arranque con la idea de hacer este ejemplo a modo practica, sinceramente no tenia ni la mas pálida idea en que me estaba por meter
... una cosa es expectativa, y otra realidad
... Pero bueno, asi como me meti en esto, estoy seguro que muchos de ustedes me van a dar una mano y me van a ir corrigiendo / enseñando como salir de algunas trabas que me tildan de a ratos por un largo tiempito... En definitiva, esto es algo para ustedes, para la comunidad en general...
Bien; les comento a modo resumen rápido el flujo de dependencias, asi nos metemos de lleno en el bolonqui que tengo en la cabeza para poder cocinar esto:
1_Python / PyQt5: Nos permite arrastrar bloques y vea la GUI en gris oscuro con líneas guía.
2_Bloques PNG: Se arrastran, se colocan en la escena, y Python sabe la posición X/Y.
3_Python generate_sketch(): Recorre los bloques, detecta filas/columnas, traduce a código Arduino.
4_ os y open(): Guardan el .ino en la carpeta build.
5_ arduino-cli: Si queremos automatizar la compilación y carga al Arduino, se integra acá.
Como no encontre un banco de imágenes PNG que tuviera las imágenes de las lógicas de contacto y funciones, tuve que dibujar las 38 funciones

las primeras GUI no daba pie con bola

Hasta que al fin, luego de varias horas de lectura, interpretación a los golpes con pruebas y error, logre llegar a dar el primer paso... Tener la GUI funcional

Cómo podemos detectar la lógica?
Para generar Ladder real, necesitamos un paso intermedio de análisis, o por lo menos, yo lo creo así:
A_Leer todas las posiciones X/Y de los bloques para determinar:
Bloques que están en la misma línea → se consideran “en serie” (AND implícito).
Bloques que están alineados verticalmente con otra entrada OR → se consideran “en paralelo” (OR implícito).
B_Clasificar cada bloque por tipo (Contacto, Bobina, Timer, Comparación, etc.).
C_Construir un árbol o grafo de conexiones:
Nodo = bloque.
Aristas = flujo de energía / señal.
D_Recorrer el árbol para generar código Arduino respetando las conexiones.
Cómo puedo traducir el diagrama de contactos al lenguaje de Arduino?
Para un OR simple como el ejemplo del video:
tendría que generar algo así:
Si tuviera un AND, sería:
Para poder iniciar con este pensamiento, necesito estructurarme algunas reglas basicas:
-Los bloques no tienen que estar solo en la misma fila; necesitamos mirar la posición vertical y horizontal para detectar paralelos (OR) y series (AND).
-La bobina siempre es la salida final de la línea o rama lógica.
-Para Timer, PWM, Comparaciones, etc., se usa la misma idea: reconocer entradas, aplicar función, escribir salida...
Con estas reglas básicas, para poder generar Ladder/Arduino, lo que haemos entonces en el programa es recorrer todos los bloques y agruparlos por fila (cada línea del Ladder). Detectadas las series y paralelos, pasamos a agruparlos por:
-Bloques alineados horizontalmente = serie (AND).
-Bloques alineados verticalmente = paralelo (OR).
-Asignar variables Arduino a entradas, salidas y memorias.
Esto permitirá que si pones por ejemplo 2 contactos normal abierto en paralelo y conectas a una bobina, el .ino contenga un verdadero digitalRead(PIN) || digitalRead(PIN) y luego el digitalWrite a la bobina. Suena tonto la idea que tengo en mente y que estoy tratando de explicar, asi que ténganme paciencia...
En la versión que vemos arriba, la GUI simplemente recorre todos los bloques en orden de fila y columna, y genera un comentario por cada bloque:
Si bien esto no tiene lógica de conexiones todavía, lo que estoy haciendo es tratar de hacer que la GUI entienda qué bloques de función están en qué fila y columna.
En las ultimas versiones que fui creando, pude ir sumando ya los bloques lógicos que tengo terminados:
y llegue a esto:
Algunas dependencias son problemáticas si no respetamos cierto orden de ejecución... Tengo que pasar en limpio algunas cosas y voy hacer mas hincapié en algunos detalles a tener en cuenta si quieren hacer sus propis aplicaciones o editar esta misma.
Dependencias para la GUI / Drag & Drop
- PyQt5 (La librería principal para la interfaz gráfica. Maneja ventanas, layouts, listas, botones, gráficos, drag & drop, escenas y objetos gráficos. Todo lo que ves en la pantalla está hecho con PyQt5)
- os (Funciones de sistema operativo, usadas para crear carpetas (build), verificar rutas y guardar archivos .ino)
- sys (Necesario para ejecutar la aplicación (sys.argv) y salir correctamente)
-shutil (Para copiar archivos, si en algún momento queremos copiar los .ino o librerías a otra carpeta)
Dependencias para arrastrar, soltar y manejar imágenes... Mi gran dolor de cabeza!!!
- QPixmap / QDrag / QMimeData ( Representan los bloques de funciones como imágenes, permiten arrastrarlas desde el panel izquierdo y soltarlas en el área de programación)
- QGraphicsScene / QGraphicsView (Área donde se colocan las funciones. Permite dibujar líneas guía, colocar bloques, moverlos y luego obtener posiciones X/Y para generar el .ino)
- QPen / QColor (Para dibujar las líneas guía claras que delimitan cada fila en la zona de programación)
Dependencias para generación de archivos .ino
- os.path / os.makedirs / open() (Crear la carpeta build, abrir y escribir archivos .ino)
- sorted / lambda (Ordenar los bloques por fila y columna para que el código Arduino siga el orden lógico)
Herramienta externa
- arduino-cli_1.4.0_Windows_64bit (Opcional pero recomendable por que permite compilar y cargar los .ino desde Python o línea de comando)
Ultimas pruebas (6/1/2025)
En la ultimas pruebas, logre que funcione una función OR con dos entradas y una salida:
Una función AND con dos entradas y una salida:
Pero fallo implementando una AND y una OR para activar una salida:
O sea, el diagrama de contactos seria algo así:
La sintaxis correcta y/o la respuesta esperada tendría que ser:
Como veran, tengo para rato, pero voy a ir editando y subiendo los avances, como así también me gustaria que sea un proyecto colaborativo y el que quiera sumar/mejorar, bienvenido sea su aporte... Mas allá de que esto queda acá, tambien me sirve mucho para aprender, leer sus aportes / sugerencias

PD: Proxima sumo links de referencia
Les comparto todos los archivos. no se olviden de instalar todas las dependencias, incluido arduino-cli_1.4.0
Estoy haciendo un curso de Python online enfocado a todo lo referido al diseño de GUI profesionales. Como es libre, ya prácticamente me comí todos los apuntes y algunos videos de referencia, mas otros tantos que busque para satisfacer mi sed. Estoy entrando en el punto de hacer un ejercicio, y se me ocurrio hacer una mini aplicacion que nos permita programar un Arduino Nano en lenguaje LADDER... Como ya tengo un hardware armado hace ya un tiempo, estoy orientando este software a una especie de microPLC que tiene las siguientes caracteristicas:
| 7 ENTRADAS DIGITALES | D2 (Con opción de activar interrupción)-D4-D5-D6-D7-D8-D9 |
| 5 SALIDAS DIGITALES | D3 (Con opción de activar la función PWM)-D14-D15-D16-D17 |
| 2 ENTRADAS ANALOGICAS | D20(A6)-D21(A7) |
| COMUNICACION | I2c/SPI/UART |
El software si bien en esta primera etapa beta tiene un set pequeño de funciones, creo que son las mas básicas e indispensables para arrancar:
Contacto NA - Contacto NC - Bobina NC - Bobina NA - PWM - ADC - Comparadores "Igual que"/"Mayor que"/Menor que" - Bobina de salida - Bobina de memoria - Memoria 0 al 9 - Variable 0 al 9 - Timer Ton - Timer Toff - Funciones lógicas "AND"/"OR"/"NOT"
Cuando arranque con la idea de hacer este ejemplo a modo practica, sinceramente no tenia ni la mas pálida idea en que me estaba por meter
... una cosa es expectativa, y otra realidad
... Pero bueno, asi como me meti en esto, estoy seguro que muchos de ustedes me van a dar una mano y me van a ir corrigiendo / enseñando como salir de algunas trabas que me tildan de a ratos por un largo tiempito... En definitiva, esto es algo para ustedes, para la comunidad en general...Bien; les comento a modo resumen rápido el flujo de dependencias, asi nos metemos de lleno en el bolonqui que tengo en la cabeza para poder cocinar esto:
1_Python / PyQt5: Nos permite arrastrar bloques y vea la GUI en gris oscuro con líneas guía.
2_Bloques PNG: Se arrastran, se colocan en la escena, y Python sabe la posición X/Y.
3_Python generate_sketch(): Recorre los bloques, detecta filas/columnas, traduce a código Arduino.
4_ os y open(): Guardan el .ino en la carpeta build.
5_ arduino-cli: Si queremos automatizar la compilación y carga al Arduino, se integra acá.
Como no encontre un banco de imágenes PNG que tuviera las imágenes de las lógicas de contacto y funciones, tuve que dibujar las 38 funciones

las primeras GUI no daba pie con bola

Hasta que al fin, luego de varias horas de lectura, interpretación a los golpes con pruebas y error, logre llegar a dar el primer paso... Tener la GUI funcional

Cómo podemos detectar la lógica?
Para generar Ladder real, necesitamos un paso intermedio de análisis, o por lo menos, yo lo creo así:
A_Leer todas las posiciones X/Y de los bloques para determinar:
Bloques que están en la misma línea → se consideran “en serie” (AND implícito).
Bloques que están alineados verticalmente con otra entrada OR → se consideran “en paralelo” (OR implícito).
B_Clasificar cada bloque por tipo (Contacto, Bobina, Timer, Comparación, etc.).
C_Construir un árbol o grafo de conexiones:
Nodo = bloque.
Aristas = flujo de energía / señal.
D_Recorrer el árbol para generar código Arduino respetando las conexiones.
Cómo puedo traducir el diagrama de contactos al lenguaje de Arduino?
Para un OR simple como el ejemplo del video:
Código:
Entrada1 ----┐
├-- OR ----> Bobina
Entrada2 ----┘
tendría que generar algo así:
CSS:
int in1 = digitalRead(PIN1);
int in2 = digitalRead(PIN2);
int salida = in1 || in2; // OR lógico
digitalWrite(PIN_OUT, salida);
Si tuviera un AND, sería:
CSS:
int salida = in1 && in2; // AND lógico
Para poder iniciar con este pensamiento, necesito estructurarme algunas reglas basicas:
-Los bloques no tienen que estar solo en la misma fila; necesitamos mirar la posición vertical y horizontal para detectar paralelos (OR) y series (AND).
-La bobina siempre es la salida final de la línea o rama lógica.
-Para Timer, PWM, Comparaciones, etc., se usa la misma idea: reconocer entradas, aplicar función, escribir salida...
Con estas reglas básicas, para poder generar Ladder/Arduino, lo que haemos entonces en el programa es recorrer todos los bloques y agruparlos por fila (cada línea del Ladder). Detectadas las series y paralelos, pasamos a agruparlos por:
-Bloques alineados horizontalmente = serie (AND).
-Bloques alineados verticalmente = paralelo (OR).
-Asignar variables Arduino a entradas, salidas y memorias.
Esto permitirá que si pones por ejemplo 2 contactos normal abierto en paralelo y conectas a una bobina, el .ino contenga un verdadero digitalRead(PIN) || digitalRead(PIN) y luego el digitalWrite a la bobina. Suena tonto la idea que tengo en mente y que estoy tratando de explicar, asi que ténganme paciencia...
Python:
# main.py
# uPLC - ETI Patagonia - prof.martintorres@educ.ar
import sys, os
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QListWidget, QListWidgetItem,
QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QAction
)
from PyQt5.QtCore import Qt, QMimeData, QPointF
from PyQt5.QtGui import QPixmap, QDrag, QPen, QColor
MAX_COLS = 8
BLOCK_WIDTH = 100
BLOCK_HEIGHT = 52
BLOCK_SPACING = 10
# ---------------- Clase GraphicsView ----------------
class GraphicsView(QGraphicsView):
def __init__(self, scene):
super().__init__(scene)
self.setAcceptDrops(True)
self.row_counter = 0
self.col_counter = 0
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("application/x-item"):
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-item"):
event.acceptProposedAction()
def dropEvent(self, event):
mime = event.mimeData()
data = str(mime.data("application/x-item"), "utf-8")
file_name = data
pixmap = QPixmap(f"icons/{file_name}")
if pixmap.isNull():
print(f"No se encontró {file_name}")
return
col_index = self.col_counter
row_index = self.row_counter
x = col_index * (BLOCK_WIDTH + BLOCK_SPACING)
y = row_index * (BLOCK_HEIGHT + BLOCK_SPACING)
pixmap_item = self.scene().addPixmap(pixmap)
pixmap_item.setFlags(pixmap_item.ItemIsMovable | pixmap_item.ItemIsSelectable)
pixmap_item.setPos(x, y)
pixmap_item.block_file = file_name
pixmap_item.row_index = row_index
pixmap_item.col_index = col_index
pixmap_item.mouseReleaseEvent = lambda e, item=pixmap_item: self.snap_item(item, e)
# OR automático
if file_name == "Fun_OR_A.png":
pixmap_b = QPixmap("icons/Fun_OR_B.png")
pixmap_item_b = self.scene().addPixmap(pixmap_b)
pixmap_item_b.setFlags(pixmap_item_b.ItemIsMovable | pixmap_item_b.ItemIsSelectable)
x_b = x
y_b = y + BLOCK_HEIGHT // 2
pixmap_item_b.setPos(x_b, y_b)
pixmap_item_b.block_file = "Fun_OR_B.png"
pixmap_item_b.row_index = row_index
pixmap_item_b.col_index = col_index
pixmap_item_b.mouseReleaseEvent = lambda e, item=pixmap_item_b: self.snap_item(item, e)
self.col_counter += 1
if self.col_counter >= MAX_COLS:
self.col_counter = 0
self.row_counter += 1
event.acceptProposedAction()
def snap_item(self, item, event):
pos = item.pos()
x = round(pos.x() / (BLOCK_WIDTH + BLOCK_SPACING)) * (BLOCK_WIDTH + BLOCK_SPACING)
y = round(pos.y() / (BLOCK_HEIGHT + BLOCK_SPACING)) * (BLOCK_HEIGHT + BLOCK_SPACING)
item.setPos(QPointF(x, y))
item.row_index = y // (BLOCK_HEIGHT + BLOCK_SPACING)
item.col_index = x // (BLOCK_WIDTH + BLOCK_SPACING)
# ---------------- Ventana principal ----------------
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("uPLC LADDER - ETI Patagonia")
self.setGeometry(100, 100, 1000, 600)
self.setStyleSheet("background-color: #2f2f2f;")
# ---------------- Menú superior ----------------
menubar = self.menuBar()
menu_file = menubar.addMenu("Archivo")
generate_action = QAction("Generar SKETCH", self)
generate_action.triggered.connect(self.generate_sketch)
menu_file.addAction(generate_action)
# ---------------- Layout principal ----------------
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout()
central_widget.setLayout(main_layout)
# ---------------- Panel izquierdo (1/6) ----------------
self.function_list = QListWidget()
self.function_list.setFixedWidth(160)
self.function_list.setStyleSheet(
"QListWidget {background-color: #3c3c3c; color: white; font-weight: bold;}"
"QListWidget::item:selected {background-color: #5c5c5c;}"
)
self.populate_functions()
self.function_list.setDragEnabled(True)
self.function_list.startDrag = self.startDrag
left_layout = QVBoxLayout()
left_layout.addWidget(self.function_list)
left_layout.addStretch()
left_container = QWidget()
left_container.setLayout(left_layout)
# ---------------- Área de programación (5/6) ----------------
self.scene = QGraphicsScene()
self.scene.setBackgroundBrush(Qt.darkGray)
self.view = GraphicsView(self.scene)
self.view.setStyleSheet("border: 1px solid #555;")
pen = QPen(QColor(200, 200, 200, 50))
for i in range(0, 1000, BLOCK_HEIGHT):
self.scene.addLine(0, i, 5000, i, pen)
main_layout.addWidget(left_container)
main_layout.addWidget(self.view)
# ---------------- Funciones panel izquierdo ----------------
def populate_functions(self):
functions = [
("Contacto NO", "In_NA.png"),
("Contacto NC", "In_NC.png"),
("Lectura Analogica", "In_ADC.png"),
("Bobina NO", "Out_NA.png"),
("Bobina NC", "Out_NC.png"),
("Bobina Memoria", "Fun_GUARDARenMEMORIA.png"),
("PWM", "Fun_PWM.png"),
("Timer TON", "Fun_Ton.png"),
("Timer TOF", "Fun_Toff.png"),
("Comparación IGUAL QUE", "Fun_varIGUALvar.png"),
("Comparación MENOR QUE", "Fun_varMENORvar.png"),
("Comparación MAYOR QUE", "Fun_varMAYORvar.png"),
("Memoria0", "Fun_MEMORIA0.png"),
("Memoria1", "Fun_MEMORIA1.png"),
("Memoria2", "Fun_MEMORIA2.png"),
("Memoria3", "Fun_MEMORIA3.png"),
("Memoria4", "Fun_MEMORIA4.png"),
("Memoria5", "Fun_MEMORIA5.png"),
("Memoria6", "Fun_MEMORIA6.png"),
("Memoria7", "Fun_MEMORIA7.png"),
("Memoria8", "Fun_MEMORIA8.png"),
("Memoria9", "Fun_MEMORIA9.png"),
("Variable0", "Fun_VARIABLE0.png"),
("Variable1", "Fun_VARIABLE1.png"),
("Variable2", "Fun_VARIABLE2.png"),
("Variable3", "Fun_VARIABLE3.png"),
("Variable4", "Fun_VARIABLE4.png"),
("Variable5", "Fun_VARIABLE5.png"),
("Variable6", "Fun_VARIABLE6.png"),
("Variable7", "Fun_VARIABLE7.png"),
("Variable8", "Fun_VARIABLE8.png"),
("Variable9", "Fun_VARIABLE9.png"),
("AND", "Fun_AND.png"),
("NOT", "Fun_NOT.png"),
("OR", "Fun_OR_A.png"),
("Linea horizontal", "LINEA.png")
]
for name, file in functions:
item = QListWidgetItem(name)
item.setData(Qt.UserRole, file)
self.function_list.addItem(item)
# ---------------- Drag correcto ----------------
def startDrag(self, supportedActions):
item = self.function_list.currentItem()
if not item:
return
drag = QDrag(self.function_list)
mime = QMimeData()
mime.setData("application/x-item", bytes(item.data(Qt.UserRole), "utf-8"))
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
# ---------------- Generar Sketch real ----------------
def generate_sketch(self):
if not os.path.exists("build"):
os.makedirs("build")
file_path = os.path.join("build", "program.ino")
# Ordenamos los bloques por fila y columna
items = sorted(
[i for i in self.scene.items() if hasattr(i, "block_file")],
key=lambda b: (b.row_index, b.col_index)
)
with open(file_path, "w") as f:
f.write("// ==== Programa Ladder generado automáticamente ====\n\n")
f.write("void setup() {\n // Configurar pines\n}\n\n")
f.write("void loop() {\n")
for item in items:
code_line = self.block_to_code(item.block_file)
f.write(f" {code_line}\n")
f.write("}\n")
print(f"Sketch generado: {file_path}")
# ---------------- Traducción bloque -> código Arduino ----------------
def block_to_code(self, block_file):
# Contactos
if block_file == "In_NA.png":
return "// Contacto NO (digitalRead entrada)"
elif block_file == "In_NC.png":
return "// Contacto NC (digitalRead entrada negada)"
elif block_file == "In_ADC.png":
return "// Lectura analogica (analogRead)"
# Bobinas
elif block_file == "Out_NA.png":
return "// Bobina NO (digitalWrite salida)"
elif block_file == "Out_NC.png":
return "// Bobina NC (digitalWrite salida negada)"
elif block_file == "Fun_GUARDARenMEMORIA.png":
return "// Bobina memoria"
elif block_file == "Fun_PWM.png":
return "// PWM analogWrite"
# Timers
elif block_file == "Fun_Ton.png":
return "// Timer TON"
elif block_file == "Fun_Toff.png":
return "// Timer TOF"
# Comparaciones
elif block_file == "Fun_varIGUALvar.png":
return "// Comparacion IGUAL"
elif block_file == "Fun_varMENORvar.png":
return "// Comparacion MENOR"
elif block_file == "Fun_varMAYORvar.png":
return "// Comparacion MAYOR"
# Memorias
elif "Fun_MEMORIA" in block_file:
return f"// Memoria {block_file[-5]}"
# Variables
elif "Fun_VARIABLE" in block_file:
return f"// Variable {block_file[-5]}"
# Logica
elif block_file == "Fun_AND.png":
return "// AND"
elif block_file == "Fun_NOT.png":
return "// NOT"
elif block_file == "Fun_OR_A.png":
return "// OR"
elif block_file == "Fun_OR_B.png":
return "// OR parte inferior"
elif block_file == "LINEA.png":
return "// Linea horizontal"
return f"// Bloque desconocido: {block_file}"
# -------------------- Main --------------------
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
En la versión que vemos arriba, la GUI simplemente recorre todos los bloques en orden de fila y columna, y genera un comentario por cada bloque:
CSS:
// ==== Programa Ladder generado automaticamente ====
void setup() {
// Configurar pines
}
void loop() {
// Contacto NO (digitalRead entrada)
// Contacto NO (digitalRead entrada)
// OR parte inferior
// OR
// Bobina NO (digitalWrite salida)
}
Si bien esto no tiene lógica de conexiones todavía, lo que estoy haciendo es tratar de hacer que la GUI entienda qué bloques de función están en qué fila y columna.
En las ultimas versiones que fui creando, pude ir sumando ya los bloques lógicos que tengo terminados:
Python:
# main.py
# uPLC - ETI Patagonia - prof.martintorres@educ.ar
import sys, os
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QListWidget, QListWidgetItem,
QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QAction
)
from PyQt5.QtCore import Qt, QMimeData, QPointF
from PyQt5.QtGui import QPixmap, QDrag, QPen, QColor
MAX_COLS = 8
BLOCK_WIDTH = 100
BLOCK_HEIGHT = 52
BLOCK_SPACING = 10
# ---------------- Clase GraphicsView ----------------
class GraphicsView(QGraphicsView):
def __init__(self, scene):
super().__init__(scene)
self.setAcceptDrops(True)
self.row_counter = 0
self.col_counter = 0
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("application/x-item"):
event.acceptProposedAction()
def dragMoveEvent(self, event):
if event.mimeData().hasFormat("application/x-item"):
event.acceptProposedAction()
def dropEvent(self, event):
mime = event.mimeData()
file_name = str(mime.data("application/x-item"), "utf-8")
pixmap = QPixmap(f"icons/{file_name}")
if pixmap.isNull():
print(f"No se encontró {file_name}")
return
col_index = self.col_counter
row_index = self.row_counter
x = col_index * (BLOCK_WIDTH + BLOCK_SPACING)
y = row_index * (BLOCK_HEIGHT + BLOCK_SPACING)
pixmap_item = self.scene().addPixmap(pixmap)
pixmap_item.setFlags(pixmap_item.ItemIsMovable | pixmap_item.ItemIsSelectable)
pixmap_item.setPos(x, y)
pixmap_item.block_file = file_name
pixmap_item.row_index = row_index
pixmap_item.col_index = col_index
pixmap_item.mouseReleaseEvent = lambda e, item=pixmap_item: self.snap_item(item, e)
# OR automático
if file_name == "Fun_OR_A.png":
pixmap_b = QPixmap("icons/Fun_OR_B.png")
pixmap_item_b = self.scene().addPixmap(pixmap_b)
pixmap_item_b.setFlags(pixmap_item_b.ItemIsMovable | pixmap_item_b.ItemIsSelectable)
x_b = x
y_b = y + BLOCK_HEIGHT // 2
pixmap_item_b.setPos(x_b, y_b)
pixmap_item_b.block_file = "Fun_OR_B.png"
pixmap_item_b.row_index = row_index
pixmap_item_b.col_index = col_index
pixmap_item_b.mouseReleaseEvent = lambda e, item=pixmap_item_b: self.snap_item(item, e)
self.col_counter += 1
if self.col_counter >= MAX_COLS:
self.col_counter = 0
self.row_counter += 1
event.acceptProposedAction()
def snap_item(self, item, event):
pos = item.pos()
x = round(pos.x() / (BLOCK_WIDTH + BLOCK_SPACING)) * (BLOCK_WIDTH + BLOCK_SPACING)
y = round(pos.y() / (BLOCK_HEIGHT + BLOCK_SPACING)) * (BLOCK_HEIGHT + BLOCK_SPACING)
item.setPos(QPointF(x, y))
item.row_index = y // (BLOCK_HEIGHT + BLOCK_SPACING)
item.col_index = x // (BLOCK_WIDTH + BLOCK_SPACING)
# ---------------- Ventana principal ----------------
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("uPLC LADDER - ETI Patagonia")
self.setGeometry(100, 100, 1200, 700)
self.setStyleSheet("background-color: #2f2f2f;")
# Menú superior
menubar = self.menuBar()
menu_file = menubar.addMenu("Archivo")
generate_action = QAction("Generar SKETCH", self)
generate_action.triggered.connect(self.generate_sketch)
menu_file.addAction(generate_action)
# Layout principal
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout()
central_widget.setLayout(main_layout)
# Panel izquierdo (1/6)
self.function_list = QListWidget()
self.function_list.setFixedWidth(160)
self.function_list.setStyleSheet(
"QListWidget {background-color: #3c3c3c; color: white; font-weight: bold;}"
"QListWidget::item:selected {background-color: #5c5c5c;}"
)
self.populate_functions()
self.function_list.setDragEnabled(True)
self.function_list.startDrag = self.startDrag
left_layout = QVBoxLayout()
left_layout.addWidget(self.function_list)
left_layout.addStretch()
left_container = QWidget()
left_container.setLayout(left_layout)
# Área de programación (5/6)
self.scene = QGraphicsScene()
self.scene.setBackgroundBrush(Qt.darkGray)
self.view = GraphicsView(self.scene)
self.view.setStyleSheet("border: 1px solid #555;")
# Líneas guía
pen = QPen(QColor(200, 200, 200, 50))
for i in range(0, 1000, BLOCK_HEIGHT):
self.scene.addLine(0, i, 5000, i, pen)
main_layout.addWidget(left_container)
main_layout.addWidget(self.view)
def populate_functions(self):
functions = [
("Contacto NO", "In_NA.png"),
("Contacto NC", "In_NC.png"),
("Lectura Analogica", "In_ADC.png"),
("Bobina NO", "Out_NA.png"),
("Bobina NC", "Out_NC.png"),
("Bobina Memoria", "Fun_GUARDARenMEMORIA.png"),
("PWM", "Fun_PWM.png"),
("Timer TON", "Fun_Ton.png"),
("Timer TOF", "Fun_Toff.png"),
("Comparación IGUAL QUE", "Fun_varIGUALvar.png"),
("Comparación MENOR QUE", "Fun_varMENORvar.png"),
("Comparación MAYOR QUE", "Fun_varMAYORvar.png"),
("Memoria0", "Fun_MEMORIA0.png"),
("Memoria1", "Fun_MEMORIA1.png"),
("Memoria2", "Fun_MEMORIA2.png"),
("Memoria3", "Fun_MEMORIA3.png"),
("Memoria4", "Fun_MEMORIA4.png"),
("Memoria5", "Fun_MEMORIA5.png"),
("Memoria6", "Fun_MEMORIA6.png"),
("Memoria7", "Fun_MEMORIA7.png"),
("Memoria8", "Fun_MEMORIA8.png"),
("Memoria9", "Fun_MEMORIA9.png"),
("Variable0", "Fun_VARIABLE0.png"),
("Variable1", "Fun_VARIABLE1.png"),
("Variable2", "Fun_VARIABLE2.png"),
("Variable3", "Fun_VARIABLE3.png"),
("Variable4", "Fun_VARIABLE4.png"),
("Variable5", "Fun_VARIABLE5.png"),
("Variable6", "Fun_VARIABLE6.png"),
("Variable7", "Fun_VARIABLE7.png"),
("Variable8", "Fun_VARIABLE8.png"),
("Variable9", "Fun_VARIABLE9.png"),
("AND", "Fun_AND.png"),
("NOT", "Fun_NOT.png"),
("OR", "Fun_OR_A.png"),
("LINEA", "LINEA.png")
]
for name, file in functions:
item = QListWidgetItem(name)
item.setData(Qt.UserRole, file)
self.function_list.addItem(item)
def startDrag(self, supportedActions):
item = self.function_list.currentItem()
if not item:
return
drag = QDrag(self.function_list)
mime = QMimeData()
mime.setData("application/x-item", bytes(item.data(Qt.UserRole), "utf-8"))
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
# ---------------- Generar sketch con AND+OR final ----------------
def generate_sketch(self):
if not os.path.exists("build"):
os.makedirs("build")
file_path = os.path.join("build", "program.ino")
items = sorted(
[i for i in self.scene.items() if hasattr(i, "block_file")],
key=lambda b: (b.row_index, b.col_index)
)
# Organizar por fila
rows = {}
for item in items:
rows.setdefault(item.row_index, []).append(item)
# Generar .ino
with open(file_path, "w") as f:
f.write("// ==== Programa Ladder generado automáticamente ====\n")
f.write("#include <Arduino.h>\n\n")
f.write("void setup() {\n // Configurar pines\n}\n\n")
f.write("void loop() {\n")
temp_counter = 0
row_vars = {}
# Primer paso: generar AND por fila
for row in sorted(rows.keys()):
line_blocks = rows[row]
expr, var_name, temp_counter = self.process_line_and_or(line_blocks, rows, row, temp_counter)
row_vars[row] = var_name
f.write(f" int {var_name} = {expr};\n")
# Salida final
for row in sorted(rows.keys(), reverse=True):
for block in rows[row]:
if block.block_file in ["Out_NA.png", "Out_NC.png", "Fun_GUARDARenMEMORIA.png", "Fun_PWM.png"]:
f.write(f" digitalWrite(PIN_OUT, {row_vars[row]});\n")
break
else:
continue
break
f.write("}\n")
print(f"Sketch funcional generado: {file_path}")
# ---------------- Construye expresión AND + OR ----------------
def process_line_and_or(self, blocks, rows, row_index, temp_counter):
and_inputs = []
or_inputs = []
# AND horizontal
for block in blocks:
bf = block.block_file
if bf in ["In_NA.png", "In_NC.png", "In_ADC.png"]:
and_inputs.append(self.input_to_code(bf))
and_expr = " && ".join(and_inputs) if and_inputs else "0"
var_name = f"val_temp{temp_counter}"
temp_counter += 1
# OR vertical (buscar Fun_OR_A y fila siguiente)
for block in blocks:
if block.block_file == "Fun_OR_A.png":
next_row = row_index + 1
if next_row in rows:
or_inputs.append(var_name)
for b2 in rows[next_row]:
if b2.block_file in ["In_NA.png", "In_NC.png", "In_ADC.png"]:
or_inputs.append(self.input_to_code(b2.block_file))
or_expr = " || ".join(or_inputs)
var_name2 = f"val_temp{temp_counter}"
temp_counter += 1
return or_expr, var_name2, temp_counter
return and_expr, var_name, temp_counter
def input_to_code(self, block_file):
if block_file == "In_NA.png":
return "digitalRead(PIN1)"
elif block_file == "In_NC.png":
return "!digitalRead(PIN2)"
elif block_file == "In_ADC.png":
return "analogRead(ADC0)"
return "0"
# ---------------- Main ----------------
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
y llegue a esto:
Algunas dependencias son problemáticas si no respetamos cierto orden de ejecución... Tengo que pasar en limpio algunas cosas y voy hacer mas hincapié en algunos detalles a tener en cuenta si quieren hacer sus propis aplicaciones o editar esta misma.
Dependencias para la GUI / Drag & Drop
- PyQt5 (La librería principal para la interfaz gráfica. Maneja ventanas, layouts, listas, botones, gráficos, drag & drop, escenas y objetos gráficos. Todo lo que ves en la pantalla está hecho con PyQt5)
- os (Funciones de sistema operativo, usadas para crear carpetas (build), verificar rutas y guardar archivos .ino)
- sys (Necesario para ejecutar la aplicación (sys.argv) y salir correctamente)
-shutil (Para copiar archivos, si en algún momento queremos copiar los .ino o librerías a otra carpeta)
Dependencias para arrastrar, soltar y manejar imágenes... Mi gran dolor de cabeza!!!
- QPixmap / QDrag / QMimeData ( Representan los bloques de funciones como imágenes, permiten arrastrarlas desde el panel izquierdo y soltarlas en el área de programación)
- QGraphicsScene / QGraphicsView (Área donde se colocan las funciones. Permite dibujar líneas guía, colocar bloques, moverlos y luego obtener posiciones X/Y para generar el .ino)
- QPen / QColor (Para dibujar las líneas guía claras que delimitan cada fila en la zona de programación)
Dependencias para generación de archivos .ino
- os.path / os.makedirs / open() (Crear la carpeta build, abrir y escribir archivos .ino)
- sorted / lambda (Ordenar los bloques por fila y columna para que el código Arduino siga el orden lógico)
Herramienta externa
- arduino-cli_1.4.0_Windows_64bit (Opcional pero recomendable por que permite compilar y cargar los .ino desde Python o línea de comando)
Ultimas pruebas (6/1/2025)
En la ultimas pruebas, logre que funcione una función OR con dos entradas y una salida:
CSS:
// ==== Programa Ladder generado autom ticamente ====
#include <Arduino.h>
void setup()
{ // Configurar pines }
void loop()
{ int val_temp0 = digitalRead(PIN1) || digitalRead(PIN1);
digitalWrite(PIN_OUT, val_temp0); }
Una función AND con dos entradas y una salida:
CSS:
// ==== Programa Ladder generado automáticamente ====
#include <Arduino.h>
void setup() { // Configurar pines }
void loop()
{ int val_temp0 = digitalRead(PIN1) && digitalRead(PIN1);
digitalWrite(PIN_OUT, val_temp0); }
Pero fallo implementando una AND y una OR para activar una salida:
CSS:
// ==== Programa Ladder generado automáticamente ====
#include <Arduino.h>
void setup() { // Configurar pines }
void loop()
{ int val_temp0 = digitalRead(PIN1) || digitalRead(PIN1) || digitalRead(PIN1);
digitalWrite(PIN_OUT, val_temp0); }
O sea, el diagrama de contactos seria algo así:
CSS:
Fila 0: Contacto NO1 -- AND -- Contacto NO2 \
OR --> Bobina
Fila 1: Contacto NO3 ----------------------/
La sintaxis correcta y/o la respuesta esperada tendría que ser:
CSS:
int val_temp0 = digitalRead(PIN1) && digitalRead(PIN2);
int val_temp1 = digitalRead(PIN3);
int val_or = val_temp0 || val_temp1;
digitalWrite(PIN_OUT, val_or);
Como veran, tengo para rato, pero voy a ir editando y subiendo los avances, como así también me gustaria que sea un proyecto colaborativo y el que quiera sumar/mejorar, bienvenido sea su aporte... Mas allá de que esto queda acá, tambien me sirve mucho para aprender, leer sus aportes / sugerencias
PD: Proxima sumo links de referencia
Les comparto todos los archivos. no se olviden de instalar todas las dependencias, incluido arduino-cli_1.4.0
Adjuntos
Última edición:

Termino de corregir HORRORES y arranco con las dudas/consultas