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:
parent
babac7ae2e
commit
c6e7aefb96
4
main.kv
4
main.kv
|
@ -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
336
main.py
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue