Mostrando entradas con la etiqueta diy. Mostrar todas las entradas
Mostrando entradas con la etiqueta diy. Mostrar todas las entradas

viernes, 11 de octubre de 2013

Conectar pantalla LCD 16x2 a Raspberry Pi

Aunque el proyecto de la radio está muy olvidado, ahora que tengo algo de tiempo he investigado si en lugar de utilizar un Arduino de intermediario (como hacía hasta ahora) era posible conectar una pantalla LCD directamente al Raspberry Pi utilizando los puertos GPIO (General Purpose Input Output) que incluye. La respuesta es que sí que se puede, y que no es demasiado complicado. Así que, cuando continúe con el proyecto de la radio, posiblemente me decida a descartar el uso del Arduino y conectar todo al Raspberry. Para muestra, un botón:

IMG_0461

En mi caso, he usado una pantalla simple 16x2 compatible con el controlador HDD44780. Básicamente, cualquier pantalla económica de 16x2 que encuentres en eBay o similares será compatible con este controlador. Algunas, más caras, tienen una interfaz I2C o SPI que nos permiten entre otras cosas utilizar menos cables. La interfaz de dicha pantalla es paralela, con 4 cables para datos y, además, tiene algunos conectores adicionales para la luz, el contraste, el modo de funcionamiento, etc.

Para mostrar el texto en la pantalla utilizaremos un pequeño programa en Python (una vez hayamos instalado las librerías necesarias para poder controlar desde Python los puertos GPIO del Raspberry). Pero empecemos por el principio:

1. Realizando las conexiones


Además de los puertos que solemos usar en el Raspberry (USBs, Ethernet, HDMI, Audio…), tenemos disponibles otros puertos para conexiones de más bajo nivel. Entre otros tenemos un puerto serie, UART, I2C, SPI y 13 puertos GPIO que se pueden programar para adaptarse a nuestras necesidades. Para más información se puede consultar este enlace. Todos estos puertos están situados en los pines de la esquina del Raspberry:

RPi_P1_header

Y el diagrama de dichos puertos es el siguiente:

GPIOs

En cuanto a las pantallas 16x2 (aunque también hay disponibles otros tamaños como 20x4), todas tienen 16 pines cuyo diagrama es el siguiente:

Lcd_0

Los pines de datos son los DBX, aunque en la práctica se suelen utilizar sólo DB4-DB7. Para más información sobre estas pantallas se puede consultar este enlace, donde además se puede descargar el datasheet completo de las mismas.

Siendo el Pin 1 de la pantalla situado más a la izquierda (GND), las conexiones que hay que realizar entre el Raspberry y la pantalla son las siguientes (es conveniente ayudarse de una protoboard):

  • Pin 1 de la pantalla (GND) a tierra.
  • Pin 2 de la pantalla (VCC) a +5V.
  • Pin 3 de la pantalla (VEE) a tierra.
  • Pin 4 de la pantalla (RS) a GPIO 25.
  • Pin 5 de la pantalla (RW) a tierra.
  • Pin 6 de la pantalla (EN) a GPIO 24
  • Pin 7, 8, 9 y 10 de la pantalla desconectados.
  • Pin 11 de la pantalla (D4) a GPIO 23
  • Pin 12 de la pantalla (D5) a GPIO 17
  • Pin 13 de la pantalla (D6) a GPIO 21
  • Pin 14 de la pantalla (D7) a GPIO 22
  • Pin 15 de la pantalla (LED+) a 5V.
  • Pin 16 de la pantalla (LED-) a tierra.

Aunque con estas conexiones tenemos suficiente, podemos conectar un potenciómetro al pin 3 para poder regular el contraste de la pantalla (si lo conectamos a tierra, estará al máximo). Del mismo modo, podemos conectar un potenciómetro a LED+ para reducir el brillo de la misma (o incluso conectarlo a 3V3 Power, de proporciona 3.3V en lugar de los 5V).

El diagrama de las conexiones sería el siguiente:

UntitledEste diagrama (y gran parte de este tutorial está basado en el de la web de Adafruit).


2. Instalando los paquetes necesarios

El segundo paso es instalar en nuestra distribución (yo estoy usando Raspbian) los paquetes necesarios para poder ejecutar nuestro script. En este paso será necesaria una conexión a Internet en el Raspberry.

