Introduction: Plotting Real-time Data From Arduino Using Python (matplotlib)

Picture of Plotting Real-time Data From Arduino Using Python (matplotlib)

Arduino is fantastic as an intermediary between your computer and a raw electronic circuit. Using the serial interface, you can retrieve information from sensors attached to your Arduino. (You can also send information via the serial interface to actuate circuits and devices (LEDs, relays, servos, etc.) connected to your Arduino.) Once you have the data in your computer, you can do all sorts of things with it – analyze it, display it, or share it on the internet, for instance.

In this instructable, I will be reading and displaying analog data from a pair of LDRs connected to an Arduino. Attached is the schematic.

The Arduino sketch is very simple – it just reads the values from analog pins A0 and A1 (in the range [0, 1023]) and prints it to the serial port.

Here is the code:

https://gist.github.com/electronut/5641938

The serial port sends values in the format:

512 300
513 280
400 200
...

On the computer side, I need to read these values, and plot them as a function of time. I am using Python and the Matplotlib library for this. I wanted to display this as a scrolling graph that moves to the right as data keeps coming in. For that, I am using the Python deque class to keep and update a fixed number of data points for each time frame.

You can see the full implementation here:

https://gist.github.com/electronut/d5e5f68c610821e311b0

Comments

RitwikaM (author)2016-05-26

Hello Elecronut,

Thank you for the code. I am very new to this, so I have been facing quite a few issues with running the code. I keep getting errors while running the argparse command. I replaced port with COM3 like below, but keep getting errors!

def main():

# create parser

parser = argparse.ArgumentParser(description="LDR serial")

# add expected arguments

parser.add_argument('--COM3', dest='COM3', required=True)

Error in python:

usage: test_python.py [-h] --COM3 COM3

test_python.py: error: argument --COM3 is required

My problem is I am not sure where to replace strPort or port with COM3.

Please help!

Thanks!

Ritwika

electronut (author)RitwikaM2016-05-26

Without changing the code, run it as:

>python ldr.py --port COM3

What is the output?

RitwikaM (author)electronut2016-05-27

Thanks, it works!!! I am trying now to add the data to a csv file. Thank you for all the help again!

fmarengo (author)RitwikaM2017-07-12

Hi RitwikaM

Did you happen to execute the python script from a python editor (e.g Spyder)? If your answer is yes, could you please send me your code?
I would like to modify the code so as to plot ONE data series from port COM10. Also, I would like to save the data series on the local HDD.
Thanks!

electronut (author)RitwikaM2016-05-27

Awesome!

fmarengo (author)2017-07-12

Dear electronut,

I tried to implement your code in my PC using Spyder editor, but unfortunately it gives me an error (see at the bottom). I also replaced

strPort = args.port

by

strPort = "COM10"

since COM10 is the port I am using in my PC (Windows7 64bits). Even in this case, the error is the same. How can I solve it from the editor, please? Thanks!

usage: ldr.py [-h] --port PORT
ldr.py: error: argument --port is required
An exception has occurred, use %tb to see the full traceback.
SystemExit: 2
Dylan TaylorA (author)2017-02-11

Works 'out of the box' which is seriously just fantastic in the world of analog and digital communication. Serial port can be found by looking in your arduino IDE under tools-> serial port. for me it was /dev/ttyACM0

Great work!

aghosca (author)2017-01-24

Great! How to reverse the flow of the graph from right to left?

cristhianc12 (author)2016-10-17

good day apologize for my English, I'm from Colombia I am currently developing my thesis project and have been almost impossible to create the code acquisition 4 sensors, I wonder if I can use some of their code in my project. and I wonder if there is any way to predefine the port COM6 not run the program from the console. thank you very much for contribution.

Guddi11 (author)2016-09-06

Hi,

I also tried your code and I also get a nice Tk Window, BUT no plot…
apparently analogPlot.update is not called or at least does not do
anything. I already tried some things to fix it, but without success.
Any hint?

Could it be the backend, that doesn’t want to show me anything?

Otherwise, a nice, easy and understandable code!!! Well done!

Thanks in advance.

Robert

Guddi11 (author)Guddi112016-09-07

Woohooo! I made it - it was only some wrong or missing dependencies between matplotlib and Tkinter. Found some nice solution for that here:

