Better responses for serial port changes, temperature handling. Fixed PlotWidget image loading for kivy 1.9
This commit is contained in:
parent
bf5a6c6920
commit
5e6a2d3f09
40
main.kv
40
main.kv
|
@ -1,23 +1,43 @@
|
|||
<MyLabel@Label>:
|
||||
text: 'Current Temperature: n/a'
|
||||
#<MyLabel@Label>:
|
||||
# text: 'Current Temperature: n/a'
|
||||
|
||||
<PlotWidget@Image>:
|
||||
allow_stretch: True
|
||||
#<PlotWidget@Image>:
|
||||
# allow_stretch: True
|
||||
|
||||
#<SerialPortDropdown@DropDown>:
|
||||
|
||||
#<SerialPortButton@Button>:
|
||||
|
||||
<MainWindow>:
|
||||
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:
|
||||
orientation: 'vertical'
|
||||
BoxLayout:
|
||||
id: 'top_menu'
|
||||
size_hint_y: 0.1
|
||||
pos_hint: {'top':1}
|
||||
orientation: 'horizontal'
|
||||
MyLabel:
|
||||
id: 'current_temperature'
|
||||
id: current_temperature
|
||||
Button:
|
||||
text: 'Start/Stop'
|
||||
Label:
|
||||
text: 'No File Chosen'
|
||||
Label:
|
||||
text: 'Data Source'
|
||||
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:
|
||||
id: 'mainplot'
|
||||
size_hint_y: 0.9
|
||||
id: mainplot
|
||||
|
||||
MainWindow:
|
206
main.py
206
main.py
|
@ -2,12 +2,18 @@ import kivy
|
|||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
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
|
||||
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.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.gridlayout import GridLayout
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.label import Label
|
||||
import kivy.lang
|
||||
|
@ -16,6 +22,7 @@ import matplotlib
|
|||
matplotlib.use('Agg')
|
||||
import matplotlib.image
|
||||
import matplotlib.pyplot
|
||||
import pygame.image
|
||||
import Queue
|
||||
import StringIO
|
||||
import temp_log
|
||||
|
@ -24,12 +31,28 @@ import threading
|
|||
dataThread = None
|
||||
dataThreadDataQueue = Queue.Queue()
|
||||
dataThreadCommandQueue = Queue.Queue()
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
|
@ -45,11 +68,21 @@ class MainApp(App):
|
|||
|
||||
class MainWindow(FloatLayout):
|
||||
"""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.)
|
||||
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
|
||||
# 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:
|
||||
|
@ -66,49 +99,139 @@ class MainWindow(FloatLayout):
|
|||
|
||||
def on_lastTemperature(self, instance, 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):
|
||||
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[:]
|
||||
|
||||
|
||||
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:
|
||||
child = children.pop()
|
||||
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
|
||||
#Logger.debug(str(child))
|
||||
try:
|
||||
child.on_lastTime(instance, value)
|
||||
#Logger.debug('Called on_lastTime for child: ' + str(child))
|
||||
except AttributeError, e:
|
||||
if text and text not in values:
|
||||
Logger.debug('Child ' + str(child) + ' is now unused, to be removed')
|
||||
self.remove_widget(child)
|
||||
self.data = ''
|
||||
self.dispatch('on_serial_port_changed', '')
|
||||
# add in new children
|
||||
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)
|
||||
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
|
||||
except Exception, e:
|
||||
Logger.exception(str(e))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class PlotWidget(Image):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Image, self).__init__(**kwargs)
|
||||
super(PlotWidget, self).__init__(**kwargs)
|
||||
self.data = numpy.array([], dtype=float);
|
||||
self.lastTime = 0
|
||||
self.lastTemperature = 0
|
||||
|
@ -118,9 +241,11 @@ class PlotWidget(Image):
|
|||
self.plot_axes.set_ylabel('Temperature (deg C)')
|
||||
self.plot_axes.set_xlabel('Time')
|
||||
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():
|
||||
return
|
||||
#Logger.debug('self.data: ' + str(self.data))
|
||||
|
@ -136,15 +261,30 @@ class PlotWidget(Image):
|
|||
image_data = StringIO.StringIO()
|
||||
self.figure.savefig(image_data, format = 'png')
|
||||
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.update_data()
|
||||
|
||||
|
||||
def on_lastTime(self, instance, value):
|
||||
def on_lastTime(self, value):
|
||||
self.lastTime = value
|
||||
self.update_data()
|
||||
|
||||
|
@ -159,9 +299,7 @@ class PlotWidget(Image):
|
|||
#Logger.debug('self.data: ' + str(self.data))
|
||||
#Logger.debug('newpoint: ' + str(newpoint))
|
||||
self.data = numpy.vstack((self.data, newpoint))
|
||||
self.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
|
||||
self.do_update()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in New Issue