viernes, 31 de agosto de 2012

Utilizando el LCDKeypad para controlar la radio (I)

Una vez que hemos conseguido que se muestren artista y canción en nuestro display, vamos a conseguir pausar y reanudar la reproducción cuando se pulse uno de los botones del LCDKeypad.

El LCDKeypad tiene 5 botones programables por el usuario y uno que resetea el Arduino, como se puede ver en la imagen:

IMG_0414

Básicamente tenemos un botón de selección (SELECT) y una cruceta de control (UP, DOWN, LEFT, RIGHT). Mediante la librería LCDKeypad se puede detectar la pulsación de dichos botones. Lo que tenemos que hacer, básicamente, es conseguir que el Arduino detecte que hemos pulsado un botón (en nuestro caso el botón SELECT) y envíe una señal al RPi. Éste a su vez pausará o reanudará la reproducción al recibir la señal y mandará una actualización por pantalla del estado de la misma.

Detectando las pulsaciones con Arduino

Lo primero será detectar cuándo se ha pulsado cualquiera de los botones programables utilizando el Arduino. Mirando la librería, en concreto el fichero LCDKeypad.h, vemos que hay 6 macros, 5 asignadas a cada uno de los botones y la restante a cuando no hay ningún botón pulsado:

  1: // library interface description
  2: #define KEYPAD_NONE -1
  3: #define KEYPAD_RIGHT 0
  4: #define KEYPAD_UP 1
  5: #define KEYPAD_DOWN 2
  6: #define KEYPAD_LEFT 3
  7: #define KEYPAD_SELECT 4

Veamos los cambios necesarios en el sketch de Arduino:

  1: #include <LiquidCrystal.h>
  2: #include <LCDKeypad.h>
  3: 
  4: LCDKeypad lcd;
  5: String serialIn;
  6: String serialOut;
  7: boolean serialComplete = false;
  8: int buttonActive;
  9: 
 10: void setup(){
 11:   Serial.begin(9600);
 12:   
 13:   lcd.begin(16,2);
 14:   lcd.clear();
 15:   lcd.print("Arduino Radio");
 16:   lcd.setCursor(0,1);
 17:   lcd.print("V 0.0");
 18:   serialIn.reserve(200);
 19:   
 20:   Serial.println("INITOK"); 
 21: }
 22: 
 23: void loop(){
 24:   buttonActive = buttonPressed();
 25:   
 26:   if(serialComplete){
 27:      if (serialIn.startsWith("UP")){
 28:        lcd.setCursor(0,0);
 29:        lcd.print("                ");
 30:        lcd.setCursor(0,0);
 31:        lcd.print(serialIn.substring(3));
 32:        
 33:        Serial.println("OK");
 34:      }
 35:      
 36:      else if (serialIn.startsWith("DW")){;
 37:        lcd.setCursor(0,1);
 38:        lcd.print("                ");
 39:        lcd.setCursor(0,1);
 40:        lcd.print(serialIn.substring(3));
 41:        
 42:        Serial.println("OK");
 43:      }
 44:      
 45:      else{
 46:        Serial.println("KO");
 47:      }
 48: 
 49:      serialIn = "";
 50:      serialComplete = false;
 51:   }
 52:   
 53:   // Checking buttons
 54:   if(buttonActive!=KEYPAD_NONE){
 55:     if(buttonActive == KEYPAD_SELECT){
 56:       Serial.println("BTNSEL");
 57:     }
 58:     else if(buttonActive == KEYPAD_RIGHT){
 59:       Serial.println("BTNRGT");
 60:     }
 61:     else if(buttonActive == KEYPAD_LEFT){
 62:       Serial.println("BTNLFT");
 63:     }
 64:     else if(buttonActive == KEYPAD_UP){
 65:       Serial.println("BTNUP");
 66:     }
 67:     else if(buttonActive == KEYPAD_DOWN){
 68:       Serial.println("BTNDWN");
 69:     }
 70:   }
 71:   else{
 72:     //Serial.println("LOOP");
 73:   }
 74: }
 75: 
 76: 
 77: // Reads the serial input and stores in serialIn
 78: void serialEvent() {
 79:   while (Serial.available()) {
 80:     // get the new byte:
 81:     char inChar = (char)Serial.read(); 
 82:     // add it to the inputString:
 83:     if(inChar != '\n'){
 84:       serialIn += inChar;
 85:     }
 86:     // if the incoming character is a newline, set a flag
 87:     // so the main loop can do something about it:
 88:     if (inChar == '\n') {
 89:       serialComplete = true;
 90:     }
 91:   }
 92: }
 93: 
 94: // Returns the pressed button
 95: int buttonPressed(){
 96:   int aux = lcd.button();
 97:   if(aux != KEYPAD_NONE){
 98:     while(lcd.button() != KEYPAD_NONE){
 99:     }
100:     return aux;
101:   }
102:   return KEYPAD_NONE;
103: }
104: 
105: 

Como podemos ver, el mayor cambio respecto al código anterior son las líneas 54 a 70, donde se realiza el chequeo de cualquier botón que se haya podido pulsar y se envía un código identificativo por el puerto serie. Para ello se compara la variable global buttonActive con cada uno de los botones y, si coincide con alguno, se envía su identificador al RPi.


La pregunta lógica es, ¿de dónde sale esta variable? Se actualiza en cada ciclo del loop principal (línea 24) llamando a la función buttonPressed. Esta función está implementada al final del código (líneas 95 a 103). buttonPressed comprueba que se haya pulsado un botón y, si es así, espera a que se deje de pulsar y devuelve la macro del botón que se ha pulsado. Si no se ha pulsado nada, devuelve la macro KEYPAD_NONE). La función es muy útil, ya que evita que si dejamos pulsado un botón se estén enviando continuamente las pulsaciones al RPi. En nuestro caso lo que haríamos es que el RPi estuviese pausando y reanudando la reproducción contínuamente hasta que soltásemos el botón. Con buttonPressed conseguiremos que todo funcione como se espera.


Hay algún cambio menor en el código, pero básicamente lo añadido es la parte que acabo de describir. Si probamos el código con el RPi conectado al PC podremos ver como se envían los códigos de cada botón en el Serial Monitor al pulsarlos.


Ampliando el programa controlador de la radio Wifi


En la entrada anterior teníamos un programa rudimentario que simplemente preguntaba a MPC qué canción estaba sonando y enviaba las información de artista y álbum al Arduino para que éste la representase en el display. Ahora tenemos que controlar eso y, además, estar preparados para recibir información sobre las pulsaciones de los botones para poder pausar y reanudar la reproducción. Para ello vamos a utilizar dos hilos de ejecución. Uno se encargará de enviar la información de la canción, pero sólo cuando se esté reproduciendo (ya que si la radio está en pausa es obvio que la canción no va a cambiar). El otro hilo se encargará de estar pendiente de los envíos del Arduino sobre el estado de sus botones.


En principio se podría hacer todo en un solo hilo, pero en mi opinión queda más limpio así y permite ampliar el programa más adelante de manera sencilla. Vamos con el código:

  1: import serial, subprocess, time, shlex, threading, sys
  2: 
  3: # Sets the serial connection
  4: if(sys.platform == 'win32'):
  5: 	ser = serial.Serial('COM4', 9600)
  6: else:
  7: 	ser = serial.Serial('/dev/ttyUSB0', 9600)
  8: 
  9: ser.timeout = 4
 10: 
 11: # Globals
 12: isPlaying = False
 13: 
 14: '''This function receives button interaction from Arduino and respond to them'''	
 15: def button_receiver():
 16: 	print "Receiver started"
 17: 	global isPlaying
 18: 	while(True):
 19: 		serial_in = ser.readline();
 20: 		serial_out = ""
 21: 		print "[RECEIVER] Arduino -> RPi:",serial_in
 22: 		
 23: 		if(serial_in == 'BTNSEL\r\n'):
 24: 			args = shlex.split("mpc toggle")
 25: 			subprocess.check_output(args)
 26: 			isPlaying = not isPlaying
 27: 			if(isPlaying):
 28: 				serial_out = "DW Play"
 29: 			else:
 30: 				serial_out = "DW Pause"
 31: 			
 32: 		elif(serial_in == 'BTNLFT\r\n'):
 33: 			serial_out = "DW BTNLFT OK"
 34: 		elif(serial_in == 'BTNRGT\r\n'):
 35: 			serial_out = "DW BTNRGT OK"
 36: 		elif(serial_in == 'BTNUP\r\n'):
 37: 			serial_out = "DW BTNUP OK"
 38: 		elif(serial_in == 'BTNDWN\r\n'):
 39: 			serial_out = "DW BTNDWN OK"
 40: 			
 41: 		if(serial_out != ""):
 42: 			print "[RECEIVER] RPi -> Arduino:",serial_out
 43: 			serial_out += '\n'
 44: 			ser.write(serial_out)
 45: 		else:
 46: 			print "No data"
 47: 		#time.sleep(2)
 48: 	
 49: '''This function sends display updates to arduino'''	
 50: def display_sender():
 51: 	global isPlaying
 52: 	
 53: 	print "Sender started"
 54: 	
 55: 	# Strings to ask things to mpc
 56: 	com_radio = 'mpc current -f "%name%"'
 57: 	com_song = 'mpc current -f "%title%"'
 58: 	
 59: 	# String to send over serial
 60: 	serial_out = ""
 61: 	
 62: 	while(True):
 63: 		if (isPlaying):
 64: 			args = shlex.split(com_song)
 65: 			str_song = subprocess.check_output(args)
 66: 			list = str_song.split(' - ')
 67: 
 68: 			serial_out = "UP "
 69: 			try:
 70: 				serial_out += list[0]
 71: 			except IndexError:
 72: 				serial_out += "Unknown artist"
 73: 			serial_out += '\n'
 74: 			ser.write(serial_out)
 75: 			print "RPi->Arduino:",serial_out
 76: 
 77: 			
 78: 			serial_out = "DW "
 79: 			try:
 80: 				serial_out += list[1]
 81: 			except IndexError:
 82: 				serial_out += "Unknown song"
 83: 			serial_out += '\n'
 84: 			ser.write(serial_out)
 85: 			print "[SENDER] RPi -> Arduino:",serial_out
 86: 		
 87: 		time.sleep(1)
 88: 		
 89: if __name__ == "__main__":
 90: 	rec = threading.Thread(target=button_receiver,name='ButtonReceiverTh')
 91: 	sen = threading.Thread(target=display_sender,name='DisplaySenderTh')
 92: 	rec.start()
 93: 	sen.start()
 94: 	

En la línea 1 he añadido la librería sys, que nos servirá para determinar la plataforma en la que se está ejecutando el programa; y la librería threading necesaria para la creación y control de hilos en Python.


Las líneas 4 a 7 determinan si estamos ejecutando el programa en Windows o en el RPi, y establece la conexión serie según el caso. Esto lo he añadido para no tener que cambiarlo cada vez que modifico el programa (estoy programándolo en Windows y luego lo copio al RPi por SCP).


La línea 12 define la variable booleana isPlaying, que establece si estamos o no reproduciendo en cada momento.


Las líneas 15 a 47 contienen la función button_receiver, que es lo que ejecutará el hilo encargado de recibir el estado de los botones del LCDKeypad. Como vemos es un bucle infinito que comprueba si se ha recibido alguna pulsación y actúa en consecuencia. Por ahora, si recibe una pulsación de cualquiera de los botones que no sean el SELECT, simplemente muestra un mensaje de confirmación en el display del Arduino. Si recibe una pulsación del botón SELECT, lanza un subproceso con el comando necesario para pausar/reanudar MPD y actualiza la variable isPlaying.


Por último, comprueba si se ha recibido otra orden del Arduino que no esté actualmente implementada y la imprime por la salida estándar. Si no se ha recibido nada y se ha llegado al timeout, simplemente imprime “No data”.


Algo curioso de Python es que obliga a definir explícitamente que queremos utilizar una variable como global dentro de una función. Es el caso de isPlaying, que definimos como global al principio del programa, y que tenemos que indicar que queremos utilizarla en esta función como se ve en la línea 17.


Las líneas 50 a 87 contienen la función display_sender, que es lo que ejecutará el hilo encargado de actualizar el display del Arduino. Contiene básicamente el mismo código que el programa de la entrada anterior, pero se han introducido algunas modificaciones, en concreto:



  • En las líneas 69 a 72 y 79 a 82 he introducido un control de excepciones para que en caso de que no se haya parseado correctamente el artista o el álbum, ya sea por un error en el MPC, en el formato de la cadena o en cualquier otra cosa, se muestre una línea indicando artista o canción desconocida, según el caso.
  • Se han eliminado las esperas entre envíos ya que he comprobado que la librería se encarga de evitar que se solapen dos envíos.
  • Se ha reducido el tiempo de espera a 1 segundo entre comprobaciones.
  • La comprobación de la canción y actualización sólo se lleva a cabo cuando la música está sonando.

Por último, en las líneas 89 a 93 está la función main del programa. En ella se crean dos hilos con las dos funciones que acabamos de describir y se lanzan, por lo que se ejecutarán simultáneamente (en realidad no, pero nos sirve perfectamente) y se encargarán de verificar los botones del LCDKeypad y de mantener actualizado el display.


Podéis ver cómo funciona todo en un pequeño vídeo que he grabado (la música se oye muy baja, pero se oye):



Ahora que ya funciona, es el turno de implementar el cambio de emisora. Tengo algunas ideas, pero lo dejo para el siguiente post.

No hay comentarios:

Publicar un comentario