http://www.pyimagesearch.com/2015/08/24/resolved-m...

Thanks again!

Robert

batmanDIY made it! (author)2016-06-16

Awesome....neat and quick way without using octave or matlab

russ_hensel (author)2016-01-23

I love the combination of Python and the Arduino. So I have created a collection about it. I have added your instructable, you can see the collection at: >> https://www.instructables.com/id/Arduino-and-Pytho...

JordiG2 (author)2015-06-16

Hello elecronut,

Thank you for sharing. I like your project and I will use with my robot.

I have a ultrasonic attached to a servo to mapping obstacles. I send to serial port to numbers: angle of the servo and detected distance. Now, I want to graphic the obstacles.

I try your code but I need a XY praph. I mean, use the first number (angle) for X and the second number (distance) as Y.

I tried to make some modification but without any success.

Could you give me any suggestions?

Thank you in advance!

electronut (author)JordiG22015-06-16

Hello,

You need to specify the axes data differently. Take a look at the matplotlib tutorial below:

http://matplotlib.org/users/pyplot_tutorial.html

Regards

JordiG2 (author)electronut2015-06-17

Thank you!

Now I can see the problem. I have made some changes and... it works :)!

DanielleC4 (author)JordiG22015-11-25

Hello, I am currently working with the same thing but in a different project. I am just a newbie in python programming would you mind if I ask a copy of the changes you made to make the x axis have a different value instead of time or could you tell me how? I am trying to plot x-axis as velocity and y-axis as position for a phase plot in my case study. Thank You in Advance.

By the way I've already tried this line: a1,=ax.plot([0], [1]), 0-velocity and 1-position, however i didn't get anything on the plotting window.

electronut (author)DanielleC42015-11-26

Hello,

The axes can be anything. I've created an example for you - an animation that shows an oscillating circle:

https://gist.github.com/electronut/332b9097179f33c...

For phase plots, also take a look at:

http://kitchingroup.cheme.cmu.edu/blog/2013/02/21/...

DanielleC4 (author)electronut2015-11-29

Thank you for the example you've made, i will try this in my vertical spring oscillation with arduino, hoping it would work :)

electronut (author)DanielleC42015-11-30

Great, good luck!

electronut (author)JordiG22015-06-17

Awesome!

ice_lin (author)2015-11-18

Hello, electronut,

Thanks for your tutorial. This code works.

However, when my arduino device writes the serial at 200Hz, I notice there is a significant delay (about 7 seconds) of drawing.

I have tried to reduce the interval to 1, but it does not solve this problem.

is there any way to reduce such delay?

electronut (author)ice_lin2015-11-18

Hello,

I don't think the plot will be able to keep up at 200 Hz. Have you tried blit=True?

ice_lin (author)electronut2015-11-19

yes, I have tried blit=true, it helps, but does not solve it completely.

I was wondering:

1)what is the reason for such degraded performance? what is the bottleneck?

2) is there any way to draw real data sampled at high frequency, i.e., 300 Hz

electronut (author)ice_lin2015-11-19

Matplotlib I don't think was designed to update animations at the rate you mentioned. If you need to draw things that update very fast, you'll have to look a low level API - something which uses OpenGL for example. Here's one:

http://pyqtgraph.org/

agu3rra made it! (author)2015-09-19

Made it work. I find it odd, that the performance degrades when removing the delay between reads. I'll try to change the code for a single Ain read in order to see if I can increase the baud rate on the serial comms to get higher sampling rates. Thanks for your post! :)

electronut (author)agu3rra2015-09-19

Great!

putz182 (author)2015-08-26

The code work, but when I put blit=True for better performance, I get overlapped plots. Probably missing init_func?

electronut (author)putz1822015-08-26

Probably the older frame is not getting cleared in the animation. I did not use this flag because it wasn't working well across different OS.

putz182 (author)electronut2015-08-26

I've made a similar program but with 6 subplots (12 arduino variáveis). It is almost impossible to run without blit. I think init function can fix it since it works for a single plot, but I have no idea how to perform it since init should stay inside the class and must receive some parameters. The documentation doesn't cover how to use init_func with parameters. Can you point some direction or how to perform serial communication outside a class.

electronut (author)putz1822015-08-26

