Saltar al contenido

Leer datos de Arduino desde Python

Introducción

En este artículo vamos a ver cómo leer datos procedentes de una plataforma Arduino con Python. Para quienes no lo conozcáis, Arduino es una plataforma de hardware libre concebida para crear prototipos de manera rápida y fácil usando componentes electrónicos. Gracias a Arduino vamos a alejarnos un poco de lo que solemos ver en este blog, que es solo software, y vamos a poder interactuar con el mundo real de una manera más directa.

Este artículo nace gracias a mi reciente incorporación a Aerobot, el club de robótica de mi escuela, donde iré explorando las posibilidades de Arduino y Python 🙂

En esta entrada se han usado python 3.3.3, numpy 1.8.0, pyserial 2.7 y matplotlib 1.3.1.

Prefacio: ¿cómo funciona?

En este artículo no vamos a ver en detalle qué es Arduino o el lenguaje con el que se programa. Para ello os remito a gente que sabe mucho más que nosotros: al final del artículo incluyo unos enlaces que os pueden interesar si queréis profundizar en este tema. Sin embargo, sí que vamos a explicar brevemente cómo es el proceso de escribir un programa para Arduino, por razones que en seguida veremos.

Tal y como se detalla en la documentación, el proceso de compilación en Arduino funciona a grandes rasgos de la siguiente manera:

  1. Se escribe un programa (sketch) en el IDE de Arduino en C o C++.
  2. El IDE comprueba que la sintaxis es correcta y añade #include "Arduino.h" y una función main() propia de la placa.
  3. El programa se compila con avr-gcc y se manda el binario a la placa.
  4. Una vez Arduino tiene el programa y mientras esté alimentado, el programa se ejecutará, normalmente de manera indefinida.

Esta explicación viene para aclarar que, al menos en este artículo, no vamos a programar nuestra placa en Python. Por el proceso que hemos visto, las únicas maneras de hacer esto serían:

Con el segundo método no tenemos disponibles todas las funciones de Arduino, de modo que solo sirve para prototipar programas. Hoy me voy a olvidar de esto y voy a usar Arduino para programar la placa y Python para obtener los datos.

En esta entrada se han usado python 3.3.3, pyserial 2.7.

Comunicación por puerto serie con pySerial

El código mínimo

Para comunicarnos con nuestra placa Arduino por puerto serie utilizaremos la biblioteca pySerial, disponible también en PyPI. Una vez que tenemos nuestra placa enchufada, lo único que necesitamos para acceder a ella es esto:

import serial
arduino = serial.Serial('/dev/ttyACM0', baudrate=9600, timeout=1.0)

La clase Serial puede recibir muchos parámetros, pero los fundamentales son estos:

  • El puerto (en nuestro caso '/dev/ttyACM0') que identifica la placa. En Windows sería 'COM3'.
  • La velocidad en baudios (en nuestro caso 9600). Algunos valores estándares son 9600, 19200, 38400…
  • El timeout (en nuestro caso 1 segundo) o tiempo máximo de espera para una lectura. Es importante poner un valor mayor que 0 para que en caso de error la lectura no «se cuelgue» indefinidamente.

Nota: La función readline() esperará a que se reciba una nueva línea, independientemente del timeout.

A partir de este momento podemos leer de la variable arduino como si fuera un fichero normal (de hecho los objetos Serial heredan de RawIOBase). Supongamos que en nuestra placa Arduino hemos cargado el ejemplo AnalogInOutSerial, y que por tanto nuestra placa está escribiendo datos al puerto serie:

while True:
    line = arduino.readline()
    print(line)

Y la salida será algo así:

$ python arduino_python.py
b'sensor = 0toutput = 0rn'
b'sensor = 0toutput = 0rn'
b'sensor = 0toutput = 0rn'
b'sensor = 0toutput = 0rn'
b'sensor = 0toutput = 0rn'
^CTraceback (most recent call last):
  File "arduino_python.py", line 34, in
    line = arduino.readline()
  File "arduino_python.py", line 22, in readline
    time.sleep(0.1)
KeyboardInterrupt

Y podríamos dejarlo aquí, pero podemos hacer el código un poco más robusto.

Algunas mejoras

Con esto ya estamos leyendo los datos, pero podemos mejorar algunas cosas:

  • La función readline devuelve un objeto bytes: por eso vemos “rn” al final, los caracteres especiales de terminación de línea. Desde Arduino solo podemos mandar caracteres ASCII, así que podemos decodificar los bytes a una cadena. No obstante, habrá que indicar que en caso de recibir un byte erróneo no queremos que la función falle, sino que escriba algo como �.
  • Al detener el programa (Ctrl+C en la consola) no queremos ver una traza de error. Podemos capturar la excepción KeyboardInterrupt con un bloque try-except.
  • Faltaría incluir una llamada a arduino.close() al final del programa por seguridad, pero en vez de eso podemos utilizar la sentencia with y Python se encarga de cerrar la comunicación por nosotros.

El código final sería este:

https://gist.github.com/Juanlu001/8256958

¡Y ya lo tenemos! Esto es todo lo que necesitamos para leer datos desde una placa Arduino 🙂

Precauciones en el MundoReal™

La conexión a Arduino no siempre funcionará de manera ideal, y puede haber momentos en los que recibamos bytes erróneos. Hay un par de comentarios importantes que hacer al código anterior:

  • No se puede leer la placa desde dos fuentes a la vez. Si mientras corre este programa abres el monitor Serial del IDE de Arduino uno de los dos acabará fallando.
  • Podríamos iterar sobre el flujo de datos con un bucle for line in arduino, y Python se encargaría de ir invocando sucesivamente la función readline, pero este método puede fallar si se recibe algún byte corrupto. He comprobado que es más seguro hacerlo manualmente.

Esto afecta también a la hora de almacenar los datos recibidos, como veremos en la sección siguiente.
Por otro lado, hay otro asunto que hay que tener en cuenta: la placa Arduino se reinicia automáticamente al abrir una conexión por puerto serie, y al probar este código en Linux estaba dándome problemas. En los primeros segundos, entre el inicio de la conexión y el reinicio de la placa recibía datos erróneos. Por eso pregunté en Stack Overflow cómo reiniciar manualmente la placa usando pySerial y me dieron la solución:

import serial
import time
arduino = serial.Serial('/dev/ttyACM0', baudrate=9600, timeout=1.0)
# Provocamos un reseteo manual de la placa para leer desde
# el principio, ver http://stackoverflow.com/a/21082531/554319
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)

De este modo, si empezamos a leer justo después deja de haber problemas. Dejo esta solución aquí en caso de que otros hayan experimentado este problema; en Windows, por ejemplo, no es necesario.

Almacenando los datos

Una vez que ya podemos leer los datos desde Arduino, vamos a dar algunas ideas sobre cómo almacenarlos y cómo procesarlos.

Leer un bloque de datos y cerrar

El caso más sencillo es leer un número predeterminado de líneas desde la placa y cerrar la conexión, para después procesar esos datos de la manera que queramos. Para ello, lo mejor es que empleemos un array de NumPy y que incorporemos los datos utilizando la función np.fromstring. Esta función acepta como argumentos la cadena que se va a leer y un argumento que indica cuál es el separador entre datos. Para cadenas ASCII como los que manejamos ahora, debemos especificar que los datos están separados por espacios. Utilizando de nuevo el sketch AnalogInOutSerial pero con las líneas de escritura de esta forma:

// print the results to the serial monitor:
//Serial.print("sensor = " );
Serial.print(sensorValue);
Serial.print("t");
Serial.println(outputValue);

Este sería el código Python:

import time
import serial
import numpy as np

N = 10
data = np.zeros((N, 2))
# Abrimos la conexión con Arduino
arduino = serial.Serial('/dev/ttyACM0', baudrate=9600, timeout=1.0)
with arduino:
    ii = 0
    while ii < N:
        try:
            line = arduino.readline()
            if not line:
                # HACK: Descartamos líneas vacías porque fromstring produce
                # resultados erróneos, ver
                # https://github.com/numpy/numpy/issues/1714
                continue
            data[ii] = np.fromstring(line.decode('ascii', errors='replace'), sep=' ')
            ii += 1
        except KeyboardInterrupt:
            print("Exiting")
            break
        print(data)

¿Por qué usamos un contador con un bucle while en este caso, que parece que no queda muy pythonico? El motivo es que en algunos casos tendremos que descartar líneas (como se ha visto en el código por culpa de un fallo de NumPy) y esta estructura es más adecuada.

Procesar una cola de datos

Otra cosa que podemos necesitar es almacenar los datos en una cola e ir procesándolos sobre la marcha. Esto puede ser interesante si, por ejemplo, queremos representar gráficamente en tiempo real la evolución de una variable, pero solo nos interesan los últimos segundos. En este caso el módulo collections de la biblioteca estándar de Python nos proporciona la estructura deque (double-ended queue, que podríamos traducir por cola doblemente terminada), que funciona de manera que si añadimos elementos al final, a partir de una cierta longitud máxima se descartan elementos del principio.

Si quisiéramos usar una deque, el código sería este:

N = 10
data = deque(maxlen=N)  # deque con longitud máxima N
while True:
    # ...
    data.append(dd)  # Añadimos elementos al final de la cola

Y para usar arrays de NumPy, podemos usar un truco para ir recorriendo cíclicamente sus elementos:

N = 10
data = np.zeros(N)  # deque con longitud máxima N
ii = 0
while True:
    data[ii % N] = dd  # Recorremos cíclicamente el array
    ii += 1

Puedo tener dos efectos diferentes: con buff[i % N] tengo «efecto barrido», que sería similar a como representa los datos un osciloscopio, y con deque tengo «efecto pasada», que podríamos comparar con una ventana que se va desplazando.

Finale: Representación en tiempo real

Ahora no tenemos más que integrar todo lo que hemos visto arriba, y ya podremos representar en tiempo real datos procedentes de una placa Arduino con Python.

import time
import warnings
from collections import deque
import serial
import numpy as np
import matplotlib.pyplot as plt

N = 200
data = deque([0] * N, maxlen=N) # deque con longitud máxima N
#Creamos la figura
plt.ion()
fig, ax = plt.subplots()
ll, = ax.plot(data)

# Abrimos la conexión con Arduino
arduino = serial.Serial('/dev/ttyACM0', baudrate=9600, timeout=1.0)
arduino.setDTR(False)
time.sleep(1)
arduino.flushInput()
arduino.setDTR(True)

with arduino:
    while True:
        try:
            line = arduino.readline()
            if not line:
            # HACK: Descartamos líneas vacías porque fromstring produce
            # resultados erróneos, ver
            # https://github.com/numpy/numpy/issues/1714
                continue
            xx, yy = np.fromstring(line.decode('ascii', errors='replace'), sep=' ')
            data.append(yy)
            ll.set_ydata(data)
            ax.set_ylim(min(data) - 10, max(data) + 10)
            plt.pause(0.001)
        except ValueError:
            warnings.warn("Line {} didn't parse, skipping".format(line))
        except KeyboardInterrupt:
            print("Exiting")
            break
Y este es el resultado:


Aunque mejor que lo ejecutes en tu ordenador 😉

Para saber más

No soy ni mucho menos un experto en Arduino. Si estáis buscando un blog especializado os recomiendo GeekyTheory, y si queréis saber más acerca de Python + Arduino os interesará la charla de Núria Pujol sobre tracking GPS con Python y Arduino que dio recientemente en el grupo Python Barcelona. Además Núria accedió amablemente a revisar el borrador de esta entrada, ¡mil gracias! 🙂
Y tú, ¿tienes ya pensado cómo vas a combinar Python y Arduino? ¿Alguna idea o proyecto interesante que nos quieras contar? ¡Escríbenos en los comentarios!

