sábado, 1 de septiembre de 2012

Ejecutando la radio al inicio automáticamente

Dado que ya tengo la radio mínimamente funcional, una de las cosas que quería hacer es ejecutarla automáticamente al inicio del sistema, para que pueda utilizarla sin necesidad de conectarme al RPi por SSH para iniciar el programa.

En Internet he encontrado muchas formas de hacerlo, yo explico aquí una, pero seguramente habrá otras muchas que sirvan perfectamente. La información la obtuve de un post de StackOverflow.

En la mayoría de sistemas Linux, el proceso de arranque es el siguiente:

  • El bootloader termina de cargar.
  • Se ejecutan los scripts de /etc/rc.sysinit
  • Se consulta en qué runlevel debe establecerse el sistema consultando /etc/inittab
  • Se ejecutan los scripts del runlevel correspondiente, situados en /etc/rcX.d, donde X es el runlevel del sistema (0, 1, 2…)
  • Por último, se ejecutan los scripts de /etc/rc.local

Como queremos que nuestro sistema sea completamente funcional cuando arranquemos nuestra radio, debemos iniciarla desde /etc/rc.local. Para ello basta con abrir este archivo con permisos de administrador con un editor de texto:

sudo nano /etc/rc.local

Y añadir al final del fichero, justo antes del exit, nuestro programa como podemos ver en la imagen:

image

Ya sólo queda guardar el texto (Ctrl+O), Cerrar nano (Ctrl+X) y reiniciar el sistema. Si todo ha sido correcto, la radio debería arrancar automáticamente. Dejo un vídeo de cómo me funciona a mí:

Mejorando el código (I)

Hasta ahora hemos conseguido mostrar las canciones de la radio en el display del LCDKeypad y controlar la reproducción con uno de los botones. Sin embargo, el código actual es bastante chapucero, por lo que se producen algunos “fallos” un poco extraños. Hoy nos centraremos en corregir algunos de ellos:

Conexión serie sin necesidad de configuración

Un problema que tenía hasta ahora es que para que funcionase bien la conexión serie entre el Arduino y el RPi, tenía que ejecutar primero el programa minicom (del que hablamos en una entrada anterior), el RPi se conectaba correctamente y, al cerrar el programa y conectar mediante el programa en Python no había problemas.

Sin embargo, si no usaba minicom, la conexión se bloqueaba. Después de leerme la documentación de PySerial e ir probando con los distintos parámetros de conexión, comprobé que si habilitaba el flag “rtscts”, que activa el control de flujo por hardware, la conexión se establecía correctamente. El problema es que con esta opción activada, el RPi no se podía comunicar con el Arduino (al contrario no había problema), por lo que aunque podía utilizar los botones, no se mostraba nada en la pantalla.

Mi solución, al menos por ahora, ha sido crear y abrir una conexión serie con el rtscts activado, cerrarla y volverla a abrir con los parámetros por defecto. Creo que es algo parecido a lo que hacía con el minicom antes, donde dejaba que minicom hiciese una primera configuración y luego me permitía abrir sin problemas la conexión en Python. De todas formas, si alguien sabe más del tema, le agradecería que me comentase la forma más correcta de hacerlo, porque en este tema estoy un poco perdido. El código final de la conexión es el siguiente:

  1: # Sets the serial connection
  2: print "Opening serial port..."
  3: portOpened = False
  4: 
  5: while(not portOpened):
  6: 	if(sys.platform == 'win32'):
  7: 		try:
  8: 			ser = serial.Serial('COM4', 9600, rtscts=1)
  9: 			ser.close()
 10: 			ser = serial.Serial('COM4', 9600)
 11: 		except:
 12: 			print "Could not open port"
 13: 	else:
 14: 		try:
 15: 			ser = serial.Serial('/dev/ttyUSB0', 9600, rtscts=1)
 16: 			ser.close()
 17: 			ser = serial.Serial('/dev/ttyUSB0', 9600)
 18: 		except:
 19: 			print "Could not open port"
 20: 	
 21: 	if(ser.name != ""):
 22: 		print "Port opened!",ser
 23: 		portOpened = True
 24: 	else:
 25: 		print "Port can't be opened, retrying..."
 26: 		portOpened = False
 27: 
 28: # Trying with infinite timeout
 29: ser.timeout = None

