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 import numpy import matplotlib matplotlib.use('Agg') import matplotlib.image import matplotlib.pyplot import pygame.image import Queue 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()) 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: 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 def on_lastTemperature(self, instance, value): Logger.debug('lastTemperature has changed to: ' + str(value)) 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) text = getattr(child, 'text', None) if text and text in values: values_used.append(text) Logger.debug('Child ' + str(child) + ' is used') 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) class PlotWidget(Image): def __init__(self, **kwargs): super(PlotWidget, self).__init__(**kwargs) 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 def do_update(self): 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() def on_lastTemperature(self, value): self.lastTemperature = value def on_lastTime(self, value): 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() if __name__ == '__main__': dataThread = threading.Thread(None, temp_log.threaded_reader, None, ['/dev/ttyACM0', dataThreadDataQueue, dataThreadCommandQueue]) MainApp().run()