684 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			684 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env python
 | |
| 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
 | |
| import kivy.graphics.texture
 | |
| from kivy.graphics.texture import Texture
 | |
| from kivy.logger import Logger
 | |
| from kivy.properties import BooleanProperty, DictProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty
 | |
| from kivy.uix.boxlayout import BoxLayout
 | |
| from kivy.uix.button import Button
 | |
| from kivy.uix.dropdown import DropDown
 | |
| from kivy.uix.floatlayout import FloatLayout
 | |
| from kivy.uix.gridlayout import GridLayout
 | |
| from kivy.uix.image import Image
 | |
| from kivy.uix.label import Label
 | |
| from kivy.uix.modalview import ModalView
 | |
| from kivy.uix.popup import Popup
 | |
| 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
 | |
| import matplotlib.pyplot
 | |
| import numpy
 | |
| import os
 | |
| import os.path
 | |
| import pygame.image
 | |
| import Queue
 | |
| import cStringIO
 | |
| import temp_log
 | |
| import time
 | |
| import threading
 | |
| 
 | |
| dataThread = None
 | |
| 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.5)
 | |
|         return mainWindow
 | |
| 
 | |
| 
 | |
|     def on_stop(self):
 | |
|         global dataThreadCommandQueue
 | |
|         dataThreadCommandQueue.put_nowait('stop')
 | |
| 
 | |
| 
 | |
|     def on_start(self):
 | |
|         global dataThread
 | |
|         dataThread.start()
 | |
| 
 | |
| 
 | |
| class MainWindow(FloatLayout):
 | |
|     """Main Window class"""
 | |
|     dataSource = StringProperty(defaultSerialPort)
 | |
|     dataSources = ListProperty(temp_log.list_serial_ports())
 | |
|     lastTemperature = NumericProperty(-1000.)
 | |
|     lastTime = NumericProperty(-1.)
 | |
|     recordingState = StringProperty('')
 | |
|     state = StringProperty('running')
 | |
|     lastStatus = DictProperty({})
 | |
|     image_output_file = StringProperty('')
 | |
|     raw_data_output_file = StringProperty('')
 | |
|     reference_profile_file = StringProperty('')
 | |
|     lastData = ListProperty(list())
 | |
| 
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         super(MainWindow, self).__init__(**kwargs)
 | |
|         # self.ids is available here; use it's references to connect bindings
 | |
|         Logger.debug('MainWindow IDs: ' + str(self.ids))
 | |
|         self.modal_view = None
 | |
|         #self.ids['serial_chooser_dropdown'].bind(on_serial_port_change=self.serial_port_changed)
 | |
| 
 | |
| 
 | |
|     def update_last_temperature(self):
 | |
|         start = time.time()
 | |
|         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.
 | |
|         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' : last_point['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')
 | |
|         Logger.debug('update_last_temperature: Took ' + str(time.time() - start) + ' seconds to retrieve ' + str(len(data)) + ' items. ' + str(dataThreadDataQueue.qsize()) + ' items remain in queue')
 | |
| 
 | |
|     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()))
 | |
| 
 | |
| 
 | |
|     def update_data_sources(self):
 | |
|         self.dataSources = temp_log.list_serial_ports()
 | |
| 
 | |
| 
 | |
|     def on_dataSources(self, instance, value):
 | |
|         Logger.debug('dataSources changed to: ' + str(self.dataSources))
 | |
|         children = self.children[:]
 | |
| 
 | |
| 
 | |
|     def serial_port_changed(self, *args):
 | |
|         Logger.debug('MainWindow: Received on_serial_port_changed: ' + str(args))
 | |
|         Logger.debug('MainWindow: Current dataSource: ' + str(self.dataSource))
 | |
|         if args:
 | |
|             new_port = args[0]
 | |
|         if new_port == self.dataSource:
 | |
|             Logger.debug('MainWindow: Selected port matches current port, no change')
 | |
|         elif new_port in self.dataSources:
 | |
|             self.dataSource = new_port
 | |
|         else:
 | |
|             self.dataSource = ''
 | |
| 
 | |
| 
 | |
|     def on_dataSource(self, instance, value):
 | |
|         # Stop the current data thread, if any.
 | |
|         Logger.debug('MainWindow: stopping dataThread')
 | |
|         global dataThread, dataThreadDataQueue, dataThreadCommandQueue
 | |
|         dataThreadCommandQueue.put('stop')
 | |
|         # This could cause the UI to hang until the thread is joined, if ever.
 | |
|         dataThread.join()
 | |
|         # Start a new data thread using the selected data source
 | |
|         if value:
 | |
|             Logger.debug('MainWindow: Starting new data thread on ' + str(value))
 | |
|             # For some reason it seems necessary to create new Queue objects.
 | |
|             dataThreadCommandQueue = Queue.Queue()
 | |
|             dataThreadDataQueue = Queue.Queue()
 | |
|             dataThread = threading.Thread(None, temp_log.threaded_reader, None, [value, dataThreadDataQueue, dataThreadCommandQueue])
 | |
|             dataThread.start()
 | |
| 
 | |
| 
 | |
|     def set_state(self, new_state):
 | |
|         self.state = new_state
 | |
| 
 | |
| 
 | |
|     def initiate_clear_dialog(self):
 | |
|         if self.modal_view is None:
 | |
|             self.modal_view = YesNoModalView(id='startstop_modal_dialog')
 | |
|             self.modal_view.process_callback = self.process_clear_dialog
 | |
|         self.add_widget(self.modal_view)
 | |
| 
 | |
| 
 | |
|     def process_clear_dialog(self, *args, **kwargs):
 | |
|         Logger.debug('StatusWidget: process_dialog args: ' + str(args))
 | |
|         if args and args[0]:
 | |
|             self.ids.mainplot.clear_data()
 | |
|             self.image_output_file = ''
 | |
|             self.raw_data_output_file = ''
 | |
|         self.remove_widget(self.modal_view)
 | |
|         self.modal_view = None
 | |
| 
 | |
| 
 | |
|     def choose_output_file(self, *args, **kwargs):
 | |
|         Logger.debug('Choose Output File pressed')
 | |
|         output_file_dialog = OutputFileDialog(cancel = self.dismiss_popup,
 | |
|                                               set_output_file = self.set_output_file)
 | |
|         self.create_popup(title="Choose outputfile", content = output_file_dialog)
 | |
| 
 | |
| 
 | |
|     def set_output_file(self, path, file_name, *args, **kwargs):
 | |
|         #Logger.debug('Setouput file: ', str((path, file_name, args, kwargs)))
 | |
|         # If we're oaky with writing path + file_name + (.csv|.png)
 | |
|         files = {
 | |
|             'image_file' : os.path.join(path, file_name + '.png'),
 | |
|             'raw_data_file' : os.path.join(path, file_name + '.csv'),
 | |
|         }
 | |
|         errors = {}
 | |
|         has_errors = False
 | |
|         for k, file_name in files.iteritems():
 | |
|             errors[k] = self.validate_output_file(file_name)
 | |
|             if errors[k]:
 | |
|                 has_errors = True
 | |
|         if not has_errors:
 | |
|             Logger.debug('MainWindow: Set output files: ' + str(files))
 | |
|             self.raw_data_output_file = files['raw_data_file']
 | |
|             self.image_output_file = files['image_file']
 | |
|             self.dismiss_popup()
 | |
|         else:
 | |
|             # @TODO UI Feedback in the popup dialog
 | |
|             Logger.debug('MainWindow: Set output file errors: ' + str(errors))
 | |
| 
 | |
| 
 | |
|     def validate_output_file(self, file_name):
 | |
|         errors = []
 | |
|         # File doesn't exist, parent dir is writable
 | |
|         exists = os.path.exists(file_name)
 | |
|         # Note, the access check isn't for security, but usability here.
 | |
|         access = os.access(os.path.dirname(file_name), os.W_OK)
 | |
|         Logger.debug('MainWindow: validate_ouput_file file: ' + str(file_name))
 | |
|         Logger.debug('MainWindow: validate_output_file stat: ' + str(exists))
 | |
|         Logger.debug('MainWindow: validate_output_file parent dir write access: ' + str(access))
 | |
|         if exists:
 | |
|             errors.append('File already exists')
 | |
|         if not access:
 | |
|             errors.append('No write access to parent directory')
 | |
|         return errors
 | |
| 
 | |
| 
 | |
|     def create_popup(self, *args, **kwargs):
 | |
|         self._popup = Popup(*args, **kwargs)
 | |
|         self._popup.open()
 | |
| 
 | |
| 
 | |
|     def dismiss_popup(self, *args, **kwargs):
 | |
|         self._popup.dismiss()
 | |
| 
 | |
| 
 | |
|     def clear_output_files(self):
 | |
|         self.raw_data_output_file = ''
 | |
|         self.image_output_file = ''
 | |
| 
 | |
| 
 | |
|     def on_raw_data_output_file(self, *args, **kwargs):
 | |
|         if self.raw_data_output_file:
 | |
|             root = self.raw_data_output_file.rsplit('.', 1)
 | |
|             Logger.debug('MainWindow: ' + str(root))
 | |
|             self.ids.output_file_selected.text = root[0] + '[.csv|.png]'
 | |
|         else:
 | |
|             self.ids.output_file_selected.text = 'No file Chosen'
 | |
| 
 | |
| 
 | |
|     def select_reference_profile(self, *args, **kwargs):
 | |
|         dialog = SelectReferenceProfileDialog(output = self.set_reference_profile,
 | |
|                                               cancel = self.dismiss_popup)
 | |
|         self.create_popup(title='Reference Profile Selection',
 | |
|                           content = dialog)
 | |
| 
 | |
| 
 | |
|     def set_reference_profile(self, *args, **kwargs):
 | |
|         Logger.debug('MainWindow: reference profile kwargs ' + str(kwargs))
 | |
|         ref_file = ''
 | |
|         if kwargs['file']:
 | |
|             ref_file = kwargs['file'][0]
 | |
|         if not os.path.isfile(ref_file):
 | |
|             # Not a valid path/file
 | |
|             return
 | |
|         self.reference_profile_file = ref_file
 | |
|         self.dismiss_popup()
 | |
| 
 | |
| 
 | |
|     def on_reference_profile_file(self, *kargs, **kwargs):
 | |
|         if not self.reference_profile_file:
 | |
|             self.ids.reference_profile_selected.text = 'No profile selected'
 | |
|         else:
 | |
|             self.ids.reference_profile_selected.text = self.reference_profile_file
 | |
| 
 | |
| 
 | |
| class OutputFileDialog(FloatLayout):
 | |
| 
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         super(OutputFileDialog, self).__init__(*args, **kwargs)
 | |
|         self.cancel = kwargs['cancel']
 | |
|         self.set_output = kwargs['set_output_file']
 | |
| 
 | |
| 
 | |
|     def cancel(self, *args, **kwargs):
 | |
|         self.cancel(*args, **kwargs)
 | |
| 
 | |
| 
 | |
|     def set_output(self, *args, **kwargs):
 | |
|         self.set_output(*args, **kwargs)
 | |
| 
 | |
| 
 | |
| class SelectReferenceProfileDialog(FloatLayout):
 | |
| 
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         super(SelectReferenceProfileDialog, self).__init__(*args, **kwargs)
 | |
|         self.cancel = kwargs['cancel']
 | |
|         self.output = kwargs['output']
 | |
| 
 | |
| 
 | |
|     def cancel(self, *args, **kwargs):
 | |
|         self.cancel(*args, **kwargs)
 | |
| 
 | |
| 
 | |
|     def set_output(self, *args, **kwargs):
 | |
|         self.output(*args, **kwargs)
 | |
| 
 | |
| 
 | |
| class SerialPortButton(Spinner):
 | |
| 
 | |
|     __events__ = ('on_serial_port_changed',)
 | |
|     noValueText = StringProperty('No serial port selected')
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         super(SerialPortButton, self).__init__(**kwargs)
 | |
|         self.text = '/dev/ttyACM0'
 | |
|         self.register_event_type('on_serial_port_changed')
 | |
|         self.values = temp_log.list_serial_ports()
 | |
| 
 | |
| 
 | |
|     def on_text(self, instance, value):
 | |
|         if value == self.noValueText:
 | |
|             value = ''
 | |
|         Logger.debug('SerialPortDropdown: ' + 'selected with args ' + str(value))
 | |
|         self.dispatch('on_serial_port_changed', value)
 | |
| 
 | |
| 
 | |
|     def on_values(self, instance, value):
 | |
|         if self.text not in self.values:
 | |
|             self.text = self.noValueText
 | |
| 
 | |
| 
 | |
|     def on_serial_port_changed(self, *args, **kwargs):
 | |
|         pass
 | |
| 
 | |
| 
 | |
| class MyLabel(Label):
 | |
| 
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         super(MyLabel, self).__init__(**kwargs)
 | |
| 
 | |
| 
 | |
|     def on_temperature_change(self, value):
 | |
|         if not value:
 | |
|             value = 'n/a'
 | |
|         self.text = 'Current temperature: ' + str(value)
 | |
| 
 | |
| 
 | |
| class StatusWidget(BoxLayout):
 | |
| 
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         super(StatusWidget, self).__init__(**kwargs)
 | |
|         self.states = ['running', 'paused']
 | |
|         self.change_state_labels = {'running' : 'Pause', 'paused' : 'Resume'}
 | |
|         self.modal_view = None
 | |
| 
 | |
| 
 | |
|     def change_state(self):
 | |
|         current_state = self.states.index(self.state)
 | |
|         new_state = (current_state + 1) % len(self.states)
 | |
|         self.state = self.states[new_state]
 | |
| 
 | |
| 
 | |
|     def on_state(self, instance, value):
 | |
|         Logger.debug('StatusWidget: new state ' + str(self.state))
 | |
| 
 | |
| 
 | |
| class PlotWidget(Image):
 | |
| 
 | |
|     image_output_file = StringProperty('')
 | |
|     raw_data_output_file = StringProperty('')
 | |
|     reference_profile_file = StringProperty('')
 | |
|     # @TODO Include a way to change these properties.
 | |
|     show_betaglucan = BooleanProperty(True)
 | |
|     show_protease = BooleanProperty(True)
 | |
|     show_betaamylase = BooleanProperty(True)
 | |
|     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
 | |
|     # accessed on march 21, 2015
 | |
|     # x_vertices = ((x, y), width, height, color, legend_string)
 | |
|     betaglucan_vertices = ((0, 35), 12000, 10, '#57ff14', 'Betaglucanase')
 | |
|     protease_vertices = ((0,45), 12000, 10, '#f3ff14', 'Protease / Peptidase')
 | |
|     betaamylase_vertices = ((0,55), 12000, 10.6, '#ffc014', 'Beta Amylase')
 | |
|     alphaamylase_vertices = ((0, 67.7), 12000, 4.5, '#ff6614', 'Alpha Amylase')
 | |
| 
 | |
| 
 | |
|     def __init__(self, **kwargs):
 | |
|         super(PlotWidget, self).__init__(**kwargs)
 | |
|         self.imageThread = None
 | |
|         self.data = numpy.array([], dtype=float);
 | |
|         self.lastTime = 0
 | |
|         self.lastTemperature = 0
 | |
|         self.lastSave = 0
 | |
|         self.figure = matplotlib.pyplot.figure()
 | |
|         self.plot_axes = self.figure.add_subplot(1, 1, 1)
 | |
|         self.plot_axes.hold(False)
 | |
|         self.plot_axes.set_ylabel('Temperature (deg C)')
 | |
|         self.plot_axes.set_xlabel('Time')
 | |
|         self.plot_axes.set_title('Recorded Temperature')
 | |
|         self.image_data = None
 | |
|         self._image_raw_data = None
 | |
|         self.reference_profile = None
 | |
|         Clock.schedule_interval(self.do_update, self.image_update_interval)
 | |
|         Clock.schedule_once(self.do_update)
 | |
| 
 | |
| 
 | |
|     def on_image_update_interval(self, *args):
 | |
|         # Reschedule the update function.
 | |
|         Clock.unschedule(self.do_update, True)
 | |
|         Clock.schedule_interval(self.do_update, self.image_update_interval)
 | |
| 
 | |
| 
 | |