Comenzaremos instalando los paquetes de desarrollo de Python, Python PIP (una especie de Synaptic pero para las librerías de Python), las Setup Tools para poder instalar easy_install y git para descargar más adelante los códigos de ejemplo de la página de Adafruit:

sudo apt-get install python-dev python-setuptools python-pip git


A continuación actualizaremos la herramienta distribute (parte de setuptools), que nos ayudará a descargar la librería GPIO:


sudo easy_install -U distribute


Una vez hecho esto, por fin podremos descargar la librería GPIO para Python:


sudo pip install rpi.gpio




 


3. Descargando códigos de ejemplo


Adafruit mantiene un repositorio Git con diversos códigos de ejemplo para el Raspberry. La forma más sencilla de descargarlos a nuestro sistema es clonar el repositorio. Como ya instalamos git en el paso anterior, basta con introducir:


git clone git://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code.git


Tras unos instantes, se habrá creado en el directorio actual una carpeta llamada Adafruit-Raspberry-Pi-Python-Code con multitud de subcarpetas. A nosotros nos interesa en concreto la subcarpeta Adafruit_CharLCD:


cd Adafruit-Raspberry-Pi-Python-Code
cd Adafruit_CharLCD


4. Modificando el código de ejemplo (sólo Raspberry Pi Rev. 2)


El código de ejemplo que vamos a utilizar es Adafruit_LCD.py. Si tenemos un Raspberry Rev. 1 (las primeras Raspberry, que no tenían agujeros para tornillos en la placa y sólo tenían 256MB de RAM en el modelo B) el código funcionará perfectamente. Si por el contrario tenemos un Raspberry Rev. 2 (con agujeros para tornillos y 512MB de RAM en el modelo B), hay que realizar una pequeña modificación en el código. Para ello, podemos abrirlo con nano:


nano Adafruit_CharLCD.py 


Y en la línea donde pone:


def __init__(self, pin_rs=25, pin_e=24, pins_db=[23, 17, 21, 22], GPIO = None


Reemplazar el 21 por un 27 (esto es debido a un cambio en los puertos del modelo 2 del Raspberry. Para guardar el archivo modificado pulsamos Ctrl+O y salimos con Ctrl+X.


 


5. Probando todo


Sólo nos queda dar permisos de ejecución al programa y ejecutarlo con:

chmod +x Adafruit_CharLCD.py
sudo ./Adafruit_CharLCD.py

Si todo ha ido bien, deberíamos ver algo parecido a lo siguiente:


IMG_0463
En caso de no obtener el texto, lo más probable es que no hayamos hecho correctamente las conexiones entre el Raspberry y la pantalla, por lo que lo mejor es repasarlas para buscar posibles errores. También puede que nos hayamos saltado el paso 4 teniendo un Raspberry Rev. 2. Para asegurarte de tu versión de Raspberry, mira la siguiente imagen:


8429332057_c64051b253
El Raspberry de la izquierda es la revisión 1 y el de la derecha es la revisión 2. Se distinguen fácilmente por los agujeros para tornillos marcados con las flechas rojas.


 


6. Mostrando fecha, hora y dirección IP interna


Si queremos mostrar algo parecido a lo que se puede ver en la primera imagen de esta entrada, basta con ejecutar el otro ejemplo del directorio:

chmod +x ./Adafruit_CharLCD_IPclock_example.py
sudo ./Adafruit_CharLCD_IPclock_example.py

jueves, 7 de febrero de 2013

[Proyecto] Cliente torrent con Transmission y Raspberrry Pi - Parte 2: Instalación de Raspbian y configuración inicial

En esta entrada nos vamos a centrar en instalar Raspbian en el Raspberry Pi y en configurar algunas opciones iniciales. La más importante será habilitar el servidor SSH para poder acceder a él remotamente.

Instalar Raspbian es tan fácil como seguir los pasos de la entrada que lo explicaba en su momento. Una vez hecho, introducimos la tarjeta en el Raspberry, conectamos un teclado, un monitor (sólo hará falta esta vez) y un cable de red, que conectaremos a su vez a nuestro router. Al encender el Raspberry, tras unos cargar el sistema nos saldrá el asistente de configuración inicial de Raspbian, que nos facilitará activar ciertas cosillas de una forma muy sencilla. Vamos a analizarlo por encima:

image

  • La primera opción del menú simplemente muestra un mensaje que explica básicamente lo que he contado aquí. Añade también que este configurador se puede usar más adelante, si queremos modificar alguna opción en concreto. Para ello, basta con escribir en el terminal: sudo raspi-config
  • La segunda opción es expand_rootfs y lo que hace es redimensionar la partición root de la tarjeta SD hasta ocupar todo el espacio disponible de la misma. Cuando hemos instalado el Raspberry en nuestra tarjeta, lo que hemos hecho es grabar una imagen del sistema operativo. Según la web oficial se necesita una tarjeta de al menos 2GB, por lo que la imagen estará pensada para no ocupar más de 2GB. Con esta opción ampliamos el espacio disponible de nuestro sistema operativo de modo que utilice todo el que haya disponible en la tarjeta.
  • La opción overscan se utiliza cuando al conectar el Raspberry con a la pantalla vemos que o bien la imagen ocupa menos, obteniendo bordes negros, o que ocupa más, y no vemos la imagen completa.
  • La opción configure_keyboard permite seleccionar qué distribución de teclado usaremos. Por defecto se usa la distribución americana, por lo que los acentos y varios símbolos no están donde estarían en un teclado español.
  • change_pass permite cambiar la contraseña del usuario pi (que inicialmente es raspberry)
  • change_locale permite cambiar el idioma por defecto del sistema. Inicialmente es inglés. Afecta al idioma que tendrán los programas cuando se instalen, los menús, etc.
  • change_timezone permite cambiar la zona horaria.
  • memory_split: El Raspberry tiene 256 o 512 MB de RAM. Esta memoria la utiliza por un lado la CPU y por otro la GPU. Esta opción permite establecer cuánta memoria usará la GPU, de modo que el resto será para la CPU.
  • overclock: Sirve para aumentar la frecuencia de la CPU del Raspberry. Tengo que informarme más porque leí en la página oficial que, al hacer overclocking, automáticamente se anulaba la garantía del Raspberry, pero que había una opción en las últimas versiones de Raspbian que la habían probado y que no la anulaba. Ten en cuenta que aumentar la frecuencia de la CPU puede reducir la vida útil del Raspberry.
  • ssh: Habilita o deshabilita el servidor SSH, permitiendo usar el Raspberry sin necesidad de pantalla o teclado.
  • boot-behaviour: por defecto, Raspbian se inicia en modo texto, y si queremos cargar el entorno gráfico debemos escribir startx en la consola. Mediante esta opción podemos hacer que el entorno gráfico se cargue automáticamente.
  • update: esta opción permite actualizar el propio asistente si tenemos conexión a Internet.

 

Configuración inicial con Raspi-config

Vamos a ponernos manos a la obra:

  1. Como hemos conectado el Raspberry a Internet con el cable de red, lo primero que haremos es comprobar si hay actualizaciones con la opción update.
  2. A continuación expandiremos el sistema de archivos con la opción expand_rootfs.
  3. Pondremos el teclado en español con configure_keyboard.
  4. Cambiamos la contraseña por defecto por una nueva. Intenta que sea segura, ya que es la que nos preguntará el sistema cuando queramos conectarnos desde Internet.
  5. Si queremos cambiar el idioma del sistema, lo haremos con la opción change_locale. Yo lo he dejado en inglés. Si queremos usar el español, bajaremos por la lista y seleccionaremos (pulsando espacio) es_ES.UTF-8, como se ve en la imagen. Para aceptar tendremos que usar tabulador e intro.image
  6. De un modo similar, seleccionaremos nuestra zona horaria con la opción change_timezone. En mi caso he seleccionado Europa y Madrid.
  7. Seleccionamos memory_split para repartir la RAM del sistema. Como estamos haciendo un cliente de torrent, que no va a necesitar la GPU para nada, podemos usar casi toda la RAM para la CPU. Yo he seleccionado 64 MB para GPU y el resto para la CPU, pero puedes usar incluso menos, especialmente si tu Raspberry es la versión de 256 MB.
  8. Seleccionamos la opción SSH y lo activamos.
  9. El resto de opciones las dejamos por defecto y seleccionamos finish. Finalmente reiniciamos la máquina escribiendo sudo reboot.

Al cabo de unos segundos, el sistema se reiniciará y aparecerá una terminal. Sólo nos queda averiguar la dirección IP y la MAC de nuestro Raspberry. Para ello escribimos: ifconfig eth0

image

Nuestra dirección IP es la que aparece después de inet addr: y nuestra MAC será la que aparece detrás de HWaddr. Las anotamos, ya que las vamos a utilizar para conseguir una IP fija en nuestra red.

Configurando router para conseguir IP fija en el Raspberry

Lo que vamos a hacer es decirle a nuestro router que queremos que nos reserve una dirección IP concreta para nuestro Raspberry, de modo que siempre que esté conectado al router, tendrá la misma IP.

Este procedimiento varía de un router a otro. En mi caso lo voy a explicar con un router ASL-26555 que es el que te da Movistar al contratar Fusión. De todas formas el proceso es similar con el resto de routers, aunque los nombres pueden variar un poco.

  1. Estando conectados al router por Wifi o Cable desde nuestro ordenador, accedemos a la web de configuración del router. En mi caso es http://192.168.1.1:8000/ aunque en otros routers puede ser simplemente http://192.168.1.1
  2. Accedemos con el usuario y contraseña del router. Esto también depende de cada modelo, y es muy recomendable cambiarlo, ya que sino cualquier persona conectada a nuestra red podría acceder a nuestro router simplemente consultando cuál es la contraseña por defecto del mismo.image 
  3. En mi caso, en la pestaña Local Network está la opción Add Static IP Address. Basta con escribir la MAC del Raspberry y la IP que queremos asignarle, marcar la opción Enable y pulsar Add/Apply. Con esto añadiremos el Raspberry a la lista de reserva de IP, que se puede ver justo debajo:
image

Con esto ya tenemos nuestro Raspberry configurado y listo para trabajar. Podemos desconectar la pantalla y el teclado, ya que no serán necesarios a partir de ahora.

Accediendo al Raspberry mediante SSH

¿Recuerdas que cuando estábamos configurando el Raspberry habilitamos la opción de SSH Server? SSH (Secure Shell) es un protocolo de comunicaciones que permite conectarnos a una máquina (como nuestro Raspberry) de forma remota. Esto quiere decir que podemos utilizar es Raspberry desde nuestro portátil, desde el móvil o desde un PC que esté en cualquier lugar con conexión a Internet.

Para ello, necesitamos un cliente SSH, que es un programa que nos permite conectarnos al Raspberry. Yo en Windows uso Putty, y en Linux puedes conectarte directamente desde un terminal.

Si usas Putty, al abrirlo te encontrarás algo parecido a ésto:

image

Aunque haya muchísimas opciones, usar Putty es tan sencillo como escribir la IP del Raspberry (la que configuramos antes en el router y vimos con ifconfig) en la casilla Host Name (or IP address) y pulsar Open.

Se abrirá una terminal donde al cabo de un momento y si todo ha ido bien,  mostrará: login as: Escribiremos pi y acto seguido nos preguntará por la contraseña, donde pondremos la contraseña que establecimos antes.

image

Deberíamos tener algo parecido a la imagen anterior. Si es así, ya tendremos todo listo para empezar a instalar y configurar nuestro cliente torrent en el Raspberry. Pero eso será en la siguiente entrada.

PD: He intentado que el tutorial sea lo más claro posible, pero ante cualquier duda, usad los comentarios.

miércoles, 30 de enero de 2013

[Proyecto] Cliente torrent con Transmission y Raspberry Pi - Parte 1: Introducción

Aunque la reciente llegada de MEGA puede revitalizar las descargas directas, desde hace años he sido partidario del uso de BitTorrent. Empecé a utilizarlo hace bastantes años, cuando el mundo de las descargas estaba dominado por eMule y, al principio no le terminaba de ver las ventajas respecto de aquel programa, que tenía buscador integrado y que permitía descargar los archivos de manera más cómoda.

Con el tiempo, he visto el declive de eMule, Kazaa, Pando, Peer2Mail y seguro que me dejo alguno. La ventaja del protocolo BitTorrent es que es descentralizada y por tanto no depende de un servidor central que pueda cerrar. El caso más claro lo tenemos con la difunta Megaupload, que de momento sigue cerrada y sin permitir a sus usuarios el acceso a sus propios archivos.

Otra ventaja de BitTorrent es que, a diferencia de un servidor de descargas convencional, la disponibilidad del archivo se incrementa con el aumento de personas compartiéndolo, disminuyendo así posibles congestiones debidos a descargas masivas. Esto ha hecho que muchas compañías, desde Blizzard hasta Canonical, hagan uso del protocolo BitTorrent para compartir actualizaciones para sus juegos o las versiones de su sistema operativo, respectivamente.

El inconveniente de este sistema es que si sólo nosotros tenemos un determinado archivo compartiendo por torrent, tenemos que tener un PC encendido con conexión a Internet o nadie podrá descargarlo. Aunque en muchos casos podemos simplemente descargar el archivo y apagar el PC, habrá otros en los que queramos dejar ese archivo compartiendo. Sin embargo, un PC convencional puede gastar unos 70~100W encendido, por lo que no es lo más adecuado si queremos tenerlo compartiendo por torrent a todas horas, ya que el gasto en electricidad es considerable.

Y ahí es donde entra el Raspberry Pi, que aunque poco potente, al fin y al cabo es un ordenador, con alguna peculiaridad, pero un ordenador al fin y al cabo. El Raspberry Pi consume muy poco, apenas 2 o 3W según las especificaciones (y como nosotros no utilizaremos la GPU, consumirá aún menos). Si le añadimos un HDD de bajo consumo como un WD Caviar Green o un HDD de 2.5’’ podemos tener un sistema para descargar y compartir por torrent barato de construir y con un consumo muy bajo.

Digo que el Raspberry Pi es un ordenador con peculiaridades porque existen algunos inconvenientes que limitarán en cierta medida el proyecto. A priori se me ocurren los siguentes:
  • Por lo que he visto en foros, al escribir en un HDD con formato NTFS, que es el predeterminado de Windows, la CPU del Raspberry Pi se pone en torno a un 90% y el rendimiento es pobre. Tengo que mirar si existe alguna solución, pero creo que optaré por formatear el HDD directamente en Ext3.
  • He visto también en varios sitios que sin una configuración adecuada, el Raspberry tiende a bloquearse descargando por torrent, debido seguramente a que no es capaz de gestionar el alto número de conexiones y peticiones de red. Tendré que averiguar porqué pasa y ver qué configuración es la adecuada para evitar esto. Ya he visto en varios foros configuraciones que en principio no dan problemas, pero quiero enterarme de todos modos cuál es exactamente el problema.
  • Aunque el Raspberry tiene 2 puertos USB 2.0 y un puerto de red RJ45, realmente los tres van por la misma línea de datos USB 2.0, por lo que es de esperar que la velocidad de descarga baje al transferir archivos y viceversa. De hecho, creo que una de las cosas que más echo de menos del Raspberry es un puerto SATA al que conectar un HDD y no depender de conexiones USB y menos compartidas. Un puerto Gigabit hubiese estado también muy bien, aunque si la línea de datos tiene el ancho de banda de un USB 2.0 no hubiese servido para mucho.
  • El Raspberry no tiene suficiente potencia en los puertos USB para alimentar un HDD, por lo que o bien habrá que usar un HDD con alimentación externa o, como recomiendan en la página principal, un Hub USB autoalimentado.
A pesar de que a priori parezcan muchos problemas, creo que se puede conseguir algo bastante decente, y además nos despreocuparemos del ruido, del calor generado, del consumo y del tamaño; no todo iba a ser malo.

Este proyecto será corto, aunque quizá lo amplíe en un futuro para añadir alguna nueva funcionalidad, por ahora, lo que iremos desarrollando en siguientes entradas será lo siguiente:
  1. Instalación de Raspbian y configuración inicial.
  2. Montaje de HDD para descargas y configuración.
  3. Instalación de Transmission Daemon y configuración.
  4. Acceder a los archivos descargados de forma remota.
Con esto tendremos ya el sistema listo y funcionando. Es posible que en un futuro pueda ampliar el proyecto añadiendo un servidor DLNA o similar, pero prefiero mantenerlo simple de momento, y asegurarme de que esté terminado en un tiempo razonable (no como el anterior proyecto de la radio, que sigue parado de momento).

Nos vemos en el siguiente post, donde instalaremos Raspbian y configuraremos nuestra red doméstica para conseguir una IP fija y una redirección del puerto 80 del router para poder utilizar más tarde la interfaz web de Transmission.

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.