I am not sure what you mean by init_func. As for serial comms, you can just take the code out of the class:

ser = serial.Serial(strPort, 9600)

line = ser.readline()

# close when done

ser.flush()

ser.close()

mba7 (author)2014-09-21

I made an equivalent tool in python that print real time data from ADXL345 accelerometer.

https://github.com/mba7/SerialPort-RealTime-Data-P...

may be it will be helpful for someone

MauiJerry (author)mba72015-08-12

sweet!!! was just setting up to do just such an adxl345 project (actually 2 of them but thats advanced version). Will be back with more comments as I progress

AnılÖ1 (author)2015-06-23

Hello electronut,

I am trying to plot x and y axis of my joystick sensor.But I didn't understand this line;

parser.add_argument('--port', dest='port', required=True)

What should I write to '--port' section?

Thank you

electronut (author)AnılÖ12015-06-23

Hello,

The --port argument is for your serial port name. (For eg. something like COM3 under windows or /dev/tty.* under OS X) One easy way to find this information is look under Tools->Port in the Arduino software.

Regards

rohankotwani made it! (author)2015-04-11

I really have finally finished making this work. I would like to thank @bartons2 for asking those noob questions. They really helped me.. Everything to make this work is in this comments.

I used

python-mserial.tools.list_ports

python-mserial.tools.miniterm -h

(1)I used this to list the ports and find the one I needed. (this is a given from documentation). (2) I used this to figure out how to output the port. I also have to make sure the output data on serial screen was in correct format.

My ploy data is analog, and I was originally receiving input every 33 mili-seconds. The plot could not keep up. I lowered it to 150.

I will now try to figure out how to make it discrete. The plot is of a position sensor, and I just put my hand in front of it.

Wrrr 10-G (author)2014-11-30

Hello Electronut,

Thanks for this excellent foolproof ;) howto, I had my 5 MQ135 gas sensors up and running in no time.
Could
you point me in the right direction if I wanted to have your program
additionally store / log the values it receives from the serial device,
in a separate log file?

Thanks for your time, \W.

electronut (author)Wrrr 10-G2014-11-30

I am glad my code was useful to you. For logging data, you can do something like this:

>>> f = open('log.txt', 'w')
>>> str = "10 100\n"
>>> f.write(str)
>>> f.close()
>>> exit()

Do the open() right after serial.Serial() and do the file write in update() after retrieving line from serial port.

Hope that gets you started. Good luck!

Wrrr 10-G (author)electronut2015-03-01

Dear electronut,

It's bin a while, I ordered some new MQ135's and started my project again last week. The plotting still works fine with your original program. However, I have entered the code lines to additionally log to a file as you suggested but "NameError: global name 'f' is not defined" keeps bugging me. I am doing some very basic thing wrong, but my mileage is way too low to figure it out myself. Could you help me out?
Not sure I'm allowed to paste the code in this box, but here goes nothing.. My alterations are commented with double hashes ##.

"""
pyplotlog_v2.py

Display analog data from Arduino using Python (matplotlib)
... and log the serial output to a file as well
5 MQ135 airquality sensors simultaneous measuring
Author: Mahesh Venkitachalam
Website: electronut.in

"""

import sys, serial, argparse, threading
import numpy as np
import time
from time import sleep
from datetime import datetime
from collections import deque

import matplotlib.pyplot as plt
import matplotlib.animation as animation

