From 5ff70ebd2455abcdcb5528d5c2a2c5a43f8e21cd Mon Sep 17 00:00:00 2001 From: Kienan Stewart Date: Sat, 3 Jan 2015 17:03:11 -0500 Subject: [PATCH] Implemented output file choosing to save png and csv (raw data) on every update --- main.kv | 39 ++++++++++++++- main.py | 147 +++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 168 insertions(+), 18 deletions(-) diff --git a/main.kv b/main.kv index d53b613..b550186 100644 --- a/main.kv +++ b/main.kv @@ -35,6 +35,34 @@ id: status_message size_hint_x: 0.80 +: + 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) + : label_wid: "Temperature Monitor" current_temperature: current_temperature @@ -44,6 +72,10 @@ on_lastTemperature: mainplot.on_lastTemperature(self.lastTemperature) on_lastTime: mainplot.on_lastTime(self.lastTime) 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: orientation: 'vertical' BoxLayout: @@ -53,6 +85,7 @@ orientation: 'horizontal' MyLabel: id: current_temperature + text: "Current Temperature: n/a" StatusWidget: id: startstop_widget state: 'running' @@ -66,13 +99,17 @@ id: clear_button text: 'Clear' on_press: root.initiate_clear_dialog() - Label: + Button: + id: output_button text: 'No File Chosen' + on_press: root.choose_output_file() SerialPortButton: id: serial_chooser_button on_serial_port_changed: root.serial_port_changed(self.text) PlotWidget: 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 StatusBar: size_hint_y: 0.1 diff --git a/main.py b/main.py index 7b2ab7f..b16adf1 100644 --- a/main.py +++ b/main.py @@ -20,11 +20,13 @@ from kivy.uix.modalview import ModalView from kivy.uix.popup import Popup from kivy.uix.spinner import Spinner import kivy.lang -import numpy import matplotlib matplotlib.use('Agg') import matplotlib.image import matplotlib.pyplot +import numpy +import os +import os.path import pygame.image import Queue import StringIO @@ -63,6 +65,8 @@ class MainWindow(FloatLayout): recordingState = StringProperty('') state = StringProperty('running') lastStatus = DictProperty({}) + image_output_file = StringProperty('') + raw_data_output_file = StringProperty('') def __init__(self, **kwargs): super(MainWindow, self).__init__(**kwargs) @@ -162,10 +166,93 @@ class MainWindow(FloatLayout): 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._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): __events__ = ('on_serial_port_changed',) @@ -229,6 +316,9 @@ class StatusWidget(BoxLayout): class PlotWidget(Image): + image_output_file = StringProperty('') + raw_data_output_file = StringProperty('') + def __init__(self, **kwargs): super(PlotWidget, self).__init__(**kwargs) self.data = numpy.array([], dtype=float); @@ -250,22 +340,7 @@ class PlotWidget(Image): def do_update(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 + 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: @@ -282,6 +357,44 @@ class PlotWidget(Image): 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_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):