27 comentarios en «Leer datos de Arduino desde Python»

  1. tengo la idea de hacer un proyetco con arduino y python y su rastreo por gps, sera que me pueden dar una ayuda sobre los requisitos

  2. Saludos, excelente ejemplo pero tengo una duda desde hace tiempo, ¿existe algún tipo de función o evento de la librería pyserial que se ejecute si hay una entrada de datos por el puerto serie?. Yo he trabajado con Visual Basic 6.0 y el tiene algo así.

  3. Hola, muchas gracias por el artículo, es muy interesante.
    Tengo una duda que me “corroe” 🙁
    Me gustaría que mi ordenador realizara alguna tarea cuando recibe un determinado carácter por el puerto serie desde el arduino.
    La idea es la siguiente:
    El arduino recibe una señal “High” en uno de sus pines (por ejemplo el pin 1) y escribe en el puerto serie: “1”.
    Entonces, el programa, detecta este número y lanza un comando LINUX en el ordenador, por ejemplo: beep
    El problema es que no tengo ni idea de Python y si sé C, pero hacer todo esto en C es demencial.
    ¿Sabrías como hacer esto de forma sencilla?
    Me estoy volviendo loco buscando por Internet y no encuentro nada.
    Muchas gracias

  4. Muy interesante la entrada.
    Desde hace tiempo uso la plataforma Arduino para mis proyectos aunque uso para su programación ANSI C y como IDE el Atmel Studio.
    Llevaba un tiempo buscando algo similar, integración entre Arduino y Python lenguaje que estoy aprendiendo en la actualidad.
    He intentado realizar algo similar a lo que ya hice en su día con Arduino y Matlab, pero con Python. Quizás un poco ambicioso aún para mi nivel actual de Python pero tu entrada me ha aclarado bastante.
    http://arduinoyansic.blogspot.com.es/2015/05/analisis-espectral-de-vibraciones.html
    Saludos.

    1. Gracias a ti por tu comentario Ismael, en cuanto consigas hacer algo parecido avísanos o mejor, anímate a dar una charla en Python Sevilla 🙂 ¡Un saludo!

    1. Hola Carla, te recomiendo que formules tus dudas en es.stackoverflow.com de la forma más concreta y detallada posible para que te podamos ayudar allí 🙂 ¡Un saludo!

    1. Hola Lupita, ¿qué código estás usando exactamente? Danos un poco más de información para que te podamos ayudar. Efectivamente puede haber algunas cosas que no funcionen bien en Python 2, si afinas un poco intentamos solucionar el problema 🙂

  5. Pingback: Analizador de Vibraciones | electroprogramación

  6. hola, tu que has trabajado con arduino, que opinas de un proceso donde se reciba y envien datos entre arduino y phyton durante todo el dia, crees que sea lo suficientemente estable ?
    gracias

    1. ¡Hola Camilo! Seguramente no habrá problemas si tienes cuidado en no crear estructuras de datos que crezcan indefinidamente en memoria. Y si no te queda más remedio, tendrás que vigilar el uso de recursos. ¡Saludos!

    1. Hola Andinito, hace tiempo que no trabajo con Python 2 así que si alguna línea en concreto te falla avísanos 🙂 ¡Un saludo!

  7. Buenisimo, gracias por la información.
    Hay alguna manera de recibir datos de dos arduinos diferentes conectados en diferentes puertos sin que se pisen?
    /dev/ttyACM0 y /dev/ttyACM1 así ambos usan las variable arduino, si necesidad de crear un código python para cada arduino que conecte.
    Muchas gracias

    1. Hola Felix, supongo que tendrás que instanciar dos objetos Arduino diferentes, uno para cada puerto. ¡Saludos!

  8. Buenas noches, soy nueva en este mundo de la programacion, con mucho investigar he podido hacer un sensor se temperatura en arduino y mostrado la informacion por el puerto serial desde Python. Pero ahora quisiera saber si hay alguna forma o metodo para mostrar los datos por medio de la interfas grafica de PyQt5, he buscado mucha informacion pero es muy poco o nula. Si alguien me puede ayudar le quedare muy agradecida . adjunto el codigo. Muchas gracias.
    import sys
    import serial
    from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QPushButton
    from PyQt5 import uic
    from PyQt5.QtGui import QFont #asignar tipo de Fuentes
    from PyQt5.QtCore import Qt # Modulo para modificar el cursor.
    import ctypes
    #serie = serial.Serial(‘COM3’, 9600, timeout = 10)
    class Ventana(QMainWindow):
    def __init__(self):
    QMainWindow.__init__(self)
    uic.loadUi(“prueba-sensor.ui”, self)
    self.setWindowTitle(“Sensor de Temperatura interfas Grafica”)
    def showEvent(self, event): #ojo al comentar las funciones no puede generar error de identation
    #se crea la funcion showEvent y se ingresa al tabel para mostrar el texto
    serie = serial.Serial(‘COM3’, 9600, timeout = 10)# solo con este codigo se muestra el puerto serie
    while True:# solo con este codigo se muestra el puerto serie
    print (serie.readline())# solo con este codigo se muestra el puerto serie
    self.sensor.setText(“serie”, serie)
    app = QApplication(sys.argv)
    ventana = Ventana()
    ventana.show()
    app.exec_()

  9. Buen tutorial Juan, yo estoy haciendo comunicacon arduino – Pc con python y la hacer perfecto, pero quiero que el dato que se envie lo compare con un valor que yo voy a poner y si se cumple haga algo en la pc. Yo intento hacer esto para comprar el dato pero luego me di cuenta que el dato no se guarda y lo que se guarda es un espacio en blanco
    dato_arduino= (line.decode(‘ascii’, errors=’replace’), sep=’ ‘)
    if (dato_arduino == “b’4077715200\r\n'”):
    otras operaciones….
    Buen tutorial Juan, yo estoy haciendo comunicacon arduino – Pc con python y la hacer perfecto, pero quiero que el dato que se envie lo compare con un valor que yo voy a poner y si se cumple haga algo en la pc. Yo intento hacer esto para comprar el dato pero luego me di cuenta que el dato no se guarda y lo que se guarda es un espacio en blanco
    dato_arduino= (line.decode(‘ascii’, errors=’replace’), sep=’ ‘)
    if (dato_arduino == “b’4077715200\r\n'”):
    otras operaciones….
    No me entra todo el texto he subido la nota aqui http://www.heypasteit.com/clip/31GT , si puedes me puedes ayudar te lo agradezco

  10. Hola me agradó el blog, tengo en mente un proyecto pero no encuentro alguna solución.
    Quiero leer los tags de un módulo RFID MFRC522 en python, sólo que no encuentro librerías que interactuén con python, sólo para arduino. ¿Crees que sea factible leer los tags de los RFID en arduino y mandarlos a python mediante el método que describes en tu blog y compararlo en una base de dato en python?
    Saludos.

    1. Hola Arturo, efectivamente puedes hacerlo así o utilizar otro tipo de hardware, como Microbit o Raspberry Pi. ¡Saludos!

Los comentarios están cerrados.

Pybonacci