15 de febrero de 2016

Arduino y Processing 3: Mostrar los datos de dos sensores al mismo tiempo

Previamente, hemos visto enviar información desde Arduino a Procesing de una forma muy básica y, posteriormente, habíamos aprovechado la comunicación por puerto serie probando a controlar la dirección de caída de unas gotas de lluvia con un potenciómetro. Si deseáis revisar esas dos entradas, podéis encontrarlas en los siguientes enlaces:


En esta tercera entrega, veremos cómo recibir varios datos simultáneamente por nuestro puerto serie y los mostraremos gráficamente como si de un osciloscopio se tratase.

Para los datos, he decidido colocar dos fotorresistores en nuestra Arduino basándome en una entrada antigua en la que explicaba cómo utilizar un sensor de luz para controlar el brillo de una LED. Podéis leer esa entrada en el siguiente enlace:


El nivel de luz que recibamos en cada uno de los dos sensores, será el que enviemos por puerto serie hacia Processing y lo representaremos gráficamente. He aquí una imagen de ejemplo de cómo deberíamos verlo:


Empezaremos por preparar la Arduino y colocar los dos sensores. Las resistencias que he usado para las LEDs son de 330Ω y las de los sensores son de 10kΩ. Así es cómo he colocado los componentes:
Imagen creada con Fritzzing

Tal como había hecho en la entrada en la que se explicaba cómo usar un sensor de luz, he colocado una LED para cada sensor, de tal forma que, cuanta menos luz detecte, más brillo le daré a la LED correspondiente.

/* Definimos pines */
#define PIN_LED1 9   // Conectamos una LED en el pin 9
#define PIN_LED2 10  // Conectamos una LED en el pin 10
#define PIN_LUZ1 A0  // Conectamos un fotorresistor en el pin analógico A0
#define PIN_LUZ2 A5  // Conectamos un fotorresistor en el pin analógico A5

String dato;         // Para enviar un dato por el puerto serie

void setup()
{
  /* Establecemos los pines de las LED como salida */
  pinMode(PIN_LED1, OUTPUT);
  pinMode(PIN_LED2, OUTPUT);
  
  /* Establecemos los fotorresistores como entrada */
  pinMode(PIN_LUZ1, INPUT); 
  pinMode(PIN_LUZ2, INPUT); 
  
  /* Iniciamos el puerto serie */
  Serial.begin(9600);
}

void loop()
{
  /* Obtenemos valores de los fotorresistores (varían de 0 a 900) */
  int nivelDeLuz1 = analogRead(PIN_LUZ1);  // fotorresistor1
  int nivelDeLuz2 = analogRead(PIN_LUZ2);  // fotorresistor2
  
  /* Escalamos los valores para el brillo de las LED */
  nivelDeLuz1 = map(nivelDeLuz1, 0, 900, 0, 255);
  if(nivelDeLuz1 < 15){ // si el valor es muy bajo...
    nivelDeLuz1 = 0;    // ...lo hago 0 directamente
  }
  
  nivelDeLuz2 = map(nivelDeLuz2, 0, 900, 0, 255);
  if(nivelDeLuz2 < 15){
    nivelDeLuz2 = 0;
  } 
  
  /* Nos aseguramos de que los niveles de luz son correctos */
  nivelDeLuz1 = constrain(nivelDeLuz1, 0, 255);
  nivelDeLuz2 = constrain(nivelDeLuz2, 0, 255);   
  
  /* Aplicamos el brillo a las LED */
  analogWrite(PIN_LED1, nivelDeLuz1);
  analogWrite(PIN_LED2, nivelDeLuz2);
  
  /* Enviamos dato por puerto serie */
  dato = "";
  dato += nivelDeLuz1;
  dato += ",";
  dato += nivelDeLuz2;
  Serial.println(dato);
  
  delay(50); // pequeña pausa para no saturar el puerto serie
}


En general, el código parece fácil de entender. Debemos de tener en cuenta el uso la función constrain para evitar valores no deseados.

Supongamos el código siguiente:

  DATO = constrain(X, A, B);   

Si el valor de X es menor que A, el valor de DATO será A. Si el valor de X es mayor que B, el valor de DATO será B. Y, finalmente, si el valor de X está entre A y B, el valor de DATO será X. Con ello, nos aseguramos que el valor de DATO estará siempre entre A y B.

Para enviar el dato por el puerto serie, lo haremos con una variable tipo String. Como vemos al final del código, la inicializamos como vacía y luego la "construimos" poco a poco añadiendo los dos niveles de luz separados por una coma ",". Eso lo usaremos posteriormente para separarlos al recibirlos en Processing.

Tal como está ahora mismo nuestra Arduino, ya podemos comprobar el funcionamiento correcto de los sensores fijándonos en las dos LED y abriendo el Monitor Serie del IDE Arduino:


Una vez comprobado el correcto funcionamiento de nuestra Arduino, vamos con Proccessing.

Al igual que en la entrega anterior, tendremos dos archivos: uno para el programa principal, y otro con una clase. Podemos verlo a continuación:


Empezamos por la clase Punto, que nos servirá para gestionar cada uno de los puntos de la gráfica y el desplazamiento hacia la izquierda.

Este es el código que he utilizado:

class Punto {
  float x;        // Coordenada horizontal
  float y;        // Coordenada vertical
  int velocidad;  // Píxels que desplazo a la izquierda en cada movimiento
  
  Punto(){
    x = width;
    y = height / 2;
    velocidad = 5;
  }
  
  /**
   * @param c_vertical Coordenada vertical
   */
  Punto(float c_vertical){
    this();  
    setY(c_vertical);
  }
  
  
  /**
   * @param c_horizontal Coordenada horizontal
   * @param c_vertical Coordenada vertical
   */
  Punto(float c_horizontal, float c_vertical){
    this();
    setX(c_horizontal);
    setY(c_vertical);
  }
  
  /**
   * Desplaza el punto hacia la izquierda
   */
  void desplazar(){
    setX(x - velocidad);
  }
  
  /**
   * Dibuja el punto
   */
  void dibujar(){
    ellipseMode(CENTER);
    ellipse(x, y, 2, 2);
  }
  
  /**
   * Establece el valor de la coordenada horizontal del punto
   * @param nuevoX La nueva coordenada horizontal
   */
   void setX(float nuevoX){
     x = nuevoX;
   }
   
  /**
   * Establece el valor de la coordenada vertical del punto
   * @param nuevoY La nueva coordenada vertical
   */
   void setY(float nuevoY){
     y = nuevoY;
   }
   
  /**
   * Establece la velocidad de movimiento hacia la izquierda del punto
   * @param velocidad La nueva velocidad
   */
   void setVelocidad(int v){
     velocidad = v;
   }
  
  
  /**
   * Devuelve el valor de la coordenada horizontal del punto
   * @return x La coordenada horizontal
   */
   float getX(){
     return x;
   }
   
  /**
   * Devuelve el valor de la coordenada vertical del punto
   * @return y La coordenada vertical
   */
   float getY(){
     return y;
   }
   
  /**
   * Devuelve la velocidad de movimiento hacia la izquierda del punto
   * @return velocidad La velocidad
   */
   int getVelocidad(){
     return velocidad;
   }
}

Hay algún método que no utilizo, como por ejemplo "dibujar()", que dibujaría un punto en la posición del punto (valga la redundancia). Si posteriormente hacéis alguna modificación en el archivo principal para experimentar, puede seros de ayuda tener esos métodos creados.

A continuación, podemos ver el código que he utilizado para el archivo principal:

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

ArrayList<Punto> onda1;       // Conjunto de puntos para representar la onda1
ArrayList<Punto> onda2;       // Conjunto de puntos para representar la onda2
int valores[] = {0, 0};       // Array para obtener los dos valores del puerto serie
float altura[] = {0.0, 0.0};  // Array para las alturas recibidas

Serial puerto;                // Puerto serie

void setup(){
   /* Configuración de ventana */
  size(800, 600);             // Tamaño de ventana 
  frameRate(30);              // FPS
  smooth();                   // Suavizado activado  
  
  /* Inicialización de variables */
  onda1 = new ArrayList<Punto>();
  onda2 = new ArrayList<Punto>();
  
  /* Configuración de puerto serie */
  String nombrePuerto = Serial.list()[0];
  println("Puerto establecido a " + nombrePuerto + ".");
  puerto = new Serial(this, nombrePuerto, 9600);
  puerto.bufferUntil('\n');   // guardo datos hasta que encuentro salto de línea
}

void draw(){
  background(0);              // Pintamos color de fondo
  
  if(valores != null){
    altura[0] = map((float) valores[0], 0, 255, 0, height);
    altura[1] = map((float) valores[1], 0, 255, 0, height);
    
    onda1.add(new Punto(altura[0]));
    onda2.add(new Punto(altura[1]));
    
    fill(#FF0000);           // relleno color rojo (para el texto) 
    text("Valor 1: " + altura[0], 20, 30);
    fill(#FFFF00);           // relleno color amarillo (para el texto) 
    text("Valor 2: " + altura[1], 20, 50);
    
    noFill();                // sin relleno
    strokeWeight(2);         // grosor del lápiz
    stroke(#FF0000);         // color del lápiz
    dibujaCurva(onda1);
    stroke(#FFFF00);         // color del lápiz
    dibujaCurva(onda2);
  }
}

void dibujaCurva(ArrayList<Punto> onda){
  beginShape();                               // empieza curva
  for(int i = 0; i < onda.size(); i++){
    Punto punto = onda.get(i);
    punto.desplazar();       
    curveVertex(punto.getX(), punto.getY());  // añado un punto a la curva
    
    // punto.dibujar();                       // dibujo el punto
    
    /* Borrar punto si se ha desplazado demasiado a la izquierda */
    if(punto.getX() < -width){
      onda.remove(i);
    }
  }
  if(onda.size() > 0){
    curveVertex(onda.get(onda.size() - 1).getX() + 1, onda.get(onda.size() - 1).getY());
    line(onda.get(onda.size() - 1).getX(), onda.get(onda.size() - 1).getY(), width, onda.get(onda.size() - 1).getY());     
  }
  /* Dibujar curva */
  endShape();                                   // termina curva 
}

void serialEvent(Serial p){
  String dato = p.readStringUntil('\n'); // leemos el buffer del puerto serie
  
  if(dato != null){
    dato = trim(dato); 
    valores = int(split(dato, ',')); 
  }
}

La gran diferencia respecto a cómo recogíamos datos del puerto serie en la primera y segunda entradas, es que ahora hacemos uso de serialEvent. En vez de escuchar permanentemente el puerto en la sección draw(), ahora guardamos lo recibido en el puerto serie ya en la sección setup() usando bufferUntil y luego en la nueva sección de serialEvent es cuando recogemos el dato propiamente dicho.

Como valores es un array, utilizamos la función split para separar los valores contenidos en dato y que están separados por el símbolo coma ",", tal como vemos al final del código:

    valores = int(split(dato, ',')); 

Para dibujar la curva, enviamos el conjunto de puntos al método dibujaCurva y hacemos uso de curveVertex.

Para dibujar una curva dando los puntos por los que pasa, primero hacemos una llamada a beginShape() para indicar que empezamos a dibujar. Lo siguiente es ir añadiendo puntos con curveVertex() y, para terminar, usaremos endShape(), que es cuando se dibuja la curva.

Aquí vemos un ejemplo de cómo podríamos dibujar una curva:
void setup(){
  size(240, 120);  // tamaño de ventana
}
void draw(){
  background(0);   // fondo negro
  strokeWeight(2); // grosor de lápiz
  stroke(#00FF00); // lápiz verde
  noFill();        // sin relleno
  smooth();        // suavizado
  
  beginShape();         // empieza curva  
  curveVertex(0, 20);   // añado un punto a la curva
  curveVertex(20, 20);  // añado un punto a la curva
  curveVertex(40, 60);  // añado un punto a la curva
  curveVertex(60, 20);  // añado un punto a la curva
  curveVertex(80, 60);  // añado un punto a la curva
  curveVertex(100, 20); // añado un punto a la curva
  curveVertex(120, 20); // añado un punto a la curva
  endShape();           // termino curva
}
  
Si lo ejecutamos, el resultado será el siguiente:


Como vemos, no se ha dibujado ni el primer punto ni el último, de ahí el último if que utilizo en el método dibujaCurva, para añadir un punto más y que se dibuje correctamente.

A continuación, podemos ver un vídeo con el programa en funcionamiento:


Espero que os haya gustado.

Un saludo: Roi