Ahora esperamos hasta que el Arduino y el RPi están conectados correctamente entre sí antes de iniciar la radio, evitando posibles envíos perdidos.


Exclusión mutua con variable isPlaying


Recordemos que la variable isPlaying controla cuándo se está reproduciendo o no un stream en la radio. Esta variable es global y compartida entre varios hilos, que pueden leer y escribir en ella. Por tanto, si no controlamos el acceso exclusivo a la misma, podemos tener problemas. En mi caso, lo que ocurría es que a veces pulsaba el botón SELECT para parar la reproducción y la radio se volvía loca, ya que se creía que estaba en pausa cuando no lo estaba y viceversa. La solución es utilizar cerrojos para leer y escribir la variable, y evitar así condiciones de carrera y problemas de sincronización. Python tiene varios tipos de directivas de sincronización. Yo he usado la clase Lock.

  1: # Globals
  2: isPlaying = False
  3: # Locks
  4: isPlayingLock = threading.Lock()
  5: 
  6: '''This function receives button interaction from Arduino and respond to them'''	
  7: def button_receiver():
  8: 	print "Receiver started"
  9: 	global isPlaying
 10: 	while(True):
 11: 		serial_in = ser.readline();
 12: 		serial_out = ""
 13: 		
 14: 		if(serial_in == 'BTNSEL\r\n'):
 15: 			if(not isPlaying):
 16: 				print "[A->R] Received SEL: Playing..."
 17: 				# Put mpc to play
 18: 				args = shlex.split("mpc play")
 19: 				subprocess.check_output(args)
 20: 				# Send the info to Arduino
 21: 				serial_out = "DW Play"
 22: 				serial_out += '\n'
 23: 				ser.write(serial_out)
 24: 				serial_out = ""
 25: 				# Sets isPlaying to True
 26: 				time.sleep(1)
 27: 				isPlayingLock.acquire()
 28: 				try:
 29: 					isPlaying = True
 30: 				finally:
 31: 					isPlayingLock.release()
 32: 			else:
 33: 				print "[A->R] Received SEL: Stopping..."
 34: 				# Sets isPlaying to False
 35: 				isPlayingLock.acquire()
 36: 				try:
 37: 					isPlaying = False
 38: 				finally:
 39: 					isPlayingLock.release()
 40: 				# Stop mpc
 41: 				args = shlex.split("mpc stop")
 42: 				subprocess.check_output(args)
 43: 				# Send the info to Arduino
 44: 				serial_out = "DW Stop"
 45: 			
 46: 		elif(serial_in == 'BTNLFT\r\n'):
 47: 			serial_out = "DW BTNLFT OK"
 48: 		elif(serial_in == 'BTNRGT\r\n'):
 49: 			serial_out = "DW BTNRGT OK"
 50: 		elif(serial_in == 'BTNUP\r\n'):
 51: 			serial_out = "DW BTNUP OK"
 52: 		elif(serial_in == 'BTNDWN\r\n'):
 53: 			serial_out = "DW BTNDWN OK"
 54: 			
 55: 		if(serial_out != ""):
 56: 			print "[R->A]",serial_out
 57: 			serial_out += '\n'
 58: 			ser.write(serial_out)
 59: 		else:
 60: 			pass

En la línea 4 creamos un lock, este objeto se puede adquirir, bloqueando el acceso al las variables a las que se acceda hasta que se libere. Podemos ver su funcionamiento en las líneas 27 a 31. Básicamente, activamos el lock con el método acquire, intentamos poner la variable isPlaying a False, y lo consigamos o no liberamos el lock con el método release. Liberar un lock después de utilizarlo es muy importante, ya que en caso contrario podría quedarse bloqueado y no permitir el acceso a isPlaying.