# plot class
class AnalogPlot:
  # constr
  def __init__(self, strPort, maxLen):
      # open serial port
      self.ser = serial.Serial(strPort, 9600)
      f = open('probe.log', 'w')  ## open log file, write "w" or append "a"
      str="a0+' '+a1+' '+a2+' '+a3+' '+a4+'\n" ## parse serial string to be logged
      self.av = deque([0.0]*maxLen)
      self.aw = deque([0.0]*maxLen)
      self.ax = deque([0.0]*maxLen)
      self.ay = deque([0.0]*maxLen)
      self.az = deque([0.0]*maxLen)
      self.maxLen = maxLen

  # add to buffer
  def addToBuf(self, buf, val):
      if len(buf) < self.maxLen:
          buf.append(val)
      else:
          buf.pop()
          buf.appendleft(val)

  # add data
  def add(self, data):
      assert(len(data) == 5)
      self.addToBuf(self.av, data[0])
      self.addToBuf(self.aw, data[1])
      self.addToBuf(self.ax, data[2])
      self.addToBuf(self.ay, data[3])
      self.addToBuf(self.az, data[4])

  # update plot
  def update(self, frameNum, a0, a1, a2, a3, a4):
      try:
          f.write(str)  # write to log
          f.close()     # close log
          exit()        # exit logwrite (?)
          line = self.ser.readline()
          data = [float(val) for val in line.split()]
          # print data
          if(len(data) == 5):
              self.add(data)
              a0.set_data(range(self.maxLen), self.av)
              a1.set_data(range(self.maxLen), self.aw)
              a2.set_data(range(self.maxLen), self.ax)
              a3.set_data(range(self.maxLen), self.ay)
              a4.set_data(range(self.maxLen), self.az)
      except KeyboardInterrupt:
          print('exiting')

      return a0,

  # clean up
  def close(self):
      # close serial
      self.ser.flush()
      self.ser.close()

# main() function
def main():
  # create parser
  parser = argparse.ArgumentParser(description="LDR serial")
  # add expected arguments
  parser.add_argument('--port', dest='port', required=True)

  # parse args
  args = parser.parse_args()

  #strPort = '/dev/ttyACM0'
  strPort = args.port

  print('reading from serial port %s...' % strPort)

  # plot parameters
  analogPlot = AnalogPlot(strPort, 5000)

  print('plotting data...')

  # set up animation
  fig = plt.figure()
  ax = plt.axes(xlim=(0, 5000), ylim=(0, 1023))
  a0, = ax.plot([], [])
  a1, = ax.plot([], [])
  a2, = ax.plot([], [])
  a3, = ax.plot([], [])
  a4, = ax.plot([], [])
  anim = animation.FuncAnimation(fig, analogPlot.update,
                                 fargs=(a0, a1, a2, a3, a4),
                                 interval=5000)
## Taken from:
# https://www.instructables.com/id/Using-an-Arduino-and-Python-to-plotsave-data
# the following does not work:
#  row = (a0, a1, a2, a3, a4)   #combines lists together
#  row_arr = np.array(row)      #creates array from list
#  np.savetxt("probe.log", row_arr)  #save data in file
##

  # show plot
  plt.show()

  # clean up
  analogPlot.close()

  print('exiting.')


# call main
if __name__ == '__main__':
  main()

electronut (author)Wrrr 10-G2015-03-01

Make `f` a member of the AnalogPlot class. So use `self.f` everywhere and it should be fine.

CopernicusM (author)2014-12-07

Hi Electronut,

I appreciate your spirit to share your code with us :-) However, you can make your code more simple, faster, no error outputs when you hit keyboard interrupt, and naturally save the output file as a text file :-)

I would probably not use class objects since our task is so simple, but you can keep the parsers (user inputs from terminal). Here is my version with only two methods (your Arduino is required to have 2 outputs, for instance V0 and V1, but you can simply change the number of expected columns from Arduino):

___________________________________________________________

import sys, serial, argparse
import numpy as np
from time import sleep
from collections import deque

import matplotlib.pyplot as plt
import matplotlib.animation as animation

# constants
maxLen = 200
v0 = deque([0.0]*maxLen)
v1 = deque([0.0]*maxLen)
#ardtime = deque([0.0]*maxLen)

strPort = '/dev/ttyACM0'
ser = serial.Serial(strPort, 9600)

# set up animation
fig = plt.figure(1, figsize=(15,10))
ax = plt.axes(xlim=(0, 200), ylim=(0, 1023))
line_1, = ax.plot([], [], 'ko-')
line_2, = ax.plot([], [], 'ro-')

# initialization function: plot the background of each frame
def init():
line_1.set_data([], [])
line_2.set_data([], [])
return line_1, line_2,

# animation function. This is called sequentially
def animate(i):
try:
line = ser.readline()
data = [float(val) for val in line.split()]
if(len(data) == 2): # expected no. of columns from Arduino

###################
# save data locally and put header
my_file='alpha_step_rd.txt'
if i==0:
thefile = open(my_file, 'w')
thefile.write('Microcontroller time [ms], Arb. unit [bits]\n')
thefile.close()
with open(my_file, 'a') as thefile:
thefile.write("%s\n" % line.strip())
###################

