From eb565fe7979d66bbf8f2f35af3af2e3ec3f3f60e Mon Sep 17 00:00:00 2001 From: Kienan Stewart Date: Sat, 13 Dec 2014 16:44:05 -0500 Subject: [PATCH] Changed SerialChooserButton to a spinner. Added status bar, and fixed clear data popup --- main.kv | 102 ++++++++++++++++++++++++++--------- main.py | 163 +++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 204 insertions(+), 61 deletions(-) diff --git a/main.kv b/main.kv index 5fe7dd5..ce0f91e 100644 --- a/main.kv +++ b/main.kv @@ -8,36 +8,90 @@ #: +: + orientation: 'horizontal' + +: + id: clear_modal_view + process_callback: + title: 'Clear graph?' + BoxLayout: + orientation: 'vertical' + Label: + id: default_content + text: 'Continuing will clear the graph and turn off logging to file.' + BoxLayout: + orientation: 'horizontal' + Button: + id: cancel_button + text: 'Cancel' + on_press: clear_modal_view.process(0) + Button: + id: confirm_button + text: 'Delete data' + on_press: clear_modal_view.process(1) + +: + orientation: 'horizontal' + pos_hint: { 'x' : 0.01 } + Image: + id: status_image + size_hint_x: 0.05 + source: './images/ok.png' + Label: + id: status_text + size_hint_x: 0.1 + Label: + id: status_message + size_hint_x: 0.80 + : label_wid: "Temperature Monitor" current_temperature: current_temperature serial_chooser_button: serial_chooser_button - on_dataSources: serial_chooser_dropdown.on_dataSources(self.dataSources) + on_dataSources: serial_chooser_button.values = self.dataSources on_lastTemperature: current_temperature.on_temperature_change(self.lastTemperature) on_lastTemperature: mainplot.on_lastTemperature(self.lastTemperature) on_lastTime: mainplot.on_lastTime(self.lastTime) + on_lastStatus: status_bar.update_status(self.lastStatus) BoxLayout: - id: 'top_menu' - size_hint_y: 0.1 - pos_hint: {'top':1} - orientation: 'horizontal' - MyLabel: - id: current_temperature - Button: - text: 'Start/Stop' - Label: - text: 'No File Chosen' - SerialPortButton: - id: serial_chooser_button - dropdown: serial_chooser_dropdown.__self__ - on_release: serial_chooser_dropdown.open(self) - SerialPortDropdown: - id: serial_chooser_dropdown - data: - on_serial_port_changed: serial_chooser_button.serial_port_selected(self.data) - on_serial_port_changed: root.serial_port_changed(self.data) - PlotWidget: - size_hint_y: 0.9 - id: mainplot - + orientation: 'vertical' + BoxLayout: + size_hint_y: 0.1 + id: 'top_menu' + pos_hint: {'top':1} + orientation: 'horizontal' + MyLabel: + id: current_temperature + StatusWidget: + id: startstop_widget + state: 'running' + on_state: setattr(startstop_button, 'text', self.change_state_labels[self.state]) + on_state: root.set_state(self.state) + Button: + id: startstop_button + text: 'Pause' + on_press: startstop_widget.change_state() + Button: + id: clear_button + text: 'Clear' + on_press: root.initiate_clear_dialog() + Label: + text: 'No File Chosen' + SerialPortButton: + id: serial_chooser_button + on_serial_port_changed: root.serial_port_changed(self.text) + #dropdown: serial_chooser_dropdown.__self__ + #on_release: serial_chooser_dropdown.open(self) + #SerialPortDropdown: + # id: serial_chooser_dropdown + # data: + # on_serial_port_changed: serial_chooser_button.serial_port_selected(self.data) + # on_serial_port_changed: root.serial_port_changed(self.data) + PlotWidget: + id: mainplot + size_hint_y: 0.8 + StatusBar: + size_hint_y: 0.1 + id: status_bar MainWindow: \ No newline at end of file diff --git a/main.py b/main.py index 161ef67..12cf1bd 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ 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 ListProperty, NumericProperty, ObjectProperty, StringProperty +from kivy.properties import DictProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.dropdown import DropDown @@ -16,6 +16,9 @@ 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 numpy import matplotlib @@ -36,22 +39,7 @@ defaultSerialPort = '/dev/ttyACM0' class MainApp(App): def build(self): - #root = kivy.lang.Builder.load_file('./main.kv') - #Logger.debug('root from super: ' + str(root)) - #Logger.debug(str(root.ids)) - #children = root.ids mainWindow = MainWindow() - #Logger.debug('mainWindow' + str(mainWindow)) - #Logger.debug(str(mainWindow.ids)) - #children = mainWindow.children[:] - #while children: - # child = children.pop() - # children.extend(child.children) - # id = getattr(child, 'id', 'no id') - # if id is None: - # id = 'no id' - # Logger.debug(id + ' : ' + str(child)) - # Logger.debug(str(child.ids)) Clock.schedule_interval(mainWindow.update, 0.25) return mainWindow @@ -68,18 +56,20 @@ class MainApp(App): class MainWindow(FloatLayout): """Main Window class""" - # @TODO fix on_X functions so they use specific bindings in the kv file rather than trying to call everything. dataSource = StringProperty(defaultSerialPort) dataSources = ListProperty(temp_log.list_serial_ports()) lastTemperature = NumericProperty(-1000.) lastTime = NumericProperty(-1.) recordingState = StringProperty('') + state = StringProperty('running') + lastStatus = DictProperty({}) 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.ids['serial_chooser_dropdown'].bind(on_serial_port_change=self.serial_port_changed) + self.modal_view = None + #self.ids['serial_chooser_dropdown'].bind(on_serial_port_change=self.serial_port_changed) def update_last_temperature(self): @@ -89,10 +79,17 @@ class MainWindow(FloatLayout): data = dataThreadDataQueue.get_nowait() if data is not None: Logger.debug('Data: ' + str(data)) - if 'data' in data and 'temperature' in data['data']: - self.lastTemperature = float(data['data']['temperature']) - if 'time' in data: - self.lastTime = float(data['time']) + 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') except Queue.Empty: pass @@ -108,7 +105,7 @@ class MainWindow(FloatLayout): def update(self, dt): self.update_data_sources() self.update_last_temperature() - Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive())) + #Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive())) def update_data_sources(self): @@ -127,8 +124,10 @@ class MainWindow(FloatLayout): new_port = args[0] if new_port == self.dataSource: Logger.debug('MainWindow: Selected port matches current port, no change') - else: + elif new_port in self.dataSources: self.dataSource = new_port + else: + self.dataSource = '' def on_dataSource(self, instance, value): @@ -148,33 +147,63 @@ class MainWindow(FloatLayout): dataThread.start() -class SerialPortButton(Button): + def set_state(self, new_state): + self.state = new_state - currentSerialPort = StringProperty('') + + 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.remove_widget(self.modal_view) + self.modal_view = None + + +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 serial_port_selected(self, *args): - Logger.debug('SerialPortButton serial_port_selected:' + str(args)) - if args: - if args[0]: - self.text = args[0] - else: - self.text = 'No serial port selected!' + 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 SerialPortDropdown(DropDown): __events__ = ('on_serial_port_changed',) + defaultSerialPort = '/dev/ttyACM0' def __init__(self, **kwargs): super(SerialPortDropdown, self).__init__(**kwargs) self.register_event_type('on_serial_port_changed') self.size_hint = (None, None) - Logger.debug(str(self.get_root_window())) + #Logger.debug(str(self.get_root_window())) self.on_dataSources(temp_log.list_serial_ports()) @@ -188,10 +217,10 @@ class SerialPortDropdown(DropDown): text = getattr(child, 'text', None) if text and text in values: values_used.append(text) - Logger.debug('Child ' + str(child) + ' is used') + #Logger.debug('Child ' + str(child) + ' is used') continue if text and text not in values: - Logger.debug('Child ' + str(child) + ' is now unused, to be removed') + #Logger.debug('Child ' + str(child) + ' is now unused, to be removed') self.remove_widget(child) self.data = '' self.dispatch('on_serial_port_changed', '') @@ -199,7 +228,7 @@ class SerialPortDropdown(DropDown): new_values = set(values) - set(values_used) for new_value in new_values: btn = Button(text = new_value, size_hint_y = None, height = 20) - Logger.debug('Child ' + str(btn) + ' added to dropdown with value ' + new_value) + #Logger.debug('Child ' + str(btn) + ' added to dropdown with value ' + new_value) btn.bind(on_release=lambda btn: self.select(btn.text)) self.add_widget(btn) @@ -228,6 +257,26 @@ class MyLabel(Label): 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): def __init__(self, **kwargs): @@ -245,6 +294,11 @@ class PlotWidget(Image): self._image_raw_data = None + def clear_data(self): + self.data = numpy.array([], dtype=float); + self.texture = None + + def do_update(self): if not self.data.any(): return @@ -302,6 +356,41 @@ class PlotWidget(Image): self.do_update() +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' + + if __name__ == '__main__': dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue]) MainApp().run()