Pong parte 2
En la entrega anterior construimos todo el software necesario para poder jugar a Pong, sin embargo lo que no llegamos a implementar fue el cómo íbamos a usar Arduino para controlar a cada jugador. Bien pues ha llegado ese momento. Pero si por casualidad has llegado aquí y aun no has visto la primera parte te la dejo por aquí.
Programando pong con Arduino parte 1
Continuando con la lógica de Arduino lo primero que debemos definir es que tipo de dispositivo de entrada usaremos para controlar a cada jugador. podríamos usar botones, pero la experiencia sería muy similar a la que ya teníamos con el teclado, también podríamos usar potenciómetros pero al tener un límites físicos, siento que corta un poco la experiencia del jugador. por ello he decidido ir con un encoder.
Habiendo decidido el tipo de entrada exploraremos como funciona un encoder.
Para ello nos apoyaremos en la imagen a continuación.
Vamos a suponer que tenemos cuatro baldosas y dos de ellas tienen escondidas en su interior un interruptor, en la primera fase todo esta estático y por lo tanto no tenemos lectura en nuestro interruptor.
Ahora imaginemos que una persona caminará de izquierda a derecha en nuestra baldosa, lo que hará que los interruptores se accionen en la secuencia que se ve en los apartados 2, 3 y 4.

según el orden en el que se activen los interruptores podemos determinar en que sentido caminaba la persona. En la figura tenemos que primero se activa el de la la izquierda y luego el de la derecha. lo que nos indica que la persona iba de izquierda a derecha. Pero si la secuencia de activación hubiese sido al revés y primero se hubiese activado el interruptor de la derecha y luego el de la izquierda. en ese caso sabríamos que la persona caminaba de derecha a izquierda.
El modo de funcionamiento del encoder es muy similar sin embargo hay una pequeña diferencia y es que en su secuencia de activación hay ocaciones donde, bajo la rotación del eje ambos interruptores están activados o desactivados. Esto es debido a que en una fracción de tiempo sus señales están solapadas. Este comportamiento lo podemos ver mejor en la siguiente gráfica.
Diagrama de tiempo de las señales de un encoder

Podemos ver claramente que una de las señales empieza después de la otra, pero hay puntos en las que tienen el mismo estado como es en el periodo t1 a t2 donde ambas señales están en 1 (HIGH).
¿pero por que esto es así?
Esto nos ayuda a tener un control más fino al momento de analizar las señales del encoder. Veamos la siguiente gráfica para entenderlo mejor.
| tiempo | pin A | pin B |
|---|---|---|
| t0 | 1 | 0 |
| t1 | 0 | 1 |
| t0 | 1 | 0 |
| t1 | 0 | 1 |
| tiempo | pin A | pin B |
|---|---|---|
| t0 | 0 | 0 |
| t1 | 1 | 0 |
| t2 | 1 | 1 |
| t3 | 0 | 1 |
como podemos ver en la tabla anterior si el encoder no tuviera ese solapamiento tendríamos dos estados en la secuencia de activación
10 y 01
pero si lo comparamos con la version que si tiene solapamiento tenemos 4 estados
00, 10, 11 y 01
esta mayor resolución nos ayuda a tener una lectura más precisa y a su vez reducir los errores en la lectura.
Pasemos ahora a la implementación en arduino.
Implementación de la lectura de un encoder en arduino
const int intA = 10;
const int intB = 9;
const int reset = 8;
const int MID = 100;
int pos = MID;
int intARead = 1;
int intBRead = 1;
int resetRead = 1;
int previntA = 1;
int previntB = 1;
void setup() {
// put your setup code here, to run once:
pinMode(intA, INPUT);
pinMode(intB, INPUT);
pinMode(reset, INPUT);
Serial.begin(9600);
}
void loop() {
delayMicroseconds(50);
intARead = digitalRead(intA);
intBRead = digitalRead(intB);
resetRead = digitalRead(reset);
if(resetRead == LOW){
pos = MID;
}
if(
(previntA == 0 && previntB == 1 && intARead == 0 && intBRead == 0)
|(previntA == 0 && previntB == 0 && intARead == 1 && intBRead == 0)
|(previntA == 1 && previntB == 0 && intARead == 1 && intBRead == 1)
|(previntA == 1 && previntB == 1 && intARead == 0 && intBRead == 1)
) {
pos++;
}
if(
(previntA == 0 && previntB == 0 && intARead == 0 && intBRead == 1)
|(previntA == 0 && previntB == 1 && intARead == 1 && intBRead == 1)
|(previntA == 1 && previntB == 1 && intARead == 1 && intBRead == 0)
|(previntA == 1 && previntB == 0 && intARead == 0 && intBRead == 0)
) {
pos--;
}
Serial.println(pos);
previntA = intARead;
previntB = intBRead;
}
Como podemos observar los dos condicionales lo que hacen es validar la secuencia de activación que teniamos definida en la tabla anterior y según sea esa secuencia de activacion determinaremos si el encoder aumenta o disminuye.
una representación de los if en pseudocódigo sería la siguiente.
si
t3 pasa a t0
o t0 pasa a t1
o t1 pasa a t2
o t2 pasa a t3
entonces
incrementa
si
t0 pasa a t3
o t3 pasa a t2
o t2 pasa a t1
o t1 pasa a t0
entonces
decrementa
de este modo es mucho más evidente ver que lo que se hace es evaluar la secuencia si va de t0 hacia t3 o de t3 hacia t0.
Bien, ya lo tenemos casi listo para conectar arduino y procesing, solo haremos unos pequeños ajustes de ambos lados.
Por el lado de Arduino agregaremos unos límites para el incremento y el decremento. Esto es para que la paleta que golpeará la pelota no se salga de la región visible.
Implementación de límites en Arduino
const int intA = 10;
const int intB = 9;
const int reset = 8;
const int MID = 100;
const int TOP = 100;
const int BOTTOM = 0;
int pos = MID;
int intARead = 1;
int intBRead = 1;
int resetRead = 1;
int previntA = 1;
int previntB = 1;
void setup() {
pinMode(intA, INPUT);
pinMode(intB, INPUT);
pinMode(reset, INPUT);
Serial.begin(9600);
}
void loop() {
delayMicroseconds(50);
intARead = digitalRead(intA);
intBRead = digitalRead(intB);
resetRead = digitalRead(reset);
if(resetRead == LOW){
pos = MID;
}
if(
((previntA == 0 && previntB == 1 && intARead == 0 && intBRead == 0)
|(previntA == 0 && previntB == 0 && intARead == 1 && intBRead == 0)
|(previntA == 1 && previntB == 0 && intARead == 1 && intBRead == 1)
|(previntA == 1 && previntB == 1 && intARead == 0 && intBRead == 1))
&& pos <= TOP
) {
pos++;
}
if(
((previntA == 0 && previntB == 0 && intARead == 0 && intBRead == 1)
|(previntA == 0 && previntB == 1 && intARead == 1 && intBRead == 1)
|(previntA == 1 && previntB == 1 && intARead == 1 && intBRead == 0)
|(previntA == 1 && previntB == 0 && intARead == 0 && intBRead == 0))
&& pos >= BOTTOM
) {
pos--;
}
Serial.println(pos);
previntA = intARead;
previntB = intBRead;
}
Aquí lo que hemos hecho ha sido agregar dos nuevas variables "TOP" y "BOTTOM" que nos útiles para definir los límites, de tal modo que solo podrá incrementar hasta el valor definido en top y solo podrá disminuir hasta el valor definido en bottom.
Lo siguiente que haremos será recibir el dato en Processing y transformarlo de modo que podamos usarlo para mover la paleta.
import processing.serial.*;
Serial myPort;
int TOP = 60;
int BOTTOM = 0;
void setup()
{
String portName = Serial.list()[2];
for (int i=0; i < Serial.list().length; i++) {
print("[", i, "] ");
println(Serial.list()[i]);
};
myPort = new Serial(this, portName, 9600);
}
void serialEvent(Serial p){
String incomming = p.readStringUntil('\n');
if (incomming != null) {
incomming = trim(incomming); // removes \r and spaces
int value = int(incomming);
float y = map(value, BOTTOM, TOP, 0, height);
pad1.updatePosition(y);
}
}
Los cambios que debemos hacer en Processing se dividen en 3.
Importacion y definicion de variables: aquí nos importaremos la librería de comunicación serial y definiremos las constantes TOP y BOTTOM que deben corresponder a los mismos valores que hemos definido en el lado de Arduino.
Inicialización: en la función setup vamos a configurar la comunicación. Un trúco útil es imprimir todos los dispositivos seriales junto a su identificador en la lísta para saber cual corresponde al Arduino (es importante destacar que antes de ejecutar el código de Processing debemos tener el Arduino programado y conectado al ordenador) en mi caso la lista de dispositivos seriales se ve así:
[ 0 ] /dev/cu.Bluetooth-Incoming-Port
[ 1 ] /dev/cu.debug-console
[ 2 ] /dev/cu.usbmodem312301
[ 3 ] /dev/tty.Bluetooth-Incoming-Port
[ 4 ] /dev/tty.debug-console
[ 5 ] /dev/tty.usbmodem312301
Yo sé que mi Arduino aparece como /dev/cu.usbmodem312301 y es que cuando debo seleccionar que board voy a programar en el IDE de Arduino me muestra este valor.
Finalmente en la versión anterior de Processing teníamos una función llamada keyPressed, Al cambiar el método de entrada por el Arduino ya no la necesitaremos por lo que la eliminaremos del código y en su lugar agregaremos la función serialEvent.
Esta función leerá los valores hasta que se encuentre el carácter \n este carácter lo agrega automáticamente Arduino cada vez que ejecuta la función Serial.println. Despúes límpiará los datos que hemos recibido eliminando espacios en blanco o carácteres de retroceso \r. Una vez que ya se tiene el texto límpio será transformado a un valor númerico. Para posteriormente aplicarle la funcion map esta funcíon lo que hace es tomar un rango de valores de entrada como es 0 - 60 y transformarlo a un rango de salida 0 - height aquí height corresponde al alto de la ventana que hemos definido previamente en el setup la cual fué de 400px. Finalmente este valor se lo pasamos a la funcion updatePosition del objeto Pad para cambiar así su posicion en la ventana.
Demostración del funcionamiento
Diagrama de conexión