v0.pop()
v0.appendleft(data[1])
v1.pop()
v1.appendleft(data[2])
'''
ardtime.pop()
ardtime.appendleft(data[0])
'''
line_1.set_data(range(maxLen),v0)
line_2.set_data(range(maxLen),v1)
except KeyboardInterrupt:
print('exiting')

return line_1, line_2

# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init, interval=50, blit=True)

string_1 = ''.join([r'$\Delta$time $\approx$ Arduino_delay'])
plt.xlabel(string_1, fontsize=20)
plt.ylabel("Bits, arb. units", fontsize=20)
plt.tick_params(axis="both", labelsize=20)

# show plot
plt.show()

# flush and close the serial port
ser.flush()
ser.close()

electronut (author)CopernicusM2014-12-07

Thanks, but your code indentation is all mangled, making it hard to understand. (I recommend creating a github gist.)

Also, a few points:

1. blit=True does not work across all platforms. On OS X it crashes for me, which is why I did not use the flag.
2. Using classes makes your code modular and much easier to comprehend.
3. Your code doesn't support command line args.

CopernicusM (author)electronut2014-12-08

Hi electronut,

My code has no parsers, that's correct. It is just because I prefer to make changes and typing in the editor :-)

Your functions "addToBuf" and "add" can be included in the "update" function by adding only 3 lines. The most importing thing to remember is that matplotlib method FuncAnimation expects array returns from method "set_data" :-) The rest of the coding is just matter of taste.

All the best,

Copernicus

bartons2 (author)2014-11-11

Hi, total newb here. When I run your scripts, I get:

Traceback (most recent call last):

File "/Users/barton/Documents/Arduino/ldr.py", line 10, in <module>

import sys, serial, argparse

ImportError: No module named serial

Using a Mega with macbook and OSX10. I can see from discussion below that Python doesn't know which serial port I am using. Where do I put that in?

electronut (author)bartons22014-11-11

Hello,

I think you need to install pyserial:

http://pyserial.sourceforge.net/pyserial.html#inst...

Also, your Arduino IDE Tools->Serial Port will tell you the name of the serial port used on your Mac. My code takes this as the --port command line argument.

Good luck!

bartons2 (author)electronut2014-11-13

Thanks, you were right. I was missing many elements of Python (which I've never used before). The code runs and makes a plot with no data. The plot doesn't look like yours (see pic). I've checked the serial window in Arduino and the data is flowing. I changed the line about the serial port to strPort = '/dev/tty.usbmodem1411' which is what my port is called in the Arduino IDE. Any more advice? I appreciate the help. If it matters, I am building a logger for my fermenter.

electronut (author)bartons22014-11-13

When you deal with serial port (or other real-time comms) it won't work from inside the Python interpreter. Try running it from the shell as in:

>python test.py

Also, print your data out (instead of plotting) to ensure that the values make sense.

I'd love to see pics of your project. By the way, my Python book is going to be out soon:

http://www.nostarch.com/pythonplayground

bartons2 (author)electronut2014-11-13

Right again. Next problem seems to be that the data from the arduino is not formatted the way your python code wants or like you specified above. Instead of

512 300
513 280

I am getting

512

300
513

280

My sketch is identical to yours.

I will send pics once it's pretty!

electronut (author)bartons22014-11-13

Are you using the updated Python code below?

https://gist.github.com/electronut/d5e5f68c610821e...

Also, please post your code that's printing the values. Instead of cut & paste, please upload it as a github gist and post the link here (useful to have a gthub account anyway) so we can see it properly. Or you can just use pastebin - no setup required:

http://pastebin.com/

bartons2 (author)electronut2014-11-14

I am using your new Python code. Here is my sketch:

http://pastebin.com/5HiBa1hQ

About This Instructable

101,632views

44favorites

License:

Bio: Open Source Hardware Projects - Microcontrollers, Programming, and new prototyping technologies.
More by electronut:Touch Activated Blinky BadgeTemperature/Altitude/Pressure Display using Arduino & BMP180A simple hack to adapt an 8 pin male dual row header to a breadboard.
Add instructable to: