Implemented output file choosing to save png and csv (raw data) on every update

This commit is contained in:
Kienan Stewart 2015-01-03 17:03:11 -05:00
parent 41d1e07749
commit 5ff70ebd24
2 changed files with 168 additions and 18 deletions

39
main.kv
View File

@ -35,6 +35,34 @@
id: status_message id: status_message
size_hint_x: 0.80 size_hint_x: 0.80
<OutputFileDialog>:
textinput: text_input
BoxLayout:
orientation: 'vertical'
Label:
text: "Select a folder & file name for the output. A csv & png file will be saved."
size_hint_y: 0.1
FileChooserListView:
id: filechooser
on_selection: text_input.text = self.selection and self.selection[0] or ''
BoxLayout:
size_hint_y: 0.1
orientation: 'horizontal'
Label:
text: 'File name:'
size_hint_x: 0.2
TextInput:
id: text_input
BoxLayout:
size_hint_y: 0.1
orientation: 'horizontal'
Button:
text: 'Cancel'
on_press: root.cancel()
Button:
text: 'Set Output File'
on_press: root.set_output(filechooser.path, text_input.text)
<MainWindow>: <MainWindow>:
label_wid: "Temperature Monitor" label_wid: "Temperature Monitor"
current_temperature: current_temperature current_temperature: current_temperature
@ -44,6 +72,10 @@
on_lastTemperature: mainplot.on_lastTemperature(self.lastTemperature) on_lastTemperature: mainplot.on_lastTemperature(self.lastTemperature)
on_lastTime: mainplot.on_lastTime(self.lastTime) on_lastTime: mainplot.on_lastTime(self.lastTime)
on_lastStatus: status_bar.update_status(self.lastStatus) on_lastStatus: status_bar.update_status(self.lastStatus)
raw_data_output_file: ''
image_output_file: ''
on_raw_data_output_file: mainplot.raw_data_output_file = self.raw_data_output_file
on_image_output_file: mainplot.image_output_file = self.image_output_file
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
BoxLayout: BoxLayout:
@ -53,6 +85,7 @@
orientation: 'horizontal' orientation: 'horizontal'
MyLabel: MyLabel:
id: current_temperature id: current_temperature
text: "Current Temperature: n/a"
StatusWidget: StatusWidget:
id: startstop_widget id: startstop_widget
state: 'running' state: 'running'
@ -66,13 +99,17 @@
id: clear_button id: clear_button
text: 'Clear' text: 'Clear'
on_press: root.initiate_clear_dialog() on_press: root.initiate_clear_dialog()
Label: Button:
id: output_button
text: 'No File Chosen' text: 'No File Chosen'
on_press: root.choose_output_file()
SerialPortButton: SerialPortButton:
id: serial_chooser_button id: serial_chooser_button
on_serial_port_changed: root.serial_port_changed(self.text) on_serial_port_changed: root.serial_port_changed(self.text)
PlotWidget: PlotWidget:
id: mainplot id: mainplot
on_image_output_file: self.update_image_output_file()
on_raw_data_output_file: self.update_raw_data_output_file()
size_hint_y: 0.8 size_hint_y: 0.8
StatusBar: StatusBar:
size_hint_y: 0.1 size_hint_y: 0.1

147
main.py
View File

@ -20,11 +20,13 @@ from kivy.uix.modalview import ModalView
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.uix.spinner import Spinner from kivy.uix.spinner import Spinner
import kivy.lang import kivy.lang
import numpy
import matplotlib import matplotlib
matplotlib.use('Agg') matplotlib.use('Agg')
import matplotlib.image import matplotlib.image
import matplotlib.pyplot import matplotlib.pyplot
import numpy
import os
import os.path
import pygame.image import pygame.image
import Queue import Queue
import StringIO import StringIO
@ -63,6 +65,8 @@ class MainWindow(FloatLayout):
recordingState = StringProperty('') recordingState = StringProperty('')
state = StringProperty('running') state = StringProperty('running')
lastStatus = DictProperty({}) lastStatus = DictProperty({})
image_output_file = StringProperty('')
raw_data_output_file = StringProperty('')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs) super(MainWindow, self).__init__(**kwargs)
@ -162,10 +166,93 @@ class MainWindow(FloatLayout):
Logger.debug('StatusWidget: process_dialog args: ' + str(args)) Logger.debug('StatusWidget: process_dialog args: ' + str(args))
if args and args[0]: if args and args[0]:
self.ids.mainplot.clear_data() self.ids.mainplot.clear_data()
self.image_output_file = ''
self.raw_data_output_file = ''
self.remove_widget(self.modal_view) self.remove_widget(self.modal_view)
self.modal_view = None 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._popup = Popup(title="Choose outputfile", content = output_file_dialog)
self._popup.open()
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:
# 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 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_button.text = root[0] + '[.csv|.png]'
else:
self.ids.output_button.text = 'No File Chosen'
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 SerialPortButton(Spinner): class SerialPortButton(Spinner):
__events__ = ('on_serial_port_changed',) __events__ = ('on_serial_port_changed',)
@ -229,6 +316,9 @@ class StatusWidget(BoxLayout):
class PlotWidget(Image): class PlotWidget(Image):
image_output_file = StringProperty('')
raw_data_output_file = StringProperty('')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(PlotWidget, self).__init__(**kwargs) super(PlotWidget, self).__init__(**kwargs)
self.data = numpy.array([], dtype=float); self.data = numpy.array([], dtype=float);
@ -250,22 +340,7 @@ class PlotWidget(Image):
def do_update(self): def do_update(self):
if not self.data.any(): image_data = self.to_image_data()
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)
#Logger.debug('time: ' + str(t[0]))
#Logger.debug('temperature: ' + str(temperature[0]))
self.plot_axes.plot(t[0], temperature[0])
image_data = StringIO.StringIO()
self.figure.savefig(image_data, format = 'png')
image_data.seek(0)
self._image_raw_data = image_data
# We can't use ImageLoaders since they assume it's a file on disk. # We can't use ImageLoaders since they assume it's a file on disk.
# This replicates code from ImageLoaderPygame.load() and ImageLoaderBase.populate() # This replicates code from ImageLoaderPygame.load() and ImageLoaderBase.populate()
try: try:
@ -282,6 +357,44 @@ class PlotWidget(Image):
self.image_data = ImageData(im.get_width(), im.get_height(), fmt, data) self.image_data = ImageData(im.get_width(), im.get_height(), fmt, data)
self.texture = Texture.create_from_data(self.image_data) self.texture = Texture.create_from_data(self.image_data)
self.texture.flip_vertical() 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_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)
#Logger.debug('time: ' + str(t[0]))
#Logger.debug('temperature: ' + str(temperature[0]))
self.plot_axes.plot(t[0], temperature[0])
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()
def on_lastTemperature(self, value): def on_lastTemperature(self, value):