|     def clear_data(self):
 | |
|         self.update_output_files(True)
 | |
|         self.data = numpy.array([], dtype=float);
 | |
|         self.reference_profile_file = ''
 | |
|         self.reference_profile = None
 | |
|         self.texture = None
 | |
|         self.image_output_file = ''
 | |
|         self.raw_data_output_file = ''
 | |
|         self.do_update()
 | |
| 
 | |
| 
 | |
|     def do_update(self, *args):
 | |
|         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
 | |
|         if force or time.time() - self.lastSave > self.file_write_interval:
 | |
|             self.update_raw_data_output_file()
 | |
|             self.update_image_output_file()
 | |
|             self.lastSave = time.time()
 | |
| 
 | |
| 
 | |
|     def update_raw_data_output_file(self):
 | |
|         if self.raw_data_output_file and self.data.any():
 | |
|             numpy.savetxt(self.raw_data_output_file, self.data, delimiter=',')
 | |
| 
 | |
| 
 | |
|     def update_image_output_file(self, image_data = None):
 | |
|         if not image_data:
 | |
|             image_data = self._image_raw_data
 | |
|         if self.image_output_file and image_data:
 | |
|             f = open(self.image_output_file, 'w')
 | |
|             if f:
 | |
|                 Logger.debug('Updated image output file')
 | |
|                 f.write(image_data)
 | |
|                 f.close()
 | |
|             else:
 | |
|                 Logger.debug('update_image_output_file: Unable to open file ' + self.image_output_file)
 | |
| 
 | |
| 
 | |
|     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.array(list(), dtype = float)
 | |
|         if points:
 | |
|             extra_data = numpy.vstack(tuple(points))
 | |
|         if not extra_data.any():
 | |
|             return
 | |
|         if not self.data.any():
 | |
|             self.data = extra_data
 | |
|             return
 | |
|         #Logger.debug('self.data: ' + str(self.data))
 | |
|         #Logger.debug('newpoint: ' + str(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()
 | |
| 
 | |
| 
 | |
|     def load_reference_profile(self):
 | |
|         if not self.reference_profile_file:
 | |
|             return
 | |
|         self.reference_profile = numpy.loadtxt(self.reference_profile_file,
 | |
|                                                delimiter=',')
 | |
|         Logger.debug('PlotWidget: reference_profile: ' + str(self.reference_profile))
 | |
| 
 | |
| 
 | |
| class YesNoModalView(Popup):
 | |
| 
 | |
|     def process(self, result, *args, **kwargs):
 | |
|         Logger.debug('YesNoModalView: ' + str(result) + ' ' + str(args))
 | |
|         Logger.debug('YesNoModalView: callback ' + str(self.process_callback))
 | |
|         if self.process_callback:
 | |
|             self.process_callback(result, *args, **kwargs)
 | |
|         self.dismiss()
 | |
| 
 | |
| 
 | |
| class StatusBar(BoxLayout):
 | |
| 
 | |
|     def update_status(self, status):
 | |
|         Logger.debug('StatusBar: received status ' + str(status))
 | |
|         Logger.debug('StatusBar: ids available ' + str(self.ids))
 | |
|         if 'message' in status:
 | |
|             self.ids.status_message.text = status['message']
 | |
|         if 'status' in status:
 | |
|             current_status = status['status']
 | |
|             if current_status == 'ok':
 | |
|                 self.ids.status_text.text = 'Running'
 | |
|                 self.ids.status_image.source = './images/ok.png'
 | |
|                 if not self.ids.status_message.text:
 | |
|                     self.ids.status_message.text = "Everything's perfectly all right now. We're fine. We're all fine here now, thank you. How are you?"
 | |
|             elif current_status == 'error':
 | |
|                 self.ids.status_text.text = 'ERROR'
 | |
|                 self.ids.status_image.source = './images/error.png'
 | |
|             elif current_status == 'paused':
 | |
|                 self.ids.status_text.text = 'Paused'
 | |
|                 self.ids.status_image.source = './images/warning.png'
 | |
|             else:
 | |
|                 self.ids.status_text.text = 'Unknown Status: ' + current_status
 | |
|                 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()
 |