diff --git a/Arduino_Monitor.py b/Arduino_Monitor.py new file mode 100644 index 0000000..ab356b1 --- /dev/null +++ b/Arduino_Monitor.py @@ -0,0 +1,78 @@ +""" +Listen to serial, return most recent numeric values +Lots of help from here: +http://stackoverflow.com/questions/1093598/pyserial-how-to-read-last-line-sent-from-serial-device +""" +from threading import Thread +import time +import serial +import struct + +last_received = '' +profile = [] + +PI_TS_MIN = 0 +PI_TS_MAX = 1 +PI_TL = 2 +PI_TP = 3 +PI_TIME_MAX = 4 + +PI_RAMP_UP_MIN = 5 +PI_RAMP_UP_MAX = 6 +PI_RAMP_DOWN_MIN = 7 +PI_RAMP_DOWN_MAX = 8 + +PI_TS_DURATION_MIN = 9 +PI_TS_DURATION_MAX = 10 +PI_TL_DURATION_MIN = 11 +PI_TL_DURATION_MAX = 12 +PI_TP_DURATION_MIN = 13 +PI_TP_DURATION_MAX = 14 + +def receiving(ser): + global last_received + buffer = '' + ser.write(chr(255)) + ser.flush() + profile = struct.unpack("hhhhhhhhhhhhhhh", ser.read(30)) + ser.flushInput() + while 1: + ser.write(chr(254)) + ser.flush() + last_received = ser.read(11) + print repr(last_received) + ser.flushInput() + + +class SerialData(object): + def __init__(self, init=50): + try: + self.ser = ser = serial.Serial( + port='/dev/ttyUSB0', + baudrate=9600, timeout=2) + except serial.serialutil.SerialException: + #no serial connection + self.ser = None + else: + Thread(target=receiving, args=(self.ser,)).start() + + def next(self): + if not self.ser: + return 100 #return anything so we can test when Arduino isn't connected + + try: + return int(struct.unpack("hhhhhb", last_received)[1]) + except Exception, e: + print e + return 0 + + + def __del__(self): + if self.ser: + self.ser.close() + +if __name__=='__main__': + s = SerialData() + for i in range(500): + time.sleep(1) + print s.next() diff --git a/libs/oven_control.cpp b/libs/oven_control.cpp index ae42812..bb7b231 100644 --- a/libs/oven_control.cpp +++ b/libs/oven_control.cpp @@ -11,8 +11,8 @@ Profile _profile; OvenCtl::OvenCtl() { time = 0; - temperature = 0; - last_temperature = 0; + temperature = 1; + last_temperature = 1; actual_dt = 0; // timestamps of event beginnings/ends Ts_time_start = 0; diff --git a/libs/profile.cpp b/libs/profile.cpp index aee0968..1db9e35 100644 --- a/libs/profile.cpp +++ b/libs/profile.cpp @@ -43,7 +43,7 @@ Profile::Profile() : 180, 60, 150, - 1, + 20, 40}), config_index(0), config_state(0), diff --git a/plot.py b/plot.py new file mode 100644 index 0000000..6748b58 --- /dev/null +++ b/plot.py @@ -0,0 +1,378 @@ +# -*- coding: utf-8 -*- + +""" +GP: +Changed datasource, title, and refresh interval to use +as a poor man's Arduino oscilliscope. + +This demo demonstrates how to draw a dynamic mpl (matplotlib) +plot in a wxPython application. + +It allows "live" plotting as well as manual zooming to specific +regions. + +Both X and Y axes allow "auto" or "manual" settings. For Y, auto +mode sets the scaling of the graph to see all the data points. +For X, auto mode makes the graph "follow" the data. Set it X min +to manual 0 to always see the whole data from the beginning. + +Note: press Enter in the 'manual' text box to make a new value +affect the plot. + +Eli Bendersky (eliben@gmail.com) +License: this code is in the public domain +Last modified: 31.07.2008 +""" + + +import os +import pprint +import random +import sys +import wx + +REFRESH_INTERVAL_MS = 1000 + +# The recommended way to use wx with mpl is with the WXAgg +# backend. +# +import matplotlib +matplotlib.use('WXAgg') +from matplotlib.figure import Figure +from matplotlib.backends.backend_wxagg import \ + FigureCanvasWxAgg as FigCanvas, \ + NavigationToolbar2WxAgg as NavigationToolbar +from matplotlib.path import Path +import matplotlib.patches as patches + +import numpy as np +import pylab +#Data comes from here +from Arduino_Monitor import SerialData as DataGen + + +class BoundControlBox(wx.Panel): + """ A static box with a couple of radio buttons and a text + box. Allows to switch between an automatic mode and a + manual mode with an associated value. + """ + def __init__(self, parent, ID, label, initval): + wx.Panel.__init__(self, parent, ID) + + self.value = initval + + box = wx.StaticBox(self, -1, label) + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + + self.radio_auto = wx.RadioButton(self, -1, + label="Auto", style=wx.RB_GROUP) + self.radio_manual = wx.RadioButton(self, -1, + label="Manual") + self.manual_text = wx.TextCtrl(self, -1, + size=(35,-1), + value=str(initval), + style=wx.TE_PROCESS_ENTER) + + self.Bind(wx.EVT_UPDATE_UI, self.on_update_manual_text, self.manual_text) + self.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter, self.manual_text) + + manual_box = wx.BoxSizer(wx.HORIZONTAL) + manual_box.Add(self.radio_manual, flag=wx.ALIGN_CENTER_VERTICAL) + manual_box.Add(self.manual_text, flag=wx.ALIGN_CENTER_VERTICAL) + + sizer.Add(self.radio_auto, 0, wx.ALL, 10) + sizer.Add(manual_box, 0, wx.ALL, 10) + self.radio_auto.SetValue(False); + self.radio_manual.SetValue(True); + + self.SetSizer(sizer) + sizer.Fit(self) + + def on_update_manual_text(self, event): + self.manual_text.Enable(self.radio_manual.GetValue()) + + def on_text_enter(self, event): + self.value = self.manual_text.GetValue() + + def is_auto(self): + return self.radio_auto.GetValue() + + def manual_value(self): + return self.value + + +class GraphFrame(wx.Frame): + """ The main frame of the application + """ + title = 'Demo: dynamic matplotlib graph' + + def __init__(self): + wx.Frame.__init__(self, None, -1, self.title) + + self.datagen = DataGen() + self.data = [self.datagen.next()] + self.paused = False + + self.create_menu() + self.create_status_bar() + self.create_main_panel() + + self.redraw_timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer) + self.redraw_timer.Start(REFRESH_INTERVAL_MS) + + def create_menu(self): + self.menubar = wx.MenuBar() + + menu_file = wx.Menu() + m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file") + self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt) + menu_file.AppendSeparator() + m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit") + self.Bind(wx.EVT_MENU, self.on_exit, m_exit) + + self.menubar.Append(menu_file, "&File") + self.SetMenuBar(self.menubar) + + def create_main_panel(self): + self.panel = wx.Panel(self) + + self.init_plot() + self.canvas = FigCanvas(self.panel, -1, self.fig) + + self.xmin_control = BoundControlBox(self.panel, -1, "X min", 0) + self.xmax_control = BoundControlBox(self.panel, -1, "X max", 250) + self.ymin_control = BoundControlBox(self.panel, -1, "Y min", 0) + self.ymax_control = BoundControlBox(self.panel, -1, "Y max", 280) + + self.pause_button = wx.Button(self.panel, -1, "Pause") + self.Bind(wx.EVT_BUTTON, self.on_pause_button, self.pause_button) + self.Bind(wx.EVT_UPDATE_UI, self.on_update_pause_button, self.pause_button) + + self.cb_grid = wx.CheckBox(self.panel, -1, + "Show Grid", + style=wx.ALIGN_RIGHT) + self.Bind(wx.EVT_CHECKBOX, self.on_cb_grid, self.cb_grid) + self.cb_grid.SetValue(True) + + self.cb_xlab = wx.CheckBox(self.panel, -1, + "Show X labels", + style=wx.ALIGN_RIGHT) + self.Bind(wx.EVT_CHECKBOX, self.on_cb_xlab, self.cb_xlab) + self.cb_xlab.SetValue(True) + + self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) + self.hbox1.Add(self.pause_button, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) + self.hbox1.AddSpacer(5) + self.hbox1.Add(self.cb_grid, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) + self.hbox1.AddSpacer(5) + self.hbox1.Add(self.cb_xlab, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) + + self.hbox2 = wx.BoxSizer(wx.HORIZONTAL) + self.hbox2.Add(self.xmin_control, border=5, flag=wx.ALL) + self.hbox2.Add(self.xmax_control, border=5, flag=wx.ALL) + self.hbox2.AddSpacer(24) + self.hbox2.Add(self.ymin_control, border=5, flag=wx.ALL) + self.hbox2.Add(self.ymax_control, border=5, flag=wx.ALL) + + self.vbox = wx.BoxSizer(wx.VERTICAL) + self.vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW) + self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP) + self.vbox.Add(self.hbox2, 0, flag=wx.ALIGN_LEFT | wx.TOP) + + self.panel.SetSizer(self.vbox) + self.vbox.Fit(self) + + def create_status_bar(self): + self.statusbar = self.CreateStatusBar() + + def init_plot(self): + self.dpi = 100 + self.fig = Figure((3.0, 3.0), dpi=self.dpi) + + self.axes = self.fig.add_subplot(111) + self.axes.set_axis_bgcolor('black') + self.axes.set_title(u'Reflow Temperature', size=12) + self.axes.set_xlabel(u'Time / seconds', size=12) + self.axes.set_ylabel(u'Temperature / °C', size=12) + + pylab.setp(self.axes.get_xticklabels(), fontsize=8) + pylab.setp(self.axes.get_yticklabels(), fontsize=8) + + # plot the data as a line series, and save the reference + # to the plotted line series + # + + ts_min_x_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + ts_min_y_min = ts_min + + ts_max_x_min = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MIN] + ts_max_y_min = ts_max + + ts_max_x_max = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MAX] + ts_max_y_max = ts_max + + ts_min_x_max = ts_max_x_max - (ts_max_y_max - ts_min_y) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]) + ts_min_y_max = ts_min + + tl_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TL] - Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + tl_x_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + + tl_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TL] - Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + tl_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + + tl_x_max = tl_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TL_DURATION_MIN] + tl_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + + verts = [ + [ 0.0, 0.0], + [ 75.0, 150.0], + [100.0, 200.0], + [108.5, 217.0], + [130.0, 260.0], + [170.0, 260.0], + [300.0, 0.0], + [ 0.0, 0.0]] + + codes = [Path.MOVETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.LINETO, + Path.CLOSEPOLY] + + self.plot_data = self.axes.plot( + self.data, + linewidth=1, + color=(1, 1, 0), + )[0] + + + path = Path(verts, codes) + self.patch = patches.PathPatch(path, edgecolor="red", facecolor='orange', lw=2) + self.axes.add_patch(self.patch) + + def draw_plot(self): + """ Redraws the plot + """ + # when xmin is on auto, it "follows" xmax to produce a + # sliding window effect. therefore, xmin is assigned after + # xmax. + # + if self.xmax_control.is_auto(): + xmax = len(self.data) if len(self.data) > 50 else 50 + else: + xmax = int(self.xmax_control.manual_value()) + + if self.xmin_control.is_auto(): + xmin = xmax - 50 + else: + xmin = int(self.xmin_control.manual_value()) + + #xmax = 480 + + # for ymin and ymax, find the minimal and maximal values + # in the data set and add a mininal margin. + # + # note that it's easy to change this scheme to the + # minimal/maximal value in the current display, and not + # the whole data set. + # + if self.ymin_control.is_auto(): + ymin = round(min(self.data), 0) - 1 + else: + ymin = int(self.ymin_control.manual_value()) + + if self.ymax_control.is_auto(): + ymax = round(max(self.data), 0) + 1 + else: + ymax = int(self.ymax_control.manual_value()) + + #ymax = 300 + + self.axes.set_xbound(lower=xmin, upper=xmax) + self.axes.set_ybound(lower=ymin, upper=ymax) + + # anecdote: axes.grid assumes b=True if any other flag is + # given even if b is set to False. + # so just passing the flag into the first statement won't + # work. + # + if self.cb_grid.IsChecked(): + self.axes.grid(True, color='gray') + else: + self.axes.grid(False) + + # Using setp here is convenient, because get_xticklabels + # returns a list over which one needs to explicitly + # iterate, and setp already handles this. + # + pylab.setp(self.axes.get_xticklabels(), + visible=self.cb_xlab.IsChecked()) + + self.plot_data.set_xdata(np.arange(len(self.data))) + self.plot_data.set_ydata(np.array(self.data)) + + self.canvas.draw() + + def on_pause_button(self, event): + self.paused = not self.paused + + def on_update_pause_button(self, event): + label = "Resume" if self.paused else "Pause" + self.pause_button.SetLabel(label) + + def on_cb_grid(self, event): + self.draw_plot() + + def on_cb_xlab(self, event): + self.draw_plot() + + def on_save_plot(self, event): + file_choices = "PNG (*.png)|*.png" + + dlg = wx.FileDialog( + self, + message="Save plot as...", + defaultDir=os.getcwd(), + defaultFile="plot.png", + wildcard=file_choices, + style=wx.SAVE) + + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + self.canvas.print_figure(path, dpi=self.dpi) + self.flash_status_message("Saved to %s" % path) + + def on_redraw_timer(self, event): + # if paused do not add data, but still redraw the plot + # (to respond to scale modifications, grid change, etc.) + # + if not self.paused: + self.data.append(self.datagen.next()) + + self.draw_plot() + + def on_exit(self, event): + self.Destroy() + + def flash_status_message(self, msg, flash_len_ms=1500): + self.statusbar.SetStatusText(msg) + self.timeroff = wx.Timer(self) + self.Bind( + wx.EVT_TIMER, + self.on_flash_status_off, + self.timeroff) + self.timeroff.Start(flash_len_ms, oneShot=True) + + def on_flash_status_off(self, event): + self.statusbar.SetStatusText('') + + +if __name__ == '__main__': + app = wx.PySimpleApp() + app.frame = GraphFrame() + app.frame.Show() + app.MainLoop()