Podríamos dar por terminado este tutoríal hasta este punto. sin embargo si quieres saber como integrar nuestro segundo jugador te invito a continuar leyendo
Añadiendo el segundo jugador
Para tener un segundo Jugador lo que tenemos que hacer es duplicar ciertas partes del código, en Ardunio por ejemplo debemos añadir una segunda entrada para el otro encoder y enviar los datos obtenidos por el puerto seríal separados por "coma"
#include <ChineseEncoder.h>
ChineseEncoder encoder1(8,9,10);
ChineseEncoder encoder2(11,12,13);
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
encoder1.setup();
encoder2.setup();
encoder1.setRange(0, 60);
encoder2.setRange(0, 60);
}
void loop() {
encoder1.read();
encoder2.read();
Serial.print(encoder1.getPosition());
Serial.print(",");
Serial.println(encoder2.getPosition());
delayMicroseconds(50);
}
Gracias a la mágia de la programación este código se ve mucho más corto que la primera versión, pero lo que ha sucedido aquí es que el código que habíamos definido previamente para la lectura de un encoder lo he extraído a una librería ChineseEncoder.h ella se encarga de configurar los puertos como entrada, leer los valores del encoder y actualizar la posición consecuentemente.
Para el envio de los datos vemos que hay una nueva función Serial.print lo que la diferencia a su version Serial.println es que no agrega el carácter \n al final del mesaje. de modo que lo que enviamos por el puerto seríal es un texto similar a "20,50\r\n".
void serialEvent(Serial p){
String incomming = p.readStringUntil('\n');
if (incomming != null) {
incomming = trim(incomming);
int[] value = int(incomming.split(","));
float y1 = map(value[0], BOTTOM, TOP, 0, height);
float y2 = map(value[1], BOTTOM, TOP, 0, height);
pad1.updatePosition(y1);
pad2.updatePosition(y2);
}
}
En el lado de Processing debemos ajustar la lectura serial para recibir dos valores, sabemos que por el puerto seríal enviamos los dos valores separados por , lo que necesitamos es extraerlos del cuerpo del mensaje. Afortunamdamente muchos lenguajes de programacion ya hacen esto y Processing no es la excepcion gracias a la función split podemos separar el texto y luego almacenarlo en un "array" una vez tenemos los datos en el array solo nos falta pasarlos por la función map según sus indices y el resultado lo pasamos a cada pad. Hecho esto ya tendríamos completo el proyecto.
Nota: hay que configurar la comunicación serial a 115200 baudos para tener una experiencia fluida
Dos jugadores en PONG
Con este video demostrando el funcionamiento me despido de ustedes. Muchas gracias por haber llegado hasta el final y nos veremos prontamente en una nueva publicacion.