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
on_dataSources: serial_chooser_button.values = self.dataSources
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_lastData: mainplot.update_data(self.lastData)
raw_data_output_file: ''
image_output_file: ''
on_raw_data_output_file: mainplot.raw_data_output_file = self.raw_data_output_file
@ -173,4 +172,5 @@
StatusBar:
size_hint_y: 0.1
id: status_bar
MainWindow:

336
main.py
View File

@ -1,7 +1,10 @@
import io
import kivy
from kivy.app import App
from kivy.clock import Clock
import kivy.core.image
import kivy.config
import kivy.core.image
from kivy.core.image import ImageData
from kivy.core.image.img_pil import ImageLoaderPIL
from kivy.core.image.img_pygame import ImageLoaderPygame
@ -22,6 +25,7 @@ from kivy.uix.spinner import Spinner
import kivy.lang
import matplotlib
matplotlib.use('Agg')
import matplotlib.backends.backend_agg
import matplotlib.image
import matplotlib.lines
import matplotlib.patches
@ -41,11 +45,13 @@ dataThreadDataQueue = Queue.Queue()
dataThreadCommandQueue = Queue.Queue()
defaultSerialPort = '/dev/ttyACM0'
kivy.config.Config.set('modules', 'monitor', '')
class MainApp(App):
def build(self):
mainWindow = MainWindow()
Clock.schedule_interval(mainWindow.update, 0.25)
Clock.schedule_interval(mainWindow.update, 0.5)
return mainWindow
@ -71,6 +77,8 @@ class MainWindow(FloatLayout):
image_output_file = StringProperty('')
raw_data_output_file = StringProperty('')
reference_profile_file = StringProperty('')
lastData = ListProperty([])
def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs)
@ -83,38 +91,39 @@ class MainWindow(FloatLayout):
def update_last_temperature(self):
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.
try:
data = dataThreadDataQueue.get_nowait()
if data is not None:
Logger.debug('Data: ' + str(data))
if self.state != 'paused':
if 'data' in data and 'temperature' in data['data']:
self.lastStatus = {'status' : 'ok', 'message' : ''}
self.lastTemperature = float(data['data']['temperature'])
if 'time' in data:
self.lastTime = float(data['time'])
if 'exception' in data:
self.lastStatus = {'status' : 'error', 'message' : data['exception']}
else:
self.lastStatus = {'status' : 'paused', 'message' : 'Data reception halted by user'}
Logger.debug('MainWindow: state set to ' + str(self.state) + ' : ignoring data')
if dataThreadDataQueue.qsize():
Logger.debug('Queue Size: ' + str(dataThreadDataQueue.qsize()))
except Queue.Empty:
pass
def on_lastTemperature(self, instance, value):
Logger.debug('lastTemperature has changed to: ' + str(value))
def on_lastTime(self, instance, value):
Logger.debug('lastTime has changed to: ' + str(value))
done = False
data = []
while not done:
try:
data.append(dataThreadDataQueue.get_nowait())
except Queue.Empty:
done = True
if data:
if len(data) > 1:
Logger.debug('update_last_temperature: ' + str(len(data)) + ' items retrieved from queue')
last_point = data[-1]
if self.state == 'paused':
# Try to update the last temperature, but not to the plotwidget,
# when the data reception is halted.
self.lastStatus = {'status' : 'paused', 'message' : 'Data receiption halted by user'}
if 'data' in last_point and 'temperature' in last_point['data']:
self.lastTemperature = float(last_point['data']['temperature'])
return
if 'data' in last_point and 'temperature' in last_point['data']:
self.lastStatus = {'status' : 'ok', 'message' : ''}
self.lastTemperature = float(last_point['data']['temperature'])
if 'time' in last_point:
self.lastTime = float(last_point['time'])
if 'exception' in last_point:
self.lastStatus = {'status' : 'error', 'message' : data['exception']}
self.lastData = data
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):
self.update_data_sources()
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()))
@ -378,7 +387,8 @@ class PlotWidget(Image):
show_alphaamylase = BooleanProperty(True)
file_write_interval = NumericProperty(15)
image_update_interval = NumericProperty(5)
buildingImage = BooleanProperty(False)
imageDataQueue = ObjectProperty(None, allownone = True)
# Common data that we should only instantiate once
# Source : http://www.howtobrew.com/section3/chapter14-1.html
@ -392,6 +402,7 @@ class PlotWidget(Image):
def __init__(self, **kwargs):
super(PlotWidget, self).__init__(**kwargs)
self.imageThread = None
self.data = numpy.array([], dtype=float);
self.lastTime = 0
self.lastTemperature = 0
@ -406,7 +417,7 @@ class PlotWidget(Image):
self._image_raw_data = None
self.reference_profile = None
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):
@ -415,34 +426,6 @@ class PlotWidget(Image):
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):
self.update_output_files(True)
self.data = numpy.array([], dtype=float);
@ -455,79 +438,49 @@ class PlotWidget(Image):
def do_update(self, *args):
image_data = self.to_image_data()
# We can't use ImageLoaders since they assume it's a file on disk.
# This replicates code from ImageLoaderPygame.load() and ImageLoaderBase.populate()
try:
im = pygame.image.load(image_data)
except:
Logger.warning('Image: Unable to load image from data')
raise
fmt = ''
if im.get_bytesize() == 3:
fmt = 'rgb'
elif im.get_bytesize() == 4:
fmt = 'rgba'
data = pygame.image.tostring(im, fmt.upper())
self.image_data = ImageData(im.get_width(), im.get_height(), fmt, data)
self.texture = Texture.create_from_data(self.image_data)
self.texture.flip_vertical()
# Update output files, if any
# @TODO Add in an update interval so the disk isn't hammered on every single update
self.update_output_files()
def to_image_data(self):
# Add in polygons for hilightning particular temperature ranges of interest.
plot_args = []
t = None
time_max = 60
temp_max = 80
handles = []
if self.data.any():
#data = numpy.copy(self.data)
data = self.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 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
if self.buildingImage:
# Check end condition, retrieve data, clean-up
if self.imageThread.isAlive():
return
self.imageThread.join(0.01)
image_data = None
try:
image_data = self.imageDataQueue.get_nowait()
except Queue.Empty:
pass
if image_data:
image = image_data['kivy_image']
self.texture = image.texture
self._image_raw_data = image_data['raw_image_data']
self.imageThread = None
self.imageDataQueue = None
# @TODO self.update_output_files() should may be a scheduled task
self.update_output_files()
self.buildingImage = False
return
self.buildingImage = True
self.imageDataQueue = Queue.Queue()
settings = {
'patches' : {
'betaglucan' : { 'show' : self.show_betaglucan,
'vertices' : self.betaglucan_vertices},
'protease' : { 'show' : self.show_protease,
'vertices' : self.protease_vertices},
'betaamylase' : { 'show' : self.show_betaamylase,
'vertices' : self.betaamylase_vertices},
'alphaamylase' : { 'show' : self.show_alphaamylase,
'vertices' : self.alphaamylase_vertices},
}
}
self.imageThread = threading.Thread(None, threaded_image_builder, None,
[self.data, self.reference_profile,
self.imageDataQueue, settings])
self.imageThread.start()
def update_output_files(self, force = False):
# 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:
Logger.debug('update_output_files going ahead')
self.update_raw_data_output_file()
self.update_image_output_file()
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)
def on_lastTemperature(self, value):
self.lastTemperature = value
def on_lastTime(self, value):
self.lastTime = value
self.update_data()
def update_data(self):
if self.lastTime == 0 or self.lastTemperature == 0:
return
newpoint = numpy.array([(self.lastTime, self.lastTemperature)], dtype=float, ndmin = 2)
def update_data(self, data = list(), *args):
points = []
for point in data:
if 'data' in point and 'time' in point and 'temperature' in point['data']:
points.append(numpy.array([(point['time'], point['data']['temperature'])],
dtype=float, ndmin =2))
extra_data = numpy.vstack(tuple(points))
if not self.data.any():
self.data = newpoint
self.data = extra_data
return
#Logger.debug('self.data: ' + str(self.data))
#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):
Logger.debug('PlotWidget: on_reference_profile_file ' + str(self.reference_profile_file))
self.load_reference_profile()
Clock.schedule_once(self.do_update)
def load_reference_profile(self):
@ -621,6 +567,110 @@ class StatusBar(BoxLayout):
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__':
dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue])
MainApp().run()