Implemented output file choosing to save png and csv (raw data) on every update
This commit is contained in:
		
							parent
							
								
									41d1e07749
								
							
						
					
					
						commit
						5ff70ebd24
					
				
							
								
								
									
										39
									
								
								main.kv
								
								
								
								
							
							
						
						
									
										39
									
								
								main.kv
								
								
								
								
							|  | @ -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
								
								
								
								
							
							
						
						
									
										147
									
								
								main.py
								
								
								
								
							|  | @ -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): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue