12 de febrero de 2016

Arduino y Processing 2: Lluvia controlada por puerto serie

En una entrada anterior habíamos visto cómo comunicarnos de una forma muy básica desde nuestra Arduino con Processing a través del puerto serie. Por si deseáis revisarla, podéis encontrarla en el siguiente enlace: Arduino y Processing 1: Comunicación por puerto serie.

Esta vez, vamos a ver cómo "dibujar" lluvia en Processing y controlar la dirección en la que cae con un potenciómetro en Arduino.

Para hacernos una idea de cómo debe funcionar, veamos el siguiente vídeo:


Aunque la calidad de imagen de la animación de Processing no ha sido la mejor, nos sirve perfectamente para entender qué deseamos hacer.

Lo primero, será preparar nuestra Arduino. Necesitamos un potenciómetro, una LED y una resistencia de 330 Ohm (o la que creáis conveniente) para la LED. El montaje que he realizado es el siguiente:

Imagen realizada con Fritzing
En realidad, la LED sólo la utilizo para ver que los datos del potenciómetro se ven correctamente, ya que la intensidad de la LED variará según el valor obtenido. Por supuesto, toda la parte relativa a la LED podría eliminarse del código, lo dejo a vuestra elección.

A continuación, veamos el código que he utilizado en la Arduino:
#define PIN_POT A0       // pin de potenciómetro (analógico) 
#define PIN_LED 9        // pin de la LED (analógico)

int valor_POT = 0;       // para guardar el valor del potenciómetro
int brillo = 0;          // brillo de la LED


void setup() {
  pinMode(PIN_POT, INPUT);  // configuro pin del potenciómetro como Entrada
  pinMode(PIN_LED, OUTPUT); // configuro pin de la LED como Salida
  Serial.begin(9600);       // inicializo puerto serie
}

void loop() {
  valor_POT = analogRead(PIN_POT);             // obtengo el valor del potenciómetro (0 a 1023)
  brillo = map(valor_POT, 0, 1023, 0, 255);    // calculo el valor del brillo de la LED
  analogWrite(PIN_LED, brillo);                // cambio el brillo de la LED
  Serial.println(valor_POT);                   // envío el valor por el puerto serie
  delay(100);                                  // pequeña pausa para no saturar el puerto
}

Si cargamos el programa a nuestra Arduino, podremos ver como el brillo de la LED aumenta y disminuye según la posición en la que pongamos el potenciómetro.

Ahora que ya tenemos preparada la Arduino, vamos con Processing.

Necesitaremos dos archivos. Uno será el que gestione la animación y la comunicación por el puerto serie y, el otro, lo usaremos para crear la clase Gota, que gestionará el movimiento de una gota y también la dibujará.


Empezamos primero por la clase Gota. Este es el código que he utilizado:

class Gota {
  float x;        // Coordenada horizontal
  float y;        // Coordenada vertical
  float longitud; // Longitud de la gota
  float theta;    // Ángulo de la gota (que variará según lo obtenido por el puerto serie)
  int velocidad;  // Píxels que aumento en cada movimiento
  
  Gota(){
    x = random(width);   // Valor horizontal aleatorio, para que salga de cualquier sitio
    y = 0;               // Las gotas caen de la parte superior
    longitud = 10;       // El largo de la gota
    theta = radians(60); // Ángulo en radianes
    velocidad = 16;      // Para controlar la velocidad a la que cae la gota
  }
  
  /**
   * @param l Longitud de la gota
   * @param a Ángulo de la gota en grados
   */
  Gota(float l, float a){
    this();              // Llamo al constructor sin parámetros, para no repetir...
    longitud = l;        // Longitud que he recibido como parámetro
    theta = radians(a);  // Ángulo que he recibido como parámetro, en radianes
  }
  
  /**
   * Desplaza la gota hacia abajo y la dibuja
   */
  void caer(){
    pushMatrix();        // guardo posición actual del "lienzo"
    x = x % width;       // si la gota llega al borde, aparece por el otro
    y = y % height;      // si la gota llega al fondo, aparece por arriba
    if(y == 0)           // si la gota está arriba de todo...
       setX(random(width)); // ...obtengo una posición horizontal aleatoria
    if(x <= 0)           // si la gota se ha ido por el borde izquierdo...
      setX(width);       // ...la coloco a la derecha del todo
    translate(x, y);     // colocamos el lienzo en la posición deseada
    rotate(-theta);      // giramos lienzo los grados que queremos
    line(0, 0, 0, longitud); // dibujamos en el lienzo una línea con el largo deseado
    x += sin(theta) * velocidad; // desplazamos la gota proporcionalmente al ángulo
    y += cos(theta) * velocidad; // desplazamos la gota proporcionalmente al ángulo
    popMatrix();         // vuelvo al estado inicial del lienzo    
  }
  
  /**
   * Establece el valor de la coordenada horizontal de la gota
   * @param nuevoX La nueva coordenada horizontal
   */
   void setX(float nuevoX){
     x = nuevoX;
   }
   
  /**
   * Establece ángulo de la gota en radianes
   * @param newAngle El nuevo ángulo en grados
   */
   void setAngle(float newAngle){
     theta = radians(newAngle);
   }
}

Ahora que ya tenemos nuestra clase Gota creada, vamos con el programa principal. La base de comunicación, la habíamos visto ya en una entrada anterior. Simplemente hemos añadido lo necesario para gestionar el dato recibido: Aquí está el código necesario:

import processing.serial.*;   // Para usar el puerto serie

ArrayList<Gota> lluvia;       // Para el conjunto de gotas
static int num_gotas = 1500;  // Para limitar el número máximo de gotas
float theta = 45;             // Ángulo
float longitud = 15;          // Largo de la gota 
Serial puerto;                // Puerto serie
String dato, dato_AUX;        // Dato recibido por el puerto serie

void setup(){
   /* Configuración de ventana */
  size(800, 600);             // Tamaño de ventana 
  background(0);              // Color de fondo
  frameRate(60);              // Fotogramas por segundo 
  
  /* Configuración de pintura */
  stroke(#A0DBFF);            // Cambiamos el color del lápiz a un tono azul 
  
  /* Inicialización de variables */
  lluvia = new ArrayList<Gota>();
  dato = "1";              
  dato_AUX = "1"; 
  
  /* Configuración de puerto serie */
  String nombrePuerto = Serial.list()[0];
  println("Puerto establecido a " + nombrePuerto + "."); // Mostrar el puerto elegido
  puerto = new Serial(this, nombrePuerto, 9600);
}

void draw(){
  background(0);              // Pintamos color de fondo
  
  if(puerto.available() > 0){            // si el puerto está disponible...
    dato = puerto.readStringUntil('\n'); //...leo hasta encontrar un salto de línea
  }
  if(dato == null){                      // Si he recibido un dato nulo...
    dato = dato_AUX;                     // ...utilizo el anterior
  } else {                               // Si el dato NO es nulo...
    if(trim(dato).equals(dato_AUX) == false){ // ...y es distinto del anterior...
      println("Ángulo: " + map((float) int(dato_AUX), 0, 1023, -90, 90));
      dato_AUX = trim(dato);             // Guardo el dato borrando los espacios en blanco
    }    
  }
  
  /* Ángulo */
  theta = map((float) int(dato_AUX), 0, 1023, -90, 90); // obtengo el ángulo (de -90º a 90º)
  
  if(lluvia.size() < num_gotas && random(10) < 1) // Si hay sitio para más gotas...
    lluvia.add(new Gota(longitud, theta));        // ...añado una nueva
  
  for(Gota gota : lluvia){ // Reviso todas las gotas del conjunto
    gota.setAngle(theta);  // Establezco el ángulo en que deben orientarse
    gota.caer();           // Las desplazo y dibujo
  }
}


Además de comprobar que no excedamos el número de gotas, también he añadido una condición que hará que no se creen todas al mismo tiempo, si no que caigan aleatoriamente. Es decir, si hay sitio para más gotas, obtengo un número aleatorio entre 0 y 9 y, si ha salido cero, entonces sí creo la gota... y si no, pues no la creo. Por eso el último "if" utiliza esa segunda condición. Si no fuera así, caerían las gotas todas al mismo tiempo y el efecto sería completamente diferente:

  if(lluvia.size() < num_gotas && random(10) < 1)
    lluvia.add(new Gota(longitud, theta));

A continuación, podéis ver una imagen del resultado final:



Os recomiendo variar varios parámetros del código para ver cómo afecta la velocidad, longitud de la gota, número de gotas... incluso los fotogramas por segundo.

En la clase Gota, en el método caer, podéis cambiar line() por ellipse(), por ejemplo, y ver cómo caen pequeños círculos en vez de líneas (aunque así no apreciaréis el ángulo). Lo divertido está en experimentar.

Espero que os haya gustado.

Un saludo: Roi.