Data is now pulled from sensor queue in batches, no limit to size. Temperature queue check frequency reduced. Image plotting now happens in a background thread.

This commit is contained in:
Kienan Stewart 2015-04-01 23:06:03 -04:00
parent babac7ae2e
commit c6e7aefb96
2 changed files with 195 additions and 145 deletions

View File

@ -89,9 +89,8 @@
serial_chooser_button: serial_chooser_button serial_chooser_button: serial_chooser_button
on_dataSources: serial_chooser_button.values = self.dataSources on_dataSources: serial_chooser_button.values = self.dataSources
on_lastTemperature: current_temperature.on_temperature_change(self.lastTemperature) on_lastTemperature: current_temperature.on_temperature_change(self.lastTemperature)
on_lastTemperature: mainplot.on_lastTemperature(self.lastTemperature)
on_lastTime: mainplot.on_lastTime(self.lastTime)
on_lastStatus: status_bar.update_status(self.lastStatus) on_lastStatus: status_bar.update_status(self.lastStatus)
on_lastData: mainplot.update_data(self.lastData)
raw_data_output_file: '' raw_data_output_file: ''
image_output_file: '' image_output_file: ''
on_raw_data_output_file: mainplot.raw_data_output_file = self.raw_data_output_file on_raw_data_output_file: mainplot.raw_data_output_file = self.raw_data_output_file
@ -173,4 +172,5 @@
StatusBar: StatusBar:
size_hint_y: 0.1 size_hint_y: 0.1
id: status_bar id: status_bar
MainWindow: MainWindow:

336
main.py
View File

