2014-12-09 03:12:21 +00:00
import kivy
from kivy . app import App
2014-12-10 22:22:52 +00:00
from kivy . clock import Clock
2014-12-11 03:38:40 +00:00
import kivy . core . image
2014-12-13 17:57:33 +00:00
from kivy . core . image import ImageData
from kivy . core . image . img_pil import ImageLoaderPIL
2014-12-11 03:38:40 +00:00
from kivy . core . image . img_pygame import ImageLoaderPygame
import kivy . graphics . texture
2014-12-13 17:57:33 +00:00
from kivy . graphics . texture import Texture
2014-12-10 22:22:52 +00:00
from kivy . logger import Logger
2014-12-13 21:44:05 +00:00
from kivy . properties import DictProperty , ListProperty , NumericProperty , ObjectProperty , StringProperty
2014-12-09 03:12:21 +00:00
from kivy . uix . boxlayout import BoxLayout
2014-12-13 17:57:33 +00:00
from kivy . uix . button import Button
from kivy . uix . dropdown import DropDown
2014-12-10 22:22:52 +00:00
from kivy . uix . floatlayout import FloatLayout
2014-12-13 17:57:33 +00:00
from kivy . uix . gridlayout import GridLayout
2014-12-11 03:38:40 +00:00
from kivy . uix . image import Image
2014-12-10 22:22:52 +00:00
from kivy . uix . label import Label
2014-12-13 21:44:05 +00:00
from kivy . uix . modalview import ModalView
from kivy . uix . popup import Popup
from kivy . uix . spinner import Spinner
2014-12-10 22:22:52 +00:00
import kivy . lang
2014-12-11 03:38:40 +00:00
import matplotlib
matplotlib . use ( ' Agg ' )
import matplotlib . image
import matplotlib . pyplot
2015-01-03 22:03:11 +00:00
import numpy
import os
import os . path
2014-12-13 17:57:33 +00:00
import pygame . image
2014-12-10 22:22:52 +00:00
import Queue
2014-12-11 03:38:40 +00:00
import StringIO
2014-12-10 22:22:52 +00:00
import temp_log
import threading
2014-12-09 03:12:21 +00:00
2014-12-10 22:22:52 +00:00
dataThread = None
dataThreadDataQueue = Queue . Queue ( )
dataThreadCommandQueue = Queue . Queue ( )
2014-12-13 17:57:33 +00:00
defaultSerialPort = ' /dev/ttyACM0 '
2014-12-10 22:22:52 +00:00
class MainApp ( App ) :
2014-12-09 03:12:21 +00:00
def build ( self ) :
2014-12-10 22:22:52 +00:00
mainWindow = MainWindow ( )
2014-12-13 17:57:33 +00:00
Clock . schedule_interval ( mainWindow . update , 0.25 )
2014-12-10 22:22:52 +00:00
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 """
2014-12-13 17:57:33 +00:00
dataSource = StringProperty ( defaultSerialPort )
dataSources = ListProperty ( temp_log . list_serial_ports ( ) )
2014-12-11 03:38:40 +00:00
lastTemperature = NumericProperty ( - 1000. )
lastTime = NumericProperty ( - 1. )
2014-12-13 17:57:33 +00:00
recordingState = StringProperty ( ' ' )
2014-12-13 21:44:05 +00:00
state = StringProperty ( ' running ' )
lastStatus = DictProperty ( { } )
2015-01-03 22:03:11 +00:00
image_output_file = StringProperty ( ' ' )
raw_data_output_file = StringProperty ( ' ' )
2015-01-04 17:58:58 +00:00
reference_profile_file = StringProperty ( ' ' )
2014-12-10 22:22:52 +00:00
2014-12-13 17:57:33 +00:00
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 ) )
2014-12-13 21:44:05 +00:00
self . modal_view = None
#self.ids['serial_chooser_dropdown'].bind(on_serial_port_change=self.serial_port_changed)
2014-12-13 17:57:33 +00:00
def update_last_temperature ( self ) :
2014-12-10 22:22:52 +00:00
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 :
2014-12-11 03:38:40 +00:00
Logger . debug ( ' Data: ' + str ( data ) )
2014-12-13 21:44:05 +00:00
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 ' )
2014-12-10 22:22:52 +00:00
except Queue . Empty :
pass
2014-12-11 03:38:40 +00:00
2014-12-10 22:22:52 +00:00
def on_lastTemperature ( self , instance , value ) :
Logger . debug ( ' lastTemperature has changed to: ' + str ( value ) )
2014-12-11 03:38:40 +00:00
def on_lastTime ( self , instance , value ) :
Logger . debug ( ' lastTime has changed to: ' + str ( value ) )
2014-12-13 17:57:33 +00:00
def update ( self , dt ) :
self . update_data_sources ( )
self . update_last_temperature ( )
2014-12-13 21:44:05 +00:00
#Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive()))
2014-12-13 17:57:33 +00:00
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 ' )
2014-12-13 21:44:05 +00:00
elif new_port in self . dataSources :
2014-12-13 17:57:33 +00:00
self . dataSource = new_port
2014-12-13 21:44:05 +00:00
else :
self . dataSource = ' '
2014-12-13 17:57:33 +00:00
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 ( )
2014-12-13 21:44:05 +00:00
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 )
2014-12-13 17:57:33 +00:00
2014-12-13 21:44:05 +00:00
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 ( )
2015-01-03 22:03:11 +00:00
self . image_output_file = ' '
self . raw_data_output_file = ' '
2014-12-13 21:44:05 +00:00
self . remove_widget ( self . modal_view )
self . modal_view = None
2015-01-03 22:03:11 +00:00
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 )
2015-01-04 17:58:58 +00:00
self . create_popup ( title = " Choose outputfile " , content = output_file_dialog )
2015-01-03 22:03:11 +00:00
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 :
2015-01-04 17:58:58 +00:00
# @TODO UI Feedback in the popup dialog
2015-01-03 22:03:11 +00:00
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
2015-01-04 17:58:58 +00:00
def create_popup ( self , * args , * * kwargs ) :
self . _popup = Popup ( * args , * * kwargs )
self . _popup . open ( )
2015-01-03 22:03:11 +00:00
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 ) )
2015-01-04 17:58:58 +00:00
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 '
2015-01-03 22:03:11 +00:00
else :
2015-01-04 17:58:58 +00:00
self . ids . reference_profile_selected . text = self . reference_profile_file
2015-01-03 22:03:11 +00:00
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 )
2015-01-04 17:58:58 +00:00
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 )
2014-12-13 21:44:05 +00:00
class SerialPortButton ( Spinner ) :
__events__ = ( ' on_serial_port_changed ' , )
noValueText = StringProperty ( ' No serial port selected ' )
2014-12-13 17:57:33 +00:00
def __init__ ( self , * * kwargs ) :
super ( SerialPortButton , self ) . __init__ ( * * kwargs )
self . text = ' /dev/ttyACM0 '
2014-12-13 21:44:05 +00:00
self . register_event_type ( ' on_serial_port_changed ' )
self . values = temp_log . list_serial_ports ( )
2014-12-13 17:57:33 +00:00
2014-12-13 21:44:05 +00:00
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
2014-12-13 17:57:33 +00:00
2014-12-10 22:22:52 +00:00
class MyLabel ( Label ) :
2014-12-09 03:12:21 +00:00
2014-12-13 17:57:33 +00:00
def __init__ ( self , * * kwargs ) :
super ( MyLabel , self ) . __init__ ( * * kwargs )
def on_temperature_change ( self , value ) :
if not value :
value = ' n/a '
2014-12-10 22:22:52 +00:00
self . text = ' Current temperature: ' + str ( value )
2014-12-09 03:12:21 +00:00
2014-12-13 21:44:05 +00:00
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 ) )
2014-12-11 03:38:40 +00:00
class PlotWidget ( Image ) :
2015-01-03 22:03:11 +00:00
image_output_file = StringProperty ( ' ' )
raw_data_output_file = StringProperty ( ' ' )
2015-01-04 17:58:58 +00:00
reference_profile_file = StringProperty ( ' ' )
2015-01-03 22:03:11 +00:00
2014-12-11 03:38:40 +00:00
def __init__ ( self , * * kwargs ) :
2014-12-13 17:57:33 +00:00
super ( PlotWidget , self ) . __init__ ( * * kwargs )
2014-12-11 03:38:40 +00:00
self . data = numpy . array ( [ ] , dtype = float ) ;
self . lastTime = 0
self . lastTemperature = 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 ' )
2014-12-13 17:57:33 +00:00
self . image_data = None
self . _image_raw_data = None
2015-01-04 17:58:58 +00:00
self . reference_profile = None
2014-12-11 03:38:40 +00:00
2014-12-13 21:44:05 +00:00
def clear_data ( self ) :
self . data = numpy . array ( [ ] , dtype = float ) ;
2015-01-04 17:58:58 +00:00
self . reference_profile_file = ' '
self . reference_profile = None
2014-12-13 21:44:05 +00:00
self . texture = None
2015-01-04 17:58:58 +00:00
self . image_output_file = ' '
self . raw_data_output_file = ' '
2014-12-13 21:44:05 +00:00
2014-12-13 17:57:33 +00:00
def do_update ( self ) :
2015-01-03 22:03:11 +00:00
image_data = self . to_image_data ( )
2014-12-13 17:57:33 +00:00
# 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 ( )
2015-01-03 22:03:11 +00:00
# Update output files, if any
# @TODO Add in an update interval so the disk isn't hammered on every single update
self . update_image_output_file ( )
self . update_raw_data_output_file ( )
def to_image_data ( self ) :
if not self . data . any ( ) :
return
#Logger.debug('self.data: ' + str(self.data))
#Logger.debug('self.data shape: ' + str(self.data.shape))
data = numpy . copy ( self . data )
data = data . transpose ( )
#Logger.debug('transpoed data: ' + str(data))
#Logger.debug('transposed data shape: ' + str(data.shape))
t , temperature = numpy . split ( data , 2 , axis = 0 )
2015-01-04 17:58:58 +00:00
reference_data = None
if self . reference_profile is not None and self . reference_profile . any ( ) :
reference_data = numpy . copy ( self . reference_profile )
reference_data = reference_data . 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 ] )
#Logger.debug('PlotWidget: reference profile t: ' + str(t2))
#Logger.debug('PlotWidget: reference profile temp: ' + str(reference_temperature))
#Logger.debug('PlotWidget: time: ' + str(t[0]))
#Logger.debug('PlotWidget: temperature: ' + str(temperature[0]))
if reference_data is not None and reference_data . any ( ) :
self . plot_axes . plot ( t [ 0 ] , temperature [ 0 ] , ' b ' , t2 [ 0 ] , reference_temperature [ 0 ] , ' r ' )
else :
self . plot_axes . plot ( t [ 0 ] , temperature [ 0 ] , ' b ' )
2015-01-03 22:03:11 +00:00
image_data = StringIO . StringIO ( )
self . figure . savefig ( image_data , format = ' png ' )
image_data . seek ( 0 )
self . _image_raw_data = image_data
return self . _image_raw_data
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 . to_image_data ( )
if self . image_output_file and image_data :
f = open ( self . image_output_file , ' w ' )
f . write ( image_data . read ( ) )
f . close ( )
2014-12-11 03:38:40 +00:00
2014-12-13 17:57:33 +00:00
def on_lastTemperature ( self , value ) :
2014-12-11 03:38:40 +00:00
self . lastTemperature = value
2014-12-13 17:57:33 +00:00
def on_lastTime ( self , value ) :
2014-12-11 03:38:40 +00:00
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 )
if not self . data . any ( ) :
self . data = newpoint
return
#Logger.debug('self.data: ' + str(self.data))
#Logger.debug('newpoint: ' + str(newpoint))
self . data = numpy . vstack ( ( self . data , newpoint ) )
2014-12-13 17:57:33 +00:00
self . do_update ( )
2014-12-11 03:38:40 +00:00
2015-01-04 17:58:58 +00:00
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 ) )
2014-12-13 21:44:05 +00:00
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 '
2014-12-09 03:12:21 +00:00
if __name__ == ' __main__ ' :
2014-12-10 22:22:52 +00:00
dataThread = threading . Thread ( None , temp_log . threaded_reader , None , [ ' /dev/ttyACM0 ' , dataThreadDataQueue , dataThreadCommandQueue ] )
MainApp ( ) . run ( )