I am trying to make a simple graph from a data gathered from a continuous real-time data source.
My code for using matplotlib is below:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time
from serialdata import SerialData
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
def animate(i):
xar = []
yar = []
#Open Serial Port and Receive Continuous Data
#in format of number,number
a = SerialData()
b = a.setSerial('COM3', 9600)
dataArray = a.getSerial(9999)
for eachLine in dataArray:
if len(eachLine)>1:
x,y = eachLine.split(',')
xar.append(int(x))
yar.append(int(y))
ax1.clear()
ax1.plot(xar,yar)
ani = animation.FuncAnimation(fig, animate, interval=1000)
plt.show()
The serialdata.py just yields the data every time it gets from the data source:
import serial
from time import sleep
class SerialData:
def __init__(self):
pass
def setSerial(self, port, baudrate):
self.port = port
self.baudrate = baudrate
print("Opening Serial Port...")
self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
sleep(2)
print("Setup Successful")
def getSerial(self, read):
while True:
self.data = self.ser.read(read)
if len(self.data)>0:
yield self.data.decode('utf-8')
sleep(.1)
self.ser.close()
supposedly it should send data in form of:
1,2
2,5
3,7
4(autoincrement),5(random number)
and works just fine when I just make them print on the CLI.
However I cannot make it work with the matplotlib.
There is no specific error.
It just shows
Opening Serial Port...
Setup Successful
and... thats it. nothing happens then.
What is wrong with my code?
I did more research and found that I shouldn't be using show()
so I rewrote my code as following:
import time
import numpy as np
import matplotlib.pyplot as plt
from serialdata import SerialData
plt.axis([0, 1000, 0, 1])
plt.ion()
plt.show()
for i in range(1000):
# y = np.random.random()
a = SerialData()
b = a.setSerial('COM3', 9600)
dataArray = a.getSerial(9999)
print("Data Gathering...")
for eachLine in dataArray:
if len(eachLine)>1:
y = eachLine.split(',')[1]
plt.scatter(i, y)
plt.draw()
time.sleep(0.05)
But, the result is the same.
I don't have a serial port, but this is what I tried to do to figure it out:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time
import random
from time import sleep
class MockData():
def __init__(self, n):
self.n = n #number of data points to return
def getData(self):
start = 0 #mock autoincrement
for i in range(self.n):
yield (start, random.randint(0, 100)) #yield a tuple, not a str (x(autoincrem),y(random))
start+=1
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
def animate(i):
xar = []
yar = []
#Open Serial Port and Receive Continuous Data
#in format of number,number
a = MockData(10)
dataArray = a.getData() #this is a generator that will yield (x,y) tuples 10 times.
for eachLine in dataArray:
x,y = eachLine
xar.append(int(x))
yar.append(int(y))
ax1.clear()
ax1.plot(xar,yar)
ani = animation.FuncAnimation(fig, animate, interval=1000)
plt.show()
This code will fail\be very slow:
If I request a large amount of data at a time: a=MockData(1000) will execute for ~2-3seconds per frame
If readout times are long i.e.
def getData(self):
start = 0
for i in range(self.n):
yield (start, random.randint(0, 100))
start+=1
time.sleep(1)
this will execute ~10 seconds per frame
Both
So as far as I can conclude issue is in your SerialData class, specifically in the getSerial method. I know that because that's the method that's responsible for retrieving the actual data. Since I don't have a serial port I can't exactly test where, but I can wager a few guesses.
def getSerial(self, read):
while True:
self.data = self.ser.read(read)
if len(self.data)>0:
yield self.data.decode('utf-8')
sleep(.1)
self.ser.close()
self.data = self.ser.read(read) Issue here is that you request 9999 bytes to be read. 9600 bauds is about 1200 bytes/s. To read 9 999 bytes it would take ~10seconds. And that's if there's actually 9999 new bytes to be read. If there weren't the read function would continue waiting until it read in that amount. This is equal to my 1) test case, except the waiting period would be sleep(10). The if check therefore is already in the read function so you don't have to check again.
self.data.decode('utf-8') Check out how long it takes to decode 9999 bytes:
>>> from timeit import Timer
>>> ts = Timer("s.decode('utf-8')", "s = b'1'*9999")
>>> ts.timeit()
2.524627058740407
Now, granted, this isn't the same conversion as yours, but since I don't have a serial port on my laptop I can't test it. Anyhow, it seems like it's very slow. Right now, you have my case 2) with sleep(12)
sleep(.1) this now just seems like adding an insult to injury.
Your code doesn't report an error because it works, it just takes over 3 minutes to read in the first set of data and plot it.
My recommendation is that you ignore this approach, read in data, almost literally, byte per byte, plot them as they come. Have an array in which you just append new bytes as they come and plot that. You could easily pull something like:
serialPort = serial.Serial(self.port, self.baudrate, timeout=1)
data = list() #some global list to append to
def animate():
d = serialPort.read(2) #this will hang until it completes
data.append(d) #does the data really have to be in string?
ax.plot(data)
ani = animation.FuncAnimation(fig, animate, interval=1000) #optionally send in repeat_delay?
plt.show()
though part about this is that I'm not sure how animation behaves if the data array starts getting really big, so you might consider shifting your x axis to the right, deleting your old data and creating a new array periodically.
Hopefully I was of some help in pointing out which things you need to test for. Start timing all your read-ins from serial port to get a feeling for how long do things take. Don't forget to make sure you're actually reading anything at all from the serial port also, if you're not, the serial.read will just wait there.
I've also never done anything like this, so it's possible I'm way off course. Last time I dealt with serial ports was robotics championship from elementary (rofl).
Related
I am working with the pyaudio and matplotlib packages for the first time and I am attempting to plot live audio data from microphone input, transform it to frequency domain information, and then output peaks with an input distance. This project is a modification of the three-part guide to build a spectrum analyzer found here.
Currently the code is formatted in a class as I have alternative methods that I am applying to the audio but I am only posting the class with the relevant methods as they don't make reference to each and are self-contained. Another quirk of the program is that it calls upon a local file though it only uses input from the user microphone; this is a leftover from the original functionality of plotting a sound file's intensity while it played and is no longer integral to the code.
import pyaudio
import wave
import struct
import pandas as pd
from scipy.fftpack import fft
from scipy.signal import find_peaks
import matplotlib.pyplot as plt
import numpy as np
class Wave:
def __init__(self, file) -> None:
self.CHUNK = 1024 * 4
self.obj = wave.open(file, "r")
self.callback_output = []
self.data = self.obj.readframes(self.CHUNK)
self.rate = 44100
# Initiate an instance of PyAudio
self.p = pyaudio.PyAudio()
# Open a stream with the file specifications
self.stream = self.p.open(format = pyaudio.paInt16,
channels = self.obj.getnchannels(),
rate = self.rate,
output = True,
input = True,
frames_per_buffer = self.CHUNK)
def fft_plot(self, distance: float):
x_fft = np.linspace(0, self.rate, self.CHUNK)
fig, ax = plt.subplots()
line_fft, = ax.semilogx(x_fft, np.random.rand(self.CHUNK), "-", lw = 2)
# Bind plot window sizes
ax.set_xlim(20, self.rate / 2)
plot_data = self.stream.read(self.CHUNK)
self.data_int = pd.DataFrame(struct.unpack(\
str(self.CHUNK * 2) + 'h', plot_data)).astype(dtype = "b")[::2]
y_fft = fft(self.data_int)
line_fft.set_ydata(np.abs(y_fft[0:self.CHUNK]) / (256 * self.CHUNK))
plt.show(block = False)
while True:
# Read incoming audio data
data = self.stream.read(self.CHUNK)
# Convert data to bits then to array
self.data_int = struct.unpack(str(4 * self.CHUNK) + 'B', data)
# Recompute FFT and update line
yf = fft(self.data_int)
line_data = np.abs(yf[0:self.CHUNK]) / (128 * self.CHUNK)
line_fft.set_ydata(line_data)
# Find all values above threshold
peaks, _ = find_peaks(line_data, distance = distance)
# Update the plot
plt.plot(peaks, line_data[peaks], "x")
fig.canvas.draw()
fig.canvas.flush_events()
# Exit program when plot window is closed
fig.canvas.mpl_connect('close_event', exit)
test_file = "C:/Users/Tam/Documents/VScode/Final Project/PrismGuitars.wav"
audio_test = Wave(test_file)
audio_test.fft_plot(2000)
The code does not throw any errors and runs fine with an okay framerate and only terminates when the plot window is closed, all of which is good. The issue I'm encountering is with the determination and plotting of the peaks of line_data as when I run this code the output over time looks like this matplotlib graph instance.
It seems that the peaks (or peak) are being found but at a lower frequency than the x of line_data and as such are shifted comparatively. The other, more minor, issue is that since this is a live plot I would like to clear the previous instance of the peak marker so that it only shows the current instance and not all of the ones plotted prior.
I have attempted in prior fixes to use the line_fft in the peak detection but as it is cast to a Line2D format the peak detection algorithm isn't able to deal with the data type. I have also tried implementing a list comprehension as seen in this post but the time to cast to list is prohibitively slow and did not return any peak markers when I ran it.
EDIT: Following Jody's input the program now returns the proper values as I was only printing an index for the x-coordinate of the peak marker. Nevertheless I would still appreciate some insight as to whether it is possible to update per marker rather than having all the previous ones constantly displayed.
As for the marker updating I have attempted to clear the plot in the while loop both before and after drawing the markers (in different tests of course) but I only ever end up with a completely blank graph.
Please let me know if there is anything I should clarify and thank you for your time.
As Jody pointed out the peaks variable contains indexes for the detected peaks that then need to be retrieved from x_fft and line_data in order to match up with the displayed data.
First we create a scatter plot:
scat = ax.scatter([], [], c = "purple", marker = "x")
This data can then be stacked using a container variable in the while loop as such:
array_peaks = np.c_[x_fft[peaks], line_data[peaks]]
and update the data in the while loop with:
scat.set_offsets(array_peaks)
I am quite fresh using Python. I need it to acquire serial datas from a force sensor plugged into a COMPORT (6). With the code below, I have no problem to store the datas in a list and save it afterwards. I can also print each data without noticing any lag.
However, when I try to implement a plot in my while loop, a rather annoying shift appears between the time I touch my sensor and the datas are written (from few to tens of seconds). I first thought it was because matplotlib is a memory consuming library, but even when i add the simple line "time.sleep(0.00001), which is a very short pause compared to the rate of acquisition (60 FPS), I get the same lags. I also tried to save my datas in a csv file and plot the datas in a different function by using multiprocess but even saving datas triggers the lag also.
This is problematic as visualizing my live datas is an important part of my experiment.
Could someone please help me with this particular issue ?
Thank you so much.
import serial
import struct
import platform
import multiprocessing
import time
import numpy as np
import csv
# from pylab import *
import matplotlib.pyplot as plt
class MeasurementConverter:
def convertValue(self, bytes):
pass
class ForceMeasurementConverterKG(MeasurementConverter):
def __init__(self, F_n, S_n, u_e):
self.F_n = F_n
self.S_n = S_n
self.u_e = u_e
def convertValue(self, value):
A = struct.unpack('>H', value)[0]
# return (A - 0x8000) * (self.F_n / self.S_n) * (self.u_e / 0x8000)
return self.F_n / self.S_n * ((A - 0x8000) / 0x8000) * self.u_e * 2
class GSV3USB:
def __init__(self, com_port, baudrate=38400):
com_path = f'/dev/ttyUSB{com_port}' if platform.system(
) == 'Linux' else f'COM{com_port}'
# print(f'Using COM: {com_path}')
self.sensor = serial.Serial(com_path, baudrate)
self.converter = ForceMeasurementConverterKG(10, 0.499552, 2)
def read_value(self):
self.sensor.read_until(b'\xA5')
read_val = self.sensor.read(2)
return self.converter.convertValue(read_val)
# initialization of datas
gsv_data=[]
temps=[]
t_0=time.time()
def data_gsv():
dev = GSV3USB(6)
# fig=plt.figure()
# ax = fig.add_subplot(111)
i=0
# line1, = ax.plot(temps, gsv_data)
try:
while True:
gsv_data.append(dev.read_value())
t1=time.time()-t_0
temps.append(t1)
# I can print the datas without noticing any lags
print(dev.read_value())
# I cannot plot the datas without noticing any lags
plt.plot(temps,gsv_data)
plt.draw ()
plt.axis([temps[i]-6,temps[i]+6,-2,10])
plt.pause(0.00001)
plt.clf()
i=i+1
# I cannot pause without noticing any lags
time.sleep(0.0001)
# I cannot save datas without noticing any lags
with open('student_gsv.csv', 'w') as f:
write = csv.writer(f)
write.writerow(gsv_data)
except KeyboardInterrupt:
print("Exiting")
return
if __name__ == "__main__":
data_gsv()```
Your main loop is constructed in a way that each time when you acquire a value with gsv_data.append(dev.read_value()), the plot is drawn again. That probably takes time, and if the data frequency with that the measuring device (GSV-3 USB) send measured data is quite high - let's say >10 frames/s - you'll get these "lags".
I would lower the plot update rate:
append the measured data to an array (as you already do)
Use a counter or a comparison of time difference to get a plot update rate of about 4 times per second (250ms). This is an update rate often used for being consumed by the human eye.
Inspired by the answer to this question, I have tried the following code:
import nidaqmx
from nidaqmx import stream_readers
from nidaqmx import constants
import time
sfreq = 1000
bufsize = 100
data = np.zeros((1, 1), dtype = np.float32) # initializes total data file
with nidaqmx.Task() as task:
task.ai_channels.add_ai_voltage_chan("cDAQ2Mod1/ai1")
task.timing.cfg_samp_clk_timing(rate = sfreq, sample_mode = constants.AcquisitionType.CONTINUOUS,
samps_per_chan = bufsize) # unclear samps_per_chan is needed or why it would be different than bufsize
stream = stream_readers.AnalogMultiChannelReader(task.in_stream)
def reading_task_callback(task_id, event_type, num_samples, callback_data=None): # num_samples is set to bufsize
buffer = np.zeros((1, num_samples), dtype = np.float32) # probably better to define it here inside the callback
stream.read_many_sample(buffer, num_samples, timeout = constants.WAIT_INFINITELY)
data = np.append(data, buffer, axis = 1) # hopping to retrieve this data after the read is stopped
task.register_every_n_samples_acquired_into_buffer_event(bufsize, reading_task_callback)
Expected behavior: it reads continuously from a channel. I am not even trying to get it to do something specific yet (such as plotting in real time), but I would expect the python console to run until one stops it, since the goal is to read continuously.
Observed behavior: running this code proceeds quickly and the console prompt is returned.
Problem: it seems to me this is not reading continuously at all. Furthermore, the data variable does not get appended like I would like it to (I know that retrieving a certain number of data samples does not require such convoluted code with nidaqmx; this is just one way I thought I could try and see if this is doing what I wanted, i.e. read continuously and continuously append the buffered sample values to data, so that I can then look at the total data acquired).
Any help would be appreciated. I am essentially certain the way to achieve this is by making use of these callbacks which are part of nidaqmx, but somehow I do not seem to manage them well. Note I have been able to read a predefined and finite amount of data samples from analog input channels by making use of read_many_sample.
Details: NI cDAQ 9178 with NI 9205 module inserted, on Lenovo laptop running Windows Home 10, python 3.7 and nidaqmx package for python.
EDIT: for anyone interested, I now have this working in the following way, with a live visual feedback using matplotlib, and - not 100% percent sure yet - it seems there no buffer problems even if one aims at long acquisitions (>10 minutes). Here is the code (not cleaned, sorry):
"""
Analog data acquisition for QuSpin's OPMs via National Instruments' cDAQ unit
The following assumes:
"""
# Imports
import matplotlib.pyplot as plt
import numpy as np
import nidaqmx
from nidaqmx.stream_readers import AnalogMultiChannelReader
from nidaqmx import constants
# from nidaqmx import stream_readers # not needed in this script
# from nidaqmx import stream_writers # not needed in this script
import threading
import pickle
from datetime import datetime
import scipy.io
# Parameters
sampling_freq_in = 1000 # in Hz
buffer_in_size = 100
bufsize_callback = buffer_in_size
buffer_in_size_cfg = round(buffer_in_size * 1) # clock configuration
chans_in = 3 # set to number of active OPMs (x2 if By and Bz are used, but that is not recommended)
refresh_rate_plot = 10 # in Hz
crop = 10 # number of seconds to drop at acquisition start before saving
my_filename = 'test_3_opms' # with full path if target folder different from current folder (do not leave trailing /)
# Initialize data placeholders
buffer_in = np.zeros((chans_in, buffer_in_size))
data = np.zeros((chans_in, 1)) # will contain a first column with zeros but that's fine
# Definitions of basic functions
def ask_user():
global running
input("Press ENTER/RETURN to stop acquisition and coil drivers.")
running = False
def cfg_read_task(acquisition): # uses above parameters
acquisition.ai_channels.add_ai_voltage_chan("cDAQ2Mod1/ai1:3") # has to match with chans_in
acquisition.timing.cfg_samp_clk_timing(rate=sampling_freq_in, sample_mode=constants.AcquisitionType.CONTINUOUS,
samps_per_chan=buffer_in_size_cfg)
def reading_task_callback(task_idx, event_type, num_samples, callback_data): # bufsize_callback is passed to num_samples
global data
global buffer_in
if running:
# It may be wiser to read slightly more than num_samples here, to make sure one does not miss any sample,
# see: https://documentation.help/NI-DAQmx-Key-Concepts/contCAcqGen.html
buffer_in = np.zeros((chans_in, num_samples)) # double definition ???
stream_in.read_many_sample(buffer_in, num_samples, timeout=constants.WAIT_INFINITELY)
data = np.append(data, buffer_in, axis=1) # appends buffered data to total variable data
return 0 # Absolutely needed for this callback to be well defined (see nidaqmx doc).
# Configure and setup the tasks
task_in = nidaqmx.Task()
cfg_read_task(task_in)
stream_in = AnalogMultiChannelReader(task_in.in_stream)
task_in.register_every_n_samples_acquired_into_buffer_event(bufsize_callback, reading_task_callback)
# Start threading to prompt user to stop
thread_user = threading.Thread(target=ask_user)
thread_user.start()
# Main loop
running = True
time_start = datetime.now()
task_in.start()
# Plot a visual feedback for the user's mental health
f, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex='all', sharey='none')
while running: # make this adapt to number of channels automatically
ax1.clear()
ax2.clear()
ax3.clear()
ax1.plot(data[0, -sampling_freq_in * 5:].T) # 5 seconds rolling window
ax2.plot(data[1, -sampling_freq_in * 5:].T)
ax3.plot(data[2, -sampling_freq_in * 5:].T)
# Label and axis formatting
ax3.set_xlabel('time [s]')
ax1.set_ylabel('voltage [V]')
ax2.set_ylabel('voltage [V]')
ax3.set_ylabel('voltage [V]')
xticks = np.arange(0, data[0, -sampling_freq_in * 5:].size, sampling_freq_in)
xticklabels = np.arange(0, xticks.size, 1)
ax3.set_xticks(xticks)
ax3.set_xticklabels(xticklabels)
plt.pause(1/refresh_rate_plot) # required for dynamic plot to work (if too low, nulling performance bad)
# Close task to clear connection once done
task_in.close()
duration = datetime.now() - time_start
# Final save data and metadata ... first in python reloadable format:
filename = my_filename
with open(filename, 'wb') as f:
pickle.dump(data, f)
'''
Load this variable back with:
with open(name, 'rb') as f:
data_reloaded = pickle.load(f)
'''
# Human-readable text file:
extension = '.txt'
np.set_printoptions(threshold=np.inf, linewidth=np.inf) # turn off summarization, line-wrapping
with open(filename + extension, 'w') as f:
f.write(np.array2string(data.T, separator=', ')) # improve precision here!
# Now in matlab:
extension = '.mat'
scipy.io.savemat(filename + extension, {'data':data})
# Some messages at the end
num_samples_acquired = data[0,:].size
print("\n")
print("OPM acquisition ended.\n")
print("Acquisition duration: {}.".format(duration))
print("Acquired samples: {}.".format(num_samples_acquired - 1))
# Final plot of whole time course the acquisition
plt.close('all')
f_tot, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex='all', sharey='none')
ax1.plot(data[0, 10:].T) # note the exclusion of the first 10 iterations (automatically zoomed in plot)
ax2.plot(data[1, 10:].T)
ax3.plot(data[2, 10:].T)
# Label formatting ...
ax3.set_xlabel('time [s]')
ax1.set_ylabel('voltage [V]')
ax2.set_ylabel('voltage [V]')
ax3.set_ylabel('voltage [V]')
xticks = np.arange(0, data[0, :].size, sampling_freq_in)
xticklabels = np.arange(0, xticks.size, 1)
ax3.set_xticks(xticks)
ax3.set_xticklabels(xticklabels)
plt.show()
Of course comments are appreciated. This is probably still suboptimal.
From the help of the community I was able to install al new backend for matplotlib and run code from the Arduino serial to the output of the python window.
I was then able to make a pretty graph and it displays, but crashes when I get the following error:
AttributeError: 'str' object has no attribute 'inWaiting'
This was remedied by the help of #elethan
However, I now have the error:
pulse = float(dataArray[0])
ValueError: could not convert string to float:
This error does not happen everytime
As if that wasn't enough, the output on the plotted graph shows a value of 10 for most of the plot, this is not the value output from the Arduino serial.
I am unsure why:
1) The error is intermittend (maybe its grabbing a ',' and not a value)
2) Why I get a steady value of 10 when the graph does plot
The code is as follows:
import time
import serial
import matplotlib.pyplot as plt
import numpy
from drawnow import *
import os,sys
pulseArray = []
plt.ion()
clippingCounter = 0
# configure the serial connections (the parameters differs on the device you are connecting to)
ser = serial.Serial(
port='/dev/ttyACM0',
baudrate=115200,
parity=serial.PARITY_ODD,
stopbits=serial.STOPBITS_TWO,
bytesize=serial.SEVENBITS
)
ser.isOpen()
input=1
def plotPulseData():
plt.ylim(0,120)
plt.title('Plots from data of the Pulse Sensor')
plt.grid(True)
plt.ylabel('Pulse')
#plt.subplot(2, 2, 1)
plt.plot(pulseArray, 'ro-', label='BPM', linewidth=2)
plt.legend(loc='upper left')
while True:
#First things first, lets wait for data prior to reading
time.sleep(1)
if (ser.inWaiting()>4):
s = ser.read(4)
#print ser
dataArray = s.split(',')
pulse = float(dataArray[0])
pulseArray.append(pulse)
plt.figure(1)
drawnow(plotPulseData)
plt.pause(.000001)
clippingCounter = clippingCounter + 1
if(clippingCounter>50):
pulseArray.pop(0)
Any help is greatly appreciated, thank you in advance.
You reassign ser the first time through your while loop from a Serial object to a string:
ser = ser.read(4)
The next time through the loop when you call the inWaiting() method on that object, you get an error, because you are calling it on a string, instead of the original Serial object:
ser.inWaiting() > 4
Change the variable name for the output of ser.read(4) to a name that is not already taken, and this error should go away:
s = ser.read(4)
...
dataArray = s.split(',')
So here is the deal, I have a module which sends out data over the serial port at 9600 baud and I am using the matplotlib to plot that data in real time. I wrote this code
#! python
############ import section ###################
import serial
import struct
import numpy as np
import matplotlib.pyplot as plt
###############################################
############ Serial Configuration #############
com = serial.Serial(14,9600)
print ("Successfully opened " + com.portstr)
###############################################
def get_new_values():
s = com.read(20)
raw_data = struct.unpack('!BBBBBBBBBBBBBBBBBBBB', s)
j = 0;
norm_data = []
for i in range(0,20,2):
big_byte = raw_data[i+1]+(raw_data[i]*256)
norm_data.append(big_byte)
j = j+1
return norm_data
x = range(0,10,1)
y = get_new_values()
plt.ion()
line, = plt.plot(x,y)
while(1):
a = get_new_values()
line.set_ydata(a)
plt.draw()
com.close()
print ("Port Closed Successfully")
I get 20 bytes and then make 10 big bytes (2 byte data is split as two 1 byte values while transmitting). But I just noticed that I am not getting real time values from this. I am on windows 7 home basic if that matters.
Any clue why this is happening?
EDIT
Also, whenever I click on the plot it hangs.
I know the code below seems long and overly complex for your simple problem, but manual optimization is usually more code and more potential bugs. That's why premature optimization is almost always a mistake.
In __init__ it sets up the plot, setting references for the axis, canvas, line (it starts with a line drawn off screen), and the pre-animation background. Additionally __init__ registers callbacks to handle resizing and shutdown. The on_resize callback is needed to update the background (used for the blit) when the window is resized. The on_close callback uses a lock to update the running status. I haven't eliminated all race conditions with this, but this works to prevent a _tkinter.TclError caused by trying to blit to a terminated Tk app. I only tested with Tk, and on only one machine. YMMV, and I'm open to suggestions.
In the run method I've added a call to canvas.flush_events(). This should keep the plot window from hanging if you try to drag the window around and resize it. The while loop in this method calls self.get_new_values() to set the data in the plot. It then updates the plot using the configured method. If self.blit is true, it uses canvas.blit, else it uses pyplot.draw.
The variable spf (samples per frame) controls how many samples are plotted in a frame. You can use it in your implementation of get_new_values to determine the number of bytes to read (e.g. 2 * self.spf for 2 bytes per sample). I've set the default to 120, which is 5 frames per second if your data rate is 600 samples per second. You have to find the sweet spot that maximizes throughput vs time resolution in the graph while also keeping up with the incoming data.
Reading your data into a NumPy array instead of using a Python list will likely speed up processing. Also, it would give you easy access to tools to downsample and analyze the signal. You can read into a NumPy array directly from a byte string, but make sure you get the endianess right:
>>> data = b'\x01\xff' #big endian 256 + 255 = 511
>>> np.little_endian #my machine is little endian
True
>>> y = np.fromstring(data, dtype=np.uint16); y #so this is wrong
array([65281], dtype=uint16)
>>> if np.little_endian: y = y.byteswap()
>>> y #fixed
array([511], dtype=uint16)
Code:
from __future__ import division
from matplotlib import pyplot
import threading
class Main(object):
def __init__(self, samples_per_frame=120, blit=True):
self.blit = blit
self.spf = samples_per_frame
pyplot.ion()
self.ax = pyplot.subplot(111)
self.line, = self.ax.plot(range(self.spf), [-1] * self.spf)
self.ax.axis([0, self.spf, 0, 65536])
pyplot.draw()
self.canvas = self.ax.figure.canvas
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.canvas.mpl_connect('resize_event', self.on_resize)
self.canvas.mpl_connect('close_event', self.on_close)
self.lock = threading.Lock()
self.run()
def get_new_values(self):
import time
import random
#simulate receiving data at 9600 bps (no cntrl/crc)
time.sleep(2 * self.spf * 8 / 9600)
y = [random.randrange(65536) for i in range(self.spf)]
self.line.set_ydata(y)
def on_resize(self, event):
self.line.set_ydata([-1] * self.spf)
pyplot.draw()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
def on_close(self, event):
with self.lock:
self.running = False
def run(self):
with self.lock:
self.running = True
while self.running:
self.canvas.flush_events()
with self.lock:
self.get_new_values()
if self.running:
if self.blit:
#blit for a higher frame rate
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line)
self.canvas.blit(self.ax.bbox)
else:
#versus a regular draw
pyplot.draw()
if __name__ == '__main__':
Main(samples_per_frame=120, blit=True)