@ -1,7 +1,10 @@
import io
import kivy import kivy
from kivy.app import App from kivy.app import App
from kivy.clock import Clock from kivy.clock import Clock
import kivy.core.image import kivy.core.image
import kivy.config
import kivy.core.image
from kivy.core.image import ImageData from kivy.core.image import ImageData
from kivy.core.image.img_pil import ImageLoaderPIL from kivy.core.image.img_pil import ImageLoaderPIL
from kivy.core.image.img_pygame import ImageLoaderPygame from kivy.core.image.img_pygame import ImageLoaderPygame
@ -22,6 +25,7 @@ from kivy.uix.spinner import Spinner
import kivy.lang import kivy.lang
import matplotlib import matplotlib
matplotlib.use('Agg') matplotlib.use('Agg')
import matplotlib.backends.backend_agg
import matplotlib.image import matplotlib.image
import matplotlib.lines import matplotlib.lines
import matplotlib.patches import matplotlib.patches
@ -41,11 +45,13 @@ dataThreadDataQueue = Queue.Queue()
dataThreadCommandQueue = Queue.Queue() dataThreadCommandQueue = Queue.Queue()
defaultSerialPort = '/dev/ttyACM0' defaultSerialPort = '/dev/ttyACM0'
kivy.config.Config.set('modules', 'monitor', '')
class MainApp(App): class MainApp(App):
def build(self): def build(self):
mainWindow = MainWindow() mainWindow = MainWindow()
Clock.schedule_interval(mainWindow.update, 0.25) Clock.schedule_interval(mainWindow.update, 0.5)
return mainWindow return mainWindow
@ -71,6 +77,8 @@ class MainWindow(FloatLayout):
image_output_file = StringProperty('') image_output_file = StringProperty('')
raw_data_output_file = StringProperty('') raw_data_output_file = StringProperty('')
reference_profile_file = StringProperty('') reference_profile_file = StringProperty('')
lastData = ListProperty([])
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs) super(MainWindow, self).__init__(**kwargs)
@ -83,38 +91,39 @@ class MainWindow(FloatLayout):
def update_last_temperature(self): def update_last_temperature(self):
global dataThreadDataQueue global dataThreadDataQueue
# This breaks if there are more updates in the queue than the frequency of update_last_temperature - it pulls old data out of the queue, and not the most recent. # This breaks if there are more updates in the queue than the frequency of update_last_temperature - it pulls old data out of the queue, and not the most recent.
try: done = False
data = dataThreadDataQueue.get_nowait() data = []
if data is not None: while not done:
Logger.debug('Data: ' + str(data)) try:
if self.state != 'paused': data.append(dataThreadDataQueue.get_nowait())
if 'data' in data and 'temperature' in data['data']: except Queue.Empty:
self.lastStatus = {'status' : 'ok', 'message' : ''} done = True
self.lastTemperature = float(data['data']['temperature']) if data:
if 'time' in data: if len(data) > 1:
self.lastTime = float(data['time']) Logger.debug('update_last_temperature: ' + str(len(data)) + ' items retrieved from queue')
if 'exception' in data: last_point = data[-1]
self.lastStatus = {'status' : 'error', 'message' : data['exception']} if self.state == 'paused':
else: # Try to update the last temperature, but not to the plotwidget,
self.lastStatus = {'status' : 'paused', 'message' : 'Data reception halted by user'} # when the data reception is halted.
Logger.debug('MainWindow: state set to ' + str(self.state) + ' : ignoring data') self.lastStatus = {'status' : 'paused', 'message' : 'Data receiption halted by user'}
if dataThreadDataQueue.qsize(): if 'data' in last_point and 'temperature' in last_point['data']:
Logger.debug('Queue Size: ' + str(dataThreadDataQueue.qsize())) self.lastTemperature = float(last_point['data']['temperature'])
except Queue.Empty: return
pass if 'data' in last_point and 'temperature' in last_point['data']:
self.lastStatus = {'status' : 'ok', 'message' : ''}
self.lastTemperature = float(last_point['data']['temperature'])
def on_lastTemperature(self, instance, value): if 'time' in last_point:
Logger.debug('lastTemperature has changed to: ' + str(value)) self.lastTime = float(last_point['time'])
if 'exception' in last_point:
self.lastStatus = {'status' : 'error', 'message' : data['exception']}
def on_lastTime(self, instance, value): self.lastData = data
Logger.debug('lastTime has changed to: ' + str(value)) if dataThreadDataQueue.qsize():
Logger.debug('update_last_temperature: ' + str(dataThreadDataQueue.qsize()) + ' items left in queue after ' + str(len(data)) + ' items were taken out')
def update(self, dt): def update(self, dt):
self.update_data_sources() self.update_data_sources()
self.update_last_temperature() self.update_last_temperature()
Logger.debug('MainWindow: FPS ' + str(kivy.clock.Clock.get_fps()))
#Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive())) #Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive()))
@ -378,7 +387,8 @@ class PlotWidget(Image):
show_alphaamylase = BooleanProperty(True) show_alphaamylase = BooleanProperty(True)
file_write_interval = NumericProperty(15) file_write_interval = NumericProperty(15)
image_update_interval = NumericProperty(5) image_update_interval = NumericProperty(5)
buildingImage = BooleanProperty(False)
imageDataQueue = ObjectProperty(None, allownone = True)
# Common data that we should only instantiate once # Common data that we should only instantiate once
# Source : http://www.howtobrew.com/section3/chapter14-1.html # Source : http://www.howtobrew.com/section3/chapter14-1.html
@ -392,6 +402,7 @@ class PlotWidget(Image):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(PlotWidget, self).__init__(**kwargs) super(PlotWidget, self).__init__(**kwargs)
self.imageThread = None
self.data = numpy.array([], dtype=float); self.data = numpy.array([], dtype=float);
self.lastTime = 0 self.lastTime = 0
self.lastTemperature = 0 self.lastTemperature = 0
@ -406,7 +417,7 @@ class PlotWidget(Image):
self._image_raw_data = None self._image_raw_data = None
self.reference_profile = None self.reference_profile = None
Clock.schedule_interval(self.do_update, self.image_update_interval) Clock.schedule_interval(self.do_update, self.image_update_interval)
self.do_update() Clock.schedule_once(self.do_update)
def on_image_update_interval(self, *args): def on_image_update_interval(self, *args):
@ -415,34 +426,6 @@ class PlotWidget(Image):
Clock.schedule_interval(self.do_update, self.image_update_interval) Clock.schedule_interval(self.do_update, self.image_update_interval)
def update_patches(self, tmax = None):
items = [
(self.show_betaglucan, self.betaglucan_vertices),
(self.show_protease, self.protease_vertices),
(self.show_betaamylase, self.betaamylase_vertices),
(self.show_alphaamylase, self.alphaamylase_vertices)
]
legend_handles = []
legend_strings = []
for show, config in items:
color = '0.5' # default color.
if show:
origin, width, height, color, name = config
if tmax is not None:
width = tmax
p = self.plot_axes.add_patch(
matplotlib.patches.Rectangle(origin, width, height,
fc = color, visible = True,
fill = True)
)
legend_handles.append(p)
legend_strings.append(name)
# Disabled for the moment
self.figure.legend(legend_handles, legend_strings, 'lower right',
title = 'Enzyme Activity Zones')
return legend_handles
def clear_data(self): def clear_data(self):
self.update_output_files(True) self.update_output_files(True)
self.data = numpy.array([], dtype=float); self.data = numpy.array([], dtype=float);
@ -455,79 +438,49 @@ class PlotWidget(Image):
def do_update(self, *args): def do_update(self, *args):
image_data = self.to_image_data() if self.buildingImage:
# We can't use ImageLoaders since they assume it's a file on disk. # Check end condition, retrieve data, clean-up
# This replicates code from ImageLoaderPygame.load() and ImageLoaderBase.populate() if self.imageThread.isAlive():
try: return
im = pygame.image.load(image_data) self.imageThread.join(0.01)
except: image_data = None
Logger.warning('Image: Unable to load image from data') try:
raise image_data = self.imageDataQueue.get_nowait()
fmt = '' except Queue.Empty:
if im.get_bytesize() == 3: pass
fmt = 'rgb' if image_data:
elif im.get_bytesize() == 4: image = image_data['kivy_image']
fmt = 'rgba' self.texture = image.texture
data = pygame.image.tostring(im, fmt.upper()) self._image_raw_data = image_data['raw_image_data']
self.image_data = ImageData(im.get_width(), im.get_height(), fmt, data) self.imageThread = None
self.texture = Texture.create_from_data(self.image_data) self.imageDataQueue = None
self.texture.flip_vertical() # @TODO self.update_output_files() should may be a scheduled task
# Update output files, if any self.update_output_files()
# @TODO Add in an update interval so the disk isn't hammered on every single update self.buildingImage = False
self.update_output_files() return
self.buildingImage = True
self.imageDataQueue = Queue.Queue()
def to_image_data(self): settings = {
# Add in polygons for hilightning particular temperature ranges of interest. 'patches' : {
plot_args = [] 'betaglucan' : { 'show' : self.show_betaglucan,
t = None 'vertices' : self.betaglucan_vertices},
time_max = 60 'protease' : { 'show' : self.show_protease,
temp_max = 80 'vertices' : self.protease_vertices},
handles = [] 'betaamylase' : { 'show' : self.show_betaamylase,
if self.data.any(): 'vertices' : self.betaamylase_vertices},
#data = numpy.copy(self.data) 'alphaamylase' : { 'show' : self.show_alphaamylase,
data = self.data.transpose() 'vertices' : self.alphaamylase_vertices},
t, temperature = numpy.split(data, 2, axis = 0) }
if t[0][-1] > time_max: }
time_max = t[0][-1] self.imageThread = threading.Thread(None, threaded_image_builder, None,
#if numpy.nanmax(temperature[0]) > temp_max: [self.data, self.reference_profile,
# temp_max = numpy.nanmax(temperature[0]) self.imageDataQueue, settings])
plot_args.extend((t[0], temperature[0], 'b')) self.imageThread.start()
handles.append('Recorded Temperature Profile')
if self.reference_profile is not None and self.reference_profile.any():
#reference_data = numpy.copy(self.reference_profile)
reference_data = self.reference_profile.transpose()
t2, reference_temperature = numpy.split(reference_data, 2, axis = 0)
# Add t[0] to all reference points to make it fit on the current graph
t2 = numpy.add(t2, t[0][0])
if t2[0][-1] > time_max:
time_max = t2[0][-1]
#if numpy.nanmax(reference_temperature[0]) > temp_max:
# temp_max = numpy.nanmax(reference_temperature[0])
plot_args.extend((t2[0], reference_temperature[0], 'r'))
handles.append('Reference Temperature Profile')
lines = self.plot_axes.plot(*plot_args)
# Patches must be changed after the axes are plotted.
self.update_patches()
# Disabled the legend for the moment.
# @TODO Make a small subplot next to the main plot into which legends may be placed.
#self.plot_axes.legend(lines, handles, 'lower right', title = 'Temperature Profiles')
# Set a default in case no data is plotted. This may allow patches to show,
# and it will look a little nicer before recording starts.
#self.plot_axes.set_xlim(0, time_max)
self.plot_axes.set_ylim(0, temp_max)
image_data = cStringIO.StringIO()
self.figure.savefig(image_data, format = 'png')
image_data.seek(0)
self._image_raw_data = image_data.getvalue()
return image_data
def update_output_files(self, force = False): def update_output_files(self, force = False):
# Do this only once every 15 seconds to avoid hitting the disk frequently # Do this only once every 15 seconds to avoid hitting the disk frequently
Logger.debug('update_output_files called')
if force or time.time() - self.lastSave > self.file_write_interval: if force or time.time() - self.lastSave > self.file_write_interval:
Logger.debug('update_output_files going ahead')
self.update_raw_data_output_file() self.update_raw_data_output_file()
self.update_image_output_file() self.update_image_output_file()
self.lastSave = time.time() self.lastSave = time.time()
@ -551,31 +504,24 @@ class PlotWidget(Image):
Logger.debug('update_image_output_file: Unable to open file ' + self.image_output_file) Logger.debug('update_image_output_file: Unable to open file ' + self.image_output_file)
def on_lastTemperature(self, value): def update_data(self, data = list(), *args):
self.lastTemperature = value points = []
for point in data:
if 'data' in point and 'time' in point and 'temperature' in point['data']:
def on_lastTime(self, value): points.append(numpy.array([(point['time'], point['data']['temperature'])],
self.lastTime = value dtype=float, ndmin =2))
self.update_data() extra_data = numpy.vstack(tuple(points))
def update_data(self):
if self.lastTime == 0 or self.lastTemperature == 0:
return
newpoint = numpy.array([(self.lastTime, self.lastTemperature)], dtype=float, ndmin = 2)
if not self.data.any(): if not self.data.any():
self.data = newpoint self.data = extra_data
return return
#Logger.debug('self.data: ' + str(self.data)) #Logger.debug('self.data: ' + str(self.data))
#Logger.debug('newpoint: ' + str(newpoint)) #Logger.debug('newpoint: ' + str(newpoint))
self.data = numpy.vstack((self.data, newpoint)) self.data = numpy.vstack((self.data, extra_data))
def on_reference_profile_file(self, *args, **kwargs): def on_reference_profile_file(self, *args, **kwargs):
Logger.debug('PlotWidget: on_reference_profile_file ' + str(self.reference_profile_file)) Logger.debug('PlotWidget: on_reference_profile_file ' + str(self.reference_profile_file))
self.load_reference_profile() self.load_reference_profile()
Clock.schedule_once(self.do_update)
def load_reference_profile(self): def load_reference_profile(self):
@ -621,6 +567,110 @@ class StatusBar(BoxLayout):
self.ids.status_image.source = './images/warning.png' self.ids.status_image.source = './images/warning.png'
def threaded_image_builder(data, reference_profile, dataQueue, settings = dict()):
figure, plot_axes = init_plot(settings)
start = time.time()
image_data = create_graph(figure, plot_axes, data, reference_profile, settings)
Logger.debug('threaded_image_builder: Took ' + str(time.time() - start) + ' seconds to build image data')
return_data = dict()
start = time.time()
image_data_raw = image_data.getvalue()
image_bytes = io.BytesIO(image_data_raw)
return_data['raw_image_data'] = image_data_raw
return_data['kivy_image'] = kivy.core.image.Image(image_bytes, ext = 'png')
Logger.debug('threaded_image_builder: Took ' + str(time.time() - start) + ' seconds to manipulate image data')
dataQueue.put_nowait(return_data)
def init_plot(settings = dict()):
figure = matplotlib.pyplot.figure()
plot_axes = figure.add_subplot(1, 1, 1)
plot_axes.hold(False)
plot_axes.set_ylabel('Temperature (deg C)')
plot_axes.set_xlabel('Time')
plot_axes.set_title('Recorded Temperature')
return figure, plot_axes
def create_graph(figure, plot_axes, data, reference_profile, settings = dict()):
# Add in polygons for hilightning particular temperature ranges of interest.
plot_args = []
t = None
time_max = 60
temp_max = 80
handles = []
if data.any():
#data = numpy.copy(self.data)
data = data.transpose()
t, temperature = numpy.split(data, 2, axis = 0)
if t[0][-1] > time_max:
time_max = t[0][-1]
#if numpy.nanmax(temperature[0]) > temp_max:
# temp_max = numpy.nanmax(temperature[0])
plot_args.extend((t[0], temperature[0], 'b'))
handles.append('Recorded Temperature Profile')
if reference_profile is not None and reference_profile.any():
#reference_data = numpy.copy(self.reference_profile)
reference_data = reference_profile.transpose()
t2, reference_temperature = numpy.split(reference_data, 2, axis = 0)
# Add t[0] to all reference points to make it fit on the current graph
t2 = numpy.add(t2, t[0][0])
if t2[0][-1] > time_max:
time_max = t2[0][-1]
#if numpy.nanmax(reference_temperature[0]) > temp_max:
# temp_max = numpy.nanmax(reference_temperature[0])
plot_args.extend((t2[0], reference_temperature[0], 'r'))
handles.append('Reference Temperature Profile')
lines = plot_axes.plot(*plot_args)
# Patches must be changed after the axes are plotted.
graph_update_patches(figure, plot_axes, time_max, settings)
# Disabled the legend for the moment.
# @TODO Make a small subplot next to the main plot into which legends may be placed.
#self.plot_axes.legend(lines, handles, 'lower right', title = 'Temperature Profiles')
# Set a default in case no data is plotted. This may allow patches to show,
# and it will look a little nicer before recording starts.
#self.plot_axes.set_xlim(0, time_max)
plot_axes.set_ylim(0, temp_max)
image_data = cStringIO.StringIO()
# Save a 640x480 image, see http://stackoverflow.com/questions/13714454/specifying-and-saving-a-figure-with-exact-size-in-pixels
#canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(figure)
#canvas.draw()
#renderer = canvas.get_renderer()
#raw_data = renderer.tostring_argb()
#size = canvas.get_width_height()
figure.savefig(image_data, format = 'png', dpi = 96, figsize = (640/96, 480/96))
#image_data.seek(0)
return image_data
#return raw_data, size
def graph_update_patches(figure, plot_axes, time_max, settings):
if 'patches' in settings:
settings = settings['patches']
items = list()
for patch_id, patch_settings in settings.iteritems():
items.append((patch_settings['show'], patch_settings['vertices']))
legend_handles = []
legend_strings = []
for show, config in items:
color = '0.5' # default color.
if show:
origin, width, height, color, name = config
if time_max is not None:
width = time_max
p = plot_axes.add_patch(
matplotlib.patches.Rectangle(origin, width, height,
fc = color, visible = True,
fill = True)
)
legend_handles.append(p)
legend_strings.append(name)
# Disabled for the moment
figure.legend(legend_handles, legend_strings, 'lower right',
title = 'Enzyme Activity Zones')
return legend_handles
if __name__ == '__main__': if __name__ == '__main__':
dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue]) dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue])
MainApp().run() MainApp().run()