temperature-logger-interface/main.py

347 lines
12 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
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
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
from kivy.uix.modalview import ModalView
from kivy.uix.popup import Popup
from kivy.uix.spinner import Spinner
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):
mainWindow = MainWindow()
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"""
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('')
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.modal_view = None
#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 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
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')
elif new_port in self.dataSources:
self.dataSource = new_port
else:
self.dataSource = ''
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()
def set_state(self, new_state):
self.state = new_state
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 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 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)
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))
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 clear_data(self):
self.data = numpy.array([], dtype=float);
self.texture = None
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
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()