Better responses for serial port changes, temperature handling. Fixed PlotWidget image loading for kivy 1.9

This commit is contained in:
Kienan Stewart 2014-12-13 12:57:33 -05:00
parent bf5a6c6920
commit 5e6a2d3f09
2 changed files with 211 additions and 53 deletions

56
main.kv
View File

@ -1,23 +1,43 @@
<MyLabel@Label>: #<MyLabel@Label>:
text: 'Current Temperature: n/a' # text: 'Current Temperature: n/a'
<PlotWidget@Image>: #<PlotWidget@Image>:
allow_stretch: True # allow_stretch: True
#<SerialPortDropdown@DropDown>:
#<SerialPortButton@Button>:
<MainWindow>: <MainWindow>:
label_wid: "Temperature Monitor" label_wid: "Temperature Monitor"
current_temperature: current_temperature
serial_chooser_button: serial_chooser_button
on_dataSources: serial_chooser_dropdown.on_dataSources(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)
BoxLayout: BoxLayout:
orientation: 'vertical' id: 'top_menu'
BoxLayout: size_hint_y: 0.1
size_hint_y: 0.1 pos_hint: {'top':1}
orientation: 'horizontal' orientation: 'horizontal'
MyLabel: MyLabel:
id: 'current_temperature' id: current_temperature
Button: Button:
text: 'Start/Stop' text: 'Start/Stop'
Label: Label:
text: 'No File Chosen' text: 'No File Chosen'
Label: SerialPortButton:
text: 'Data Source' id: serial_chooser_button
PlotWidget: dropdown: serial_chooser_dropdown.__self__
id: 'mainplot' 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
MainWindow:

208
main.py
View File

@ -2,12 +2,18 @@ import kivy
from kivy.app import App from kivy.app import App
from kivy.clock import Clock from kivy.clock import Clock
import kivy.core.image import kivy.core.image
from kivy.core.image import ImageData
from kivy.core.image.img_pil import ImageLoaderPIL
from kivy.core.image.img_pygame import ImageLoaderPygame from kivy.core.image.img_pygame import ImageLoaderPygame
import kivy.graphics.texture import kivy.graphics.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 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.dropdown import DropDown
from kivy.uix.floatlayout import FloatLayout from kivy.uix.floatlayout import FloatLayout
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
import kivy.lang import kivy.lang
@ -16,6 +22,7 @@ import matplotlib
matplotlib.use('Agg') matplotlib.use('Agg')
import matplotlib.image import matplotlib.image
import matplotlib.pyplot import matplotlib.pyplot
import pygame.image
import Queue import Queue
import StringIO import StringIO
import temp_log import temp_log
@ -24,12 +31,28 @@ import threading
dataThread = None dataThread = None
dataThreadDataQueue = Queue.Queue() dataThreadDataQueue = Queue.Queue()
dataThreadCommandQueue = Queue.Queue() dataThreadCommandQueue = Queue.Queue()
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()
Clock.schedule_interval(mainWindow.update_last_temperature, 0.25) #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 return mainWindow
@ -45,11 +68,21 @@ class MainApp(App):
class MainWindow(FloatLayout): class MainWindow(FloatLayout):
"""Main Window class""" """Main Window class"""
dataSource = StringProperty('/dev/ttyACM0') # @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.) lastTemperature = NumericProperty(-1000.)
lastTime = NumericProperty(-1.) lastTime = NumericProperty(-1.)
recordingState = StringProperty('')
def update_last_temperature(self, dt): 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)
def update_last_temperature(self):
global dataThreadDataQueue global dataThreadDataQueue
# This breaks if there are more updates in the queue than the frequency of update_last_temperature - it pulls old data out of the queue, and not the most recent. # This breaks if there are more updates in the queue than the frequency of update_last_temperature - it pulls old data out of the queue, and not the most recent.
try: try:
@ -66,49 +99,139 @@ class MainWindow(FloatLayout):
def on_lastTemperature(self, instance, value): def on_lastTemperature(self, instance, value):
Logger.debug('lastTemperature has changed to: ' + str(value)) Logger.debug('lastTemperature has changed to: ' + str(value))
children = self.children[:]
while children:
child = children.pop()
children.extend(child.children)
if child is self:
continue
try:
child.on_lastTemperature(instance, value)
#Logger.debug('Called on_lastTemperature for child: ' + str(child))
except AttributeError:
pass
except Exception, e:
Logger.exception(str(e))
def on_lastTime(self, instance, value): def on_lastTime(self, instance, value):
Logger.debug('lastTime has changed to: ' + str(value)) Logger.debug('lastTime has changed to: ' + str(value))
def update(self, dt):
self.update_data_sources()
self.update_last_temperature()
Logger.debug('MainWindow: dataThread ' + str(dataThread.name) + ' status: ' + str(dataThread.is_alive()))
def update_data_sources(self):
self.dataSources = temp_log.list_serial_ports()
def on_dataSources(self, instance, value):
Logger.debug('dataSources changed to: ' + str(self.dataSources))
children = self.children[:] children = self.children[:]
def serial_port_changed(self, *args):
Logger.debug('MainWindow: Received on_serial_port_changed: ' + str(args))
Logger.debug('MainWindow: Current dataSource: ' + str(self.dataSource))
if args:
new_port = args[0]
if new_port == self.dataSource:
Logger.debug('MainWindow: Selected port matches current port, no change')
else:
self.dataSource = new_port
def on_dataSource(self, instance, value):
# Stop the current data thread, if any.
Logger.debug('MainWindow: stopping dataThread')
global dataThread, dataThreadDataQueue, dataThreadCommandQueue
dataThreadCommandQueue.put('stop')
# This could cause the UI to hang until the thread is joined, if ever.
dataThread.join()
# Start a new data thread using the selected data source
if value:
Logger.debug('MainWindow: Starting new data thread on ' + str(value))
# For some reason it seems necessary to create new Queue objects.
dataThreadCommandQueue = Queue.Queue()
dataThreadDataQueue = Queue.Queue()
dataThread = threading.Thread(None, temp_log.threaded_reader, None, [value, dataThreadDataQueue, dataThreadCommandQueue])
dataThread.start()
class SerialPortButton(Button):
currentSerialPort = StringProperty('')
def __init__(self, **kwargs):
super(SerialPortButton, self).__init__(**kwargs)
self.text = '/dev/ttyACM0'
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!'
class SerialPortDropdown(DropDown):
__events__ = ('on_serial_port_changed',)
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()))
self.on_dataSources(temp_log.list_serial_ports())
def on_dataSources(self, values):
children = self.children[:]
values_used = []
current_value = getattr(self.parent, 'text', None)
while children: while children:
child = children.pop() child = children.pop()
children.extend(child.children) children.extend(child.children)
if child is self: text = getattr(child, 'text', None)
if text and text in values:
values_used.append(text)
Logger.debug('Child ' + str(child) + ' is used')
continue continue
#Logger.debug(str(child)) if text and text not in values:
try: Logger.debug('Child ' + str(child) + ' is now unused, to be removed')
child.on_lastTime(instance, value) self.remove_widget(child)
#Logger.debug('Called on_lastTime for child: ' + str(child)) self.data = ''
except AttributeError, e: self.dispatch('on_serial_port_changed', '')
pass # add in new children
except Exception, e: new_values = set(values) - set(values_used)
Logger.exception(str(e)) 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)
btn.bind(on_release=lambda btn: self.select(btn.text))
self.add_widget(btn)
def select(self, value):
super(SerialPortDropdown, self).select(value)
Logger.debug('SerialPortDropdown: ' + 'selected with args ' + str(value))
self.data = value
self.dispatch('on_serial_port_changed', value)
def on_serial_port_changed(self, *args):
pass
class MyLabel(Label): class MyLabel(Label):
def on_lastTemperature(self, instance, value):
def __init__(self, **kwargs):
super(MyLabel, self).__init__(**kwargs)
def on_temperature_change(self, value):
if not value:
value = 'n/a'
self.text = 'Current temperature: ' + str(value) self.text = 'Current temperature: ' + str(value)
class PlotWidget(Image): class PlotWidget(Image):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Image, self).__init__(**kwargs) super(PlotWidget, self).__init__(**kwargs)
self.data = numpy.array([], dtype=float); self.data = numpy.array([], dtype=float);
self.lastTime = 0 self.lastTime = 0
self.lastTemperature = 0 self.lastTemperature = 0
@ -118,9 +241,11 @@ class PlotWidget(Image):
self.plot_axes.set_ylabel('Temperature (deg C)') self.plot_axes.set_ylabel('Temperature (deg C)')
self.plot_axes.set_xlabel('Time') self.plot_axes.set_xlabel('Time')
self.plot_axes.set_title('Recorded Temperature') self.plot_axes.set_title('Recorded Temperature')
self.image_data = None
self._image_raw_data = None
def update(self): def do_update(self):
if not self.data.any(): if not self.data.any():
return return
#Logger.debug('self.data: ' + str(self.data)) #Logger.debug('self.data: ' + str(self.data))
@ -136,15 +261,30 @@ class PlotWidget(Image):
image_data = StringIO.StringIO() image_data = StringIO.StringIO()
self.figure.savefig(image_data, format = 'png') self.figure.savefig(image_data, format = 'png')
image_data.seek(0) image_data.seek(0)
self.texture = ImageLoaderPygame(image_data, nocache = True).texture self._image_raw_data = 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:
im = pygame.image.load(image_data)
except:
Logger.warning('Image: Unable to load image from data')
raise
fmt = ''
if im.get_bytesize() == 3:
fmt = 'rgb'
elif im.get_bytesize() == 4:
fmt = 'rgba'
data = pygame.image.tostring(im, fmt.upper())
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()
def on_lastTemperature(self, instance, value): def on_lastTemperature(self, value):
self.lastTemperature = value self.lastTemperature = value
self.update_data()
def on_lastTime(self, instance, value): def on_lastTime(self, value):
self.lastTime = value self.lastTime = value
self.update_data() self.update_data()
@ -159,9 +299,7 @@ class PlotWidget(Image):
#Logger.debug('self.data: ' + str(self.data)) #Logger.debug('self.data: ' + str(self.data))
#Logger.debug('newpoint: ' + str(newpoint)) #Logger.debug('newpoint: ' + str(newpoint))
self.data = numpy.vstack((self.data, newpoint)) self.data = numpy.vstack((self.data, newpoint))
self.update() self.do_update()
#self.data = numpy.concatenate((self.data, newpoint), axis = 0)
#self.data = numpy.copy(self.data) # This will cause ObjectProperty to fire a change event
if __name__ == '__main__': if __name__ == '__main__':