Una vez liberado el lock, el resto de hilos pueden acceder libremente a la variable isPlaying. El caso es que no termino de acordarme (estudié estos mecanismos hace bastante tiempo), pero creo que el lock habría que utilizarlo en cada acceso a isPlaying, y no sólo en accesos de escritura, que es como está ahora mismo. Miraré mis apuntes y lo corregiré si es el caso. De hecho, estoy pensando en hacer una pequeña clase que se encargue de manejar las lecturas y escrituras mediante candados.


Para más detalles, en Wikipedia se explica mucho mejor esta directiva y su uso.


Cambio de play/pause por play/stop


Hasta ahora, al pulsar el botón SELECT mientras se estaba reproduciendo un stream de audio, la radio pausaba el stream, y al volverlo al pulsar reanudaba la reproducción. Esto tiene un pequeño inconveniente: como estamos en una radio online y no con archivos de audio locales, aunque nosotros pausemos la reproducción, ésta continúa en el servidor remoto. Si pausamos unos pocos segundos no pasa nada, pero si la pausa es algo más larga, el búfer del reproductor se llena y al reanudar la canción que estaba sonando se corta al poco tiempo.


Por ello, he decidido que, cuando se pulse el botón SELECT, lo que hará la radio es parar la reproducción, y al volverlo a pulsar, seguirá por lo que esté retransmitiendo la radio en ese momento. Este comportamiento es mucho más lógico que el anterior, ya que al fin y al cabo estamos escuchando una radio, la cual por definición emite independientemente de si la estamos escuchando o no.


Los cambios se pueden ver en las líneas 21 y41 del código anterior, donde en lugar de utilizar mpc toggle, utilizamos mpc play y mpc stop.


Comprobación de la conexión a Internet


Para escuchar la radio por Internet lo primero que necesitamos es, obviamente, una conexión a Internet activa. Hasta ahora no comprobábamos esto, lo que podría dar lugar a errores o a que simplemente no se reproduzca nada. Para evitarlo, haremos un chequeo de la conexión antes de iniciar la radio en sí. La función la he encontrado y adaptado de un post de StackOverflow:

  1: ''' This functon tests the Internet connection '''		
  2: def connectedToInternet():
  3: 	try:
  4: 		urllib2.urlopen("http://www.google.com", timeout=3)
  5: 	except urllib2.URLError:
  6: 		return False
  7: 	return True

Como se puede ver, lo único que hacemos es intentar abrir la página de Google (lo que suelo hacer yo para comprobar si hay Internet en mi casa), y devolver True o False según pueda cargar la página o no. Si Google desapareciese o se cayese, la radio no funcionaría aunque hubiese Internet, pero estoy dispuesto a asumir este riesgo.


Para hacer la comprobación se ha utilizado la librería urllib2 de Python. Esta librería viene incluida en la distribución estándar de Python, por lo que lo único que hay que hacer es importarla al programa.


Comprobando conexiones antes de iniciar el programa


La última modificación que he hecho es comprobar que haya conexión tanto a Internet como al Arduino antes de iniciar los hilos del programa:

  1: if __name__ == "__main__":
  2: 	arduinoReady = False
  3: 	while(not arduinoReady):
  4: 		serial = ser.readline()
  5: 		if(serial == "INITOK\r\n"):
  6: 			arduinoReady = True
  7: 			print "Arduino connected!"
  8: 			ser.write("UP RPi Connected!\n")
  9: 		else:
 10: 			print "Waiting Arduino connection..."
 11: 			time.sleep(1)
 12: 			
 13: 	while(not connectedToInternet()):
 14: 		print "Testing internet connection..."
 15: 		ser.write("DW Waiting Inet...\n")
 16: 		time.sleep(1)
 17: 		
 18: 	ser.write("DW Inet. Connected!\n")
 19: 		
 20: 	rec = threading.Thread(target=button_receiver,name='ButtonReceiverTh')
 21: 	sen = threading.Thread(target=display_sender,name='DisplaySenderTh')
 22: 	rec.start()
 23: 	sen.start()

