temperature-logger-interface/main.py

308 lines
11 KiB
Python
Raw Normal View History

import kivy
from kivy.app import App
from kivy.clock import Clock
2014-12-11 03:38:40 +00:00
import kivy.core.image
from kivy.core.image import ImageData
from kivy.core.image.img_pil import ImageLoaderPIL
2014-12-11 03:38:40 +00:00
from kivy.core.image.img_pygame import ImageLoaderPygame
import kivy.graphics.texture
from kivy.graphics.texture import Texture
from kivy.logger import Logger
2014-12-11 03:38:40 +00:00
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
2014-12-11 03:38:40 +00:00
from kivy.uix.image import Image
from kivy.uix.label import Label
import kivy.lang
2014-12-11 03:38:40 +00:00
import numpy
import matplotlib
matplotlib.use('Agg')
import matplotlib.image
import matplotlib.pyplot
import pygame.image
import Queue
2014-12-11 03:38:40 +00:00
import StringIO
import temp_log
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()
#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
def on_stop(self):
global dataThreadCommandQueue
dataThreadCommandQueue.put_nowait('stop')
def on_start(self):
global dataThread
dataThread.start()
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())
2014-12-11 03:38:40 +00:00
lastTemperature = NumericProperty(-1000.)
lastTime = NumericProperty(-1.)
recordingState = StringProperty('')
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:
data = dataThreadDataQueue.get_nowait()
if data is not None:
2014-12-11 03:38:40 +00:00
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'])
except Queue.Empty:
pass
2014-12-11 03:38:40 +00:00
def on_lastTemperature(self, instance, value):
Logger.debug('lastTemperature has changed to: ' + str(value))
2014-12-11 03:38:40 +00:00
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):
2014-12-11 03:38:40 +00:00
children = self.children[:]
values_used = []
current_value = getattr(self.parent, 'text', None)
2014-12-11 03:38:40 +00:00
while children:
child = children.pop()
children.extend(child.children)
text = getattr(child, 'text', None)
if text and text in values:
values_used.append(text)
Logger.debug('Child ' + str(child) + ' is used')
2014-12-11 03:38:40 +00:00
continue
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
class MyLabel(Label):
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)
2014-12-11 03:38:40 +00:00
class PlotWidget(Image):
def __init__(self, **kwargs):
super(PlotWidget, self).__init__(**kwargs)
2014-12-11 03:38:40 +00:00
self.data = numpy.array([], dtype=float);
self.lastTime = 0
self.lastTemperature = 0
self.figure = matplotlib.pyplot.figure()
self.plot_axes = self.figure.add_subplot(1, 1, 1)
self.plot_axes.hold(False)
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
2014-12-11 03:38:40 +00:00
def do_update(self):
2014-12-11 03:38:40 +00:00
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
# 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()
2014-12-11 03:38:40 +00:00
def on_lastTemperature(self, value):
2014-12-11 03:38:40 +00:00
self.lastTemperature = value
def on_lastTime(self, value):
2014-12-11 03:38:40 +00:00
self.lastTime = value
self.update_data()
def update_data(self):
if self.lastTime == 0 or self.lastTemperature == 0:
return
newpoint = numpy.array([(self.lastTime, self.lastTemperature)], dtype=float, ndmin = 2)
if not self.data.any():
self.data = newpoint
return
#Logger.debug('self.data: ' + str(self.data))
#Logger.debug('newpoint: ' + str(newpoint))
self.data = numpy.vstack((self.data, newpoint))
self.do_update()
2014-12-11 03:38:40 +00:00
if __name__ == '__main__':
dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue])
MainApp().run()