diff --git a/main.kv b/main.kv index fba4656..5fe7dd5 100644 --- a/main.kv +++ b/main.kv @@ -1,23 +1,43 @@ -: - text: 'Current Temperature: n/a' +#: +# text: 'Current Temperature: n/a' -: - allow_stretch: True +#: +# allow_stretch: True + +#: + +#: : 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: - size_hint_y: 0.1 - orientation: 'horizontal' - MyLabel: - id: 'current_temperature' - Button: - text: 'Start/Stop' - Label: - text: 'No File Chosen' - Label: - text: 'Data Source' - PlotWidget: - id: 'mainplot' \ No newline at end of file + id: 'top_menu' + size_hint_y: 0.1 + pos_hint: {'top':1} + orientation: 'horizontal' + MyLabel: + id: current_temperature + Button: + text: 'Start/Stop' + Label: + text: 'No File Chosen' + 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: + size_hint_y: 0.9 + id: mainplot + +MainWindow: \ No newline at end of file diff --git a/main.py b/main.py index 9ac22b2..161ef67 100644 --- a/main.py +++ b/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: - pass - except Exception, e: - Logger.exception(str(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 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__':