En el código se comprueba que se haya recibido el INITOK del Arduino para confirmar que esté conectado, y que haya Internet. Cuando todo está correcto, se mostrará un mensaje por el display del LCDKeypad y ya se podrá utilizar la radio.


Corrigiendo sketch de Arduino


Hay veces que al pulsar un botón del Arduino, se me han enviado dos pulsaciones de ese botón. Esto es especialmente molesto en el botón SELECT, ya que si queremos detener la reproducción, al enviarse dos pulsaciones, el stream se detendrá y se volverá a activar.


El “fallo” parece ser de hardware: creo que lo que ocurre es que, después de dejar de pulsar el botón, durante un pequeño intervalo de tiempo, lcd.button vuelve a detectar que el botón se ha pulsado (aunque no se haya hecho). Para solucionarlo, lo único que he hecho es añadir un pequeño retardo de 30ms para evitar esto. No estoy del todo seguro de que ésta sea la causa, pero no se me ocurre otra cosa, ya que lcd.button tiene que detectar que se ha dejado de pulsar el botón antes de enviar la pulsación. Si a alguien tiene otra explicación, le agradecería que la comentase.

  1: // Returns the pressed button
  2: int buttonPressed(){
  3:   int aux = lcd.button();
  4:   if(aux != KEYPAD_NONE){
  5:     while(lcd.button() != KEYPAD_NONE){
  6:     }
  7:     delay(30);
  8:     return aux;
  9:   }
 10:   return KEYPAD_NONE;
 11: }
 12: 

Con estas correcciones la radio comprueba que haya conexión a Internet, se conecta automáticamente al Arduino y funciona sin problemas aparentes. El código ahora es un poco más largo (aunque no llega a 200 líneas), pero todo funciona mejor.


PD: Como indiqué en la entrada anterior, he creado un repositorio con el código del proyecto, por lo que cualquiera que quiera ver el código completo, descargarlo y utilizarlo o modificarlo, puede hacerlo sin problemas.

Repositorio del proyecto Wifi Radio

Para tener todo más organizado y que cualquiera pueda descargarse el código de forma cómoda y pueda ir viendo los cambios, he creado un repositorio en GitHub con el proyecto, y he incluido la librería LCDKeypad que me costó bastante encontrar y tuve que modificar un par de detalles para que funcionase con la IDE de Arduino nueva (1.0.1). Lo pongo aquí para quien le interese.

https://github.com/mpedrero/wifi_radio

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.

martes, 28 de agosto de 2012

Atajos de teclado en la búsqueda de Google

Desde hace tiempo es posible utilizar la búsqueda de Google utilizando tan sólo el teclado. Pruébalo: abre una ventana con la página de Google, escribe algo para buscar y pulsa Intro. Hasta ahí todo normal, lo bueno viene ahora.

Si pulsas Tabulador, aparece una flecha azul a la izquierda del primer resultado, y podemos movernos entre resultados mediante las flechas del teclado (arriba y abajo). Si queremos acceder a un resultado concreto basta con poner la flecha sobre él y pulsar Intro, y si queremos volver a buscar, basta con seguir escribiendo y automáticamente el cursor vuelve a la caja de búsqueda.

Lo que puede pasar es que nada de esto ocurra, que es lo que me pasó a mí hace unos meses, coincidiendo con unos cambios en la interfaz de Google, y supuse que habrían eliminado esta característica por falta de uso. Sin embargo lo que pasaba es que tenía deshabilitado Google Instant (es decir, que Google empiece a buscar resultados mientras escribimos y no cuando pulsemos en buscar) y se ve que esto también deshabilita los atajos de teclado.

Para volver a activar Google Instant, basta con buscar algo en Google y pulsar sobre el icono de configuración de la parte superior derecha de la página (el del engranaje). En el menú seleccionamos Ajustes de búsqueda (la primera opción) y seleccionamos "Mostrar siempre resultados de Google Instant". Sólo queda guardar los cambios y ya se podrá usar sin problemas.

Lo ideal sería que estos atajos viniesen habilitados por defecto, ya que ahora hay que tener la sesión iniciada para que Google recuerde tus preferencias, pero al menos ya puedo volver a usarlos, que no es poco.

Mostrando canción en LCD con Arduino (II)

En el post anterior conseguimos mostrar en el LCD conectado al Arduino cadenas de caracteres enviadas desde la conexión serie del mismo. Nos falta la parte correspondiente al RPi, es decir, generar las cadenas correspondientes a la canción que está sonando en MPD y enviarla al Arduino por puerto serie. Para ello vamos a utilizar Python, un lenguaje de programación que nos permite hacer mucho con muy poco, como veremos a continuación.

Conectando el Arduino y el Raspberry Pi por puerto serie

Lo primero que debemos hacer es establecer una comunicación serie entre el Arduino y nuestro RPi. Para ello utilizaremos el puerto USB, que es una conexión serie al fin y al cabo, así que conectamos un cable USB entre el RPi (HUB USB) y el Arduino. Si todo va bien, el Arduino debe encenderse al igual que cuando lo conectábamos a un PC convencional.

