Changed SerialChooserButton to a spinner. Added status bar, and fixed clear data popup

This commit is contained in:
Kienan Stewart 2014-12-13 16:44:05 -05:00
parent f5ba978e64
commit eb565fe797
2 changed files with 204 additions and 61 deletions

102
main.kv
View File

@ -8,36 +8,90 @@
#<SerialPortButton@Button>: #<SerialPortButton@Button>:
<StatusWidget@BoxLayout>:
orientation: 'horizontal'
<YesNoModalView@Popup>:
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)
<StatusBar@BoxLayout>:
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
<MainWindow>: <MainWindow>:
label_wid: "Temperature Monitor" label_wid: "Temperature Monitor"
current_temperature: current_temperature current_temperature: current_temperature
serial_chooser_button: serial_chooser_button 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: current_temperature.on_temperature_change(self.lastTemperature)
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)
BoxLayout: BoxLayout:
id: 'top_menu' orientation: 'vertical'
size_hint_y: 0.1 BoxLayout:
pos_hint: {'top':1} size_hint_y: 0.1
orientation: 'horizontal' id: 'top_menu'
MyLabel: pos_hint: {'top':1}
id: current_temperature orientation: 'horizontal'
Button: MyLabel:
text: 'Start/Stop' id: current_temperature
Label: StatusWidget:
text: 'No File Chosen' id: startstop_widget
SerialPortButton: state: 'running'
id: serial_chooser_button on_state: setattr(startstop_button, 'text', self.change_state_labels[self.state])
dropdown: serial_chooser_dropdown.__self__ on_state: root.set_state(self.state)
on_release: serial_chooser_dropdown.open(self) Button:
SerialPortDropdown: id: startstop_button
id: serial_chooser_dropdown text: 'Pause'
data: on_press: startstop_widget.change_state()
on_serial_port_changed: serial_chooser_button.serial_port_selected(self.data) Button:
on_serial_port_changed: root.serial_port_changed(self.data) id: clear_button
PlotWidget: text: 'Clear'
size_hint_y: 0.9 on_press: root.initiate_clear_dialog()
id: mainplot 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: MainWindow:

163
main.py
View File

@ -8,7 +8,7 @@ from kivy.core.image.img_pygame import ImageLoaderPygame
import kivy.graphics.texture import kivy.graphics.texture
from kivy.graphics.texture import Texture from kivy.graphics.texture import Texture
from kivy.logger import Logger 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.boxlayout import BoxLayout
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown 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.gridlayout import GridLayout
from kivy.uix.image import Image from kivy.uix.image import Image
from kivy.uix.label import Label 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 kivy.lang
import numpy import numpy
import matplotlib import matplotlib
@ -36,22 +39,7 @@ defaultSerialPort = '/dev/ttyACM0'
class MainApp(App): class MainApp(App):
def build(self): 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() 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) Clock.schedule_interval(mainWindow.update, 0.25)
return mainWindow return mainWindow
@ -68,18 +56,20 @@ class MainApp(App):
class MainWindow(FloatLayout): class MainWindow(FloatLayout):
"""Main Window class""" """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) dataSource = StringProperty(defaultSerialPort)
dataSources = ListProperty(temp_log.list_serial_ports()) dataSources = ListProperty(temp_log.list_serial_ports())
lastTemperature = NumericProperty(-1000.) lastTemperature = NumericProperty(-1000.)
lastTime = NumericProperty(-1.) lastTime = NumericProperty(-1.)
recordingState = StringProperty('') recordingState = StringProperty('')
state = StringProperty('running')
lastStatus = DictProperty({})
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs) super(MainWindow, self).__init__(**kwargs)
# self.ids is available here; use it's references to connect bindings # self.ids is available here; use it's references to connect bindings
Logger.debug('MainWindow IDs: ' + str(self.ids)) 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): def update_last_temperature(self):
@ -89,10 +79,17 @@ class MainWindow(FloatLayout):
data = dataThreadDataQueue.get_nowait() data = dataThreadDataQueue.get_nowait()
if data is not None: if data is not None:
Logger.debug('Data: ' + str(data)) Logger.debug('Data: ' + str(data))
if 'data' in data and 'temperature' in data['data']: if self.state != 'paused':
self.lastTemperature = float(data['data']['temperature']) if 'data' in data and 'temperature' in data['data']:
if 'time' in data: self.lastStatus = {'status' : 'ok', 'message' : ''}
self.lastTime = float(data['time']) 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: except Queue.Empty:
pass pass
@ -108,7 +105,7 @@ class MainWindow(FloatLayout):
def update(self, dt): def update(self, dt):
self.update_data_sources() self.update_data_sources()
self.update_last_temperature() 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): def update_data_sources(self):
@ -127,8 +124,10 @@ class MainWindow(FloatLayout):
new_port = args[0] new_port = args[0]
if new_port == self.dataSource: if new_port == self.dataSource:
Logger.debug('MainWindow: Selected port matches current port, no change') Logger.debug('MainWindow: Selected port matches current port, no change')
else: elif new_port in self.dataSources:
self.dataSource = new_port self.dataSource = new_port
else:
self.dataSource = ''
def on_dataSource(self, instance, value): def on_dataSource(self, instance, value):
@ -148,33 +147,63 @@ class MainWindow(FloatLayout):
dataThread.start() 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): def __init__(self, **kwargs):
super(SerialPortButton, self).__init__(**kwargs) super(SerialPortButton, self).__init__(**kwargs)
self.text = '/dev/ttyACM0' 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): def on_text(self, instance, value):
Logger.debug('SerialPortButton serial_port_selected:' + str(args)) if value == self.noValueText:
if args: value = ''
if args[0]: Logger.debug('SerialPortDropdown: ' + 'selected with args ' + str(value))
self.text = args[0] self.dispatch('on_serial_port_changed', value)
else:
self.text = 'No serial port selected!'
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): class SerialPortDropdown(DropDown):
__events__ = ('on_serial_port_changed',) __events__ = ('on_serial_port_changed',)
defaultSerialPort = '/dev/ttyACM0'
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(SerialPortDropdown, self).__init__(**kwargs) super(SerialPortDropdown, self).__init__(**kwargs)
self.register_event_type('on_serial_port_changed') self.register_event_type('on_serial_port_changed')
self.size_hint = (None, None) 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()) self.on_dataSources(temp_log.list_serial_ports())
@ -188,10 +217,10 @@ class SerialPortDropdown(DropDown):
text = getattr(child, 'text', None) text = getattr(child, 'text', None)
if text and text in values: if text and text in values:
values_used.append(text) values_used.append(text)
Logger.debug('Child ' + str(child) + ' is used') #Logger.debug('Child ' + str(child) + ' is used')
continue continue
if text and text not in values: 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.remove_widget(child)
self.data = '' self.data = ''
self.dispatch('on_serial_port_changed', '') self.dispatch('on_serial_port_changed', '')
@ -199,7 +228,7 @@ class SerialPortDropdown(DropDown):
new_values = set(values) - set(values_used) new_values = set(values) - set(values_used)
for new_value in new_values: for new_value in new_values:
btn = Button(text = new_value, size_hint_y = None, height = 20) 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)) btn.bind(on_release=lambda btn: self.select(btn.text))
self.add_widget(btn) self.add_widget(btn)
@ -228,6 +257,26 @@ class MyLabel(Label):
self.text = 'Current temperature: ' + str(value) 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): class PlotWidget(Image):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -245,6 +294,11 @@ class PlotWidget(Image):
self._image_raw_data = None self._image_raw_data = None
def clear_data(self):
self.data = numpy.array([], dtype=float);
self.texture = None
def do_update(self): def do_update(self):
if not self.data.any(): if not self.data.any():
return return
@ -302,6 +356,41 @@ class PlotWidget(Image):
self.do_update() 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__': if __name__ == '__main__':
dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue]) dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue])
MainApp().run() MainApp().run()