Recordemos además que en el programa que escribimos en el post anterior, el Arduino enviaba por el puerto serie una cadena “INITOK” después de inicializarse, y otra cadena “Loop” en cada iteración de la función loop del programa, así que si todo ha salido bien, el RPi debe de estar recibiéndolas. Comprobémoslo:

  • Desconectamos el Arduino del RPi.
  • Escribimos en una terminal: ls /dev/tty*
  • Nos saldrá algo parecido a esta imagen:
  • image

  • Conectamos el Arduino al RPi y esperamos unos segundos.
  • Volvemos a escribir en la terminal ls /dev/tty*
  • Nos saldrá algo parecido a esta imagen:
  • image

    Si nos fijamos, en la segunda imagen hay una nueva entrada (en mi caso /dev/ttyUSB0). Ésta es la conexión serie de nuestro RPi. Para poder monitorizar esta conexión vamos a instalar un pequeño programa llamado minicom, para ello, en la terminal escribimos:

  • sudo apt-get install minicom
  • Y ahora lo configuramos para que monitorice nuestra interfaz serie. De nuevo en la terminal:

  • sudo minicom –s
  • Nos saldrá una interfaz de configuración en modo texto. Básicamente tenemos que cambiar el dispositivo a monitorizar, que en nuestro caso es /dev/ttyUSB0 y la velocidad de la conexión, que en el Arduino la establecimos a 9600 baudios.

    Para ello seleccionamos “configuración de la puerta serial” como aparece en la imagen:

    image

    Y lo configuramos modificando el Dispositivo serial al que nos haya salido antes (en mi caso /dev/ttyUSB0) y el Bps/Paridad/Bits para establecerlo a 9600 8N1:

    image

    Guardamos la configuración como dfl, que es la configuración que cargará minicom por defecto y salimos de minicom.

    Ahora sólo queda ejecutar minicom sin parámetros para comprobar que todo ha salido bien, y deberíamos ver los envíos del Arduino (podemos cerrar minicom pulsando Control+A Q):

    image

    Reproduciendo música con MPD/MPC y obteniendo datos de la canción

    En este momento voy a hacer un inciso para explicar por encima el manejo de MPD y MPC. En anteriores posts dijimos que MPD era un demonio que permitía reproducir todo tipo de audio, y MPC era un cliente que facilitaba el manejo de MPD. Para probar nuestro programa, vamos a añadir una radio online a MPD, vamos a reproducirla, y vamos a obtener los datos de la emisora y de la canción. En mi caso he optado por una radio que he descubierto hace poco y me ha gustado mucho, ya que tiene buena música y ninguna publicidad: Radio Paradise.

    MPD necesita la dirección del stream que queramos reproducir. Buscando en la página de Radio Paradise encontramos varios streams según la calidad de reproducción que queramos en la pestaña “Listen”. En mi caso he elegido la opción de 128 Kbps con el códec AAC+, que da mayor calidad de el bitrate equivalente en MP3. El link en este caso es:

    http://www.radioparadise.com/musiclinks/rp_128aac.m3u

    ¿Cómo reproducir esto en MPD? Muy sencillo: primero lo añadimos a la lista de reproducción escribiendo en consola:

    mpc load http://www.radioparadise.com/musiclinks/rp_128aac.m3u

    Y a continuación iniciamos la reproducción escribiendo:

    mpc play

    Si tenemos el RPi conectado a internet y a unos altavoces externos debería empezar a sonar la radio. De todas maneras podemos comprobar qué esta sonando escribiendo simplemente:

    mpc

    image

    Existen muchos otros comandos para MPC: podemos pausar y reanudar la reproducción con mpc toggle, pararla con mpc stop, eliminar la lista de reproducción actual con mpc clear, eliminar todas los elementos de la lista menos el actual con mpc crop, etc. Para más información se puede consultar el manual con man mpc.

    Un comando que nos interesa, sin embargo, es mpc current, que muestra información sobre la canción actualmente en reproducción. Además puede configurarse con máscaras, así, si escribimos en la terminal:

    mpc current –f “%name%”

    Obtendremos el nombre de la emisora de radio. Si escribimos:

    mpc current –f “%title%”

    Obtenemos el artista y la canción que está sonando en ese momento. Perfecto para nuestros propósitos.

    image

    Lamentablemente, las máscaras de artista y álbum no devuelven nada. Supongo que depende de lo bien (o mal en este caso) que lo hayan implementado en cada emisora. De todas formas para una primera prueba nos sobra.

    Integrando todo en Python

    Ya que tenemos la conexión serie entre el Arduino y el RPi funcionando correctamente y el MPD configurado y reproduciendo música, usaremos Python para obtener el artista y la canción que esté sonando en MPD y la enviaremos al Arduino. En mi caso he optado por mostrar el artista en la línea superior del display y la canción en la línea superior.

    Python viene instalado por defecto en Raspbian, pero no así la librería PySerial, que necesitaremos para enviar y recibir información de forma sencilla por la conexión serie del RPi. La podemos instalar escribiendo en la terminal:

    sudo apt-get install python-serial

    Solo queda crear el programa en Python. Copio el código del programa y lo comento a continuación:

      1: import serial, subprocess, time, shlex
      2: 
      3: ser = serial.Serial('/dev/ttyUSB0', 9600)
      4: 
      5: serial_out = ""
      6: serial_in = ""
      7: com_radio = 'mpc current -f "%name%"'
      8: com_song = 'mpc current -f "%title%"'
      9: 
     10: 
     11: while True:
     12: 	args = shlex.split(com_song)
     13: 	str_song = subprocess.check_output(args)
     14: 	list = str_song.split(' - ')
     15: 
     16: 	serial_out = "UP "
     17: 	serial_out += list[0]
     18: 	serial_out += '\n'
     19: 	ser.write(serial_out)
     20: 	print "RPi->Arduino:",serial_out
     21: 
     22: 	time.sleep(1)
     23: 
     24: 	serial_out = "DW "
     25: 	serial_out += list[1]
     26: 	serial_out += '\n'
     27: 	ser.write(serial_out)
     28: 	print "RPi->Arduino:",serial_out
     29: 	
     30: 	time.sleep(5)

     


    En la línea 1 importamos las librerías que usaremos en el programa. En concreto usaremos serial para la conexión serie con el Arduino, subproccess para poder invocar a MPC desde el programa y recoger el artista y la canción que está sonando en cada momento, time para poder establecer un tiempo de espera entre comprobaciones y shlex para ayudarnos a manipular cadenas de caracteres.


    En la línea 3 abrimos la comunicación serie. Tenemos que indicar el dispositivo con el que nos queremos comunicar (en mi caso /dev/ttyUSB0) y la velocidad de transmisión, que son los 9600 baudios.


    En las líneas 5 a 8 declaramos las cadenas que vamos a utilizar para enviar y recibir información (serial_in y serial_out) y las cadenas que vamos a utilizar para obtener el nombre de la estación, el artista y el título de la canción (com_radio y com_song)


    En las líneas 11 a 30 tenemos un bucle infinito. En él comprobamos el nombre de la canción y lo enviamos al Arduino: en la línea 12 separamos la cadena de comandos necesaria para obtener el artista y canción para adaptarlo al formato que necesita Python y lo guardamos en args que es una lista de los parámetros que ejecutaremos.


    En la línea 13 ejecutamos un subproceso y almacenamos el resultado en str_song. Es exactamente igual que si ejecutásemos el contenido de com_song en el terminal y almacenásemos lo que devuelve el terminal en la cadena str_song. En este caso se estamos pidiendo el campo title a MPC, que ya hemos visto antes que nos devuelve una cadena con [nombredelartista] – [nombredelacancion]. Eso mismo es lo que contendrá str_song.


    En la línea 14 lo que hacemos es separar esa cadena en una lista en la que el primer elemento sea el nombre del artista, y el segundo el nombre de la canción. Para ello usamos el método split y le especificamos que nos separe cuando encuentre un espacio, un guión y un espacio.


    En las líneas 16 a 20 construimos la cadena que enviaremos al Arduino: como queríamos mostrar el artista en la línea superior del display, tendremos que escribir una cadena que comience con “UP “, concatenar el nombre del artista (que esta en list[0]) y añadir un carácter de fin de línea. Por último, en la línea 19 enviamos la cadena que hemos construido al Arduino. Como veis, la sintaxis de Python es muy sencilla, clara y concisa; y permite abstraernos de muchos detalles de bajo nivel.


    En la línea 22 esperamos un segundo para dar tiempo a que el Arduino reciba y procese el artista (antes de que me lo digáis, se que es una chapuza, y más cuando el propio Arduino está respondiendo con un OK cuando recibe los datos correctamente, lo cambiaré más adelante).


    En las líneas 24 a 28 hacemos exactamente el mismo proceso pero con la canción y lo enviamos también al Arduino. En ambos casos imprimimos las cadenas que estamos enviando por la terminal para detectar posibles errores.


    En la última línea simplemente esperamos 5 segundos antes de volver a comprobar si ha cambiado la canción para evitar estar comprobando todo el tiempo.


    Probando todo


    Ya sólo queda probar que todo funcione correctamente, así que comprobamos que MPC esté reproduciendo, que el Arduino esté conectado y que el display esté mostrando Arduino Radio v0.0. Podemos también arrancar el minicom para monitorizar qué se envía y se recibe por el puerto serie. Sólo queda ejecutar en programa de Python (yo lo he llamado radio.py) con:


    python radio.py


    image




    IMG_0412


    Parece que efectivamente todo funciona perfectamente.


    Con esto ya tenemos la base de lo que será nuestra radio. En resumen, hemos conseguido configurar el RPi, controlar desde línea de comandos un reproductor de audio, añadir streams de radios online y reproducirlos, obtener los metadatos de las canciones que están sonando y enviarlos a un LCD controlado por un Arduino mediante conexión serie. No está nada mal. Ahora toca pulir los códigos y ampliar funcionalidades. En concreto, mi objetivo ahora es conseguir que al pulsar un botón del LCDKeypad (el shield del Arduino) podamos detener y reanudar la reproducción de la radio. ¡Nos vemos en el siguiente post!