diff --git a/reflowctl/control_widgets.py b/reflowctl/control_widgets.py new file mode 100644 index 0000000..956b9e2 --- /dev/null +++ b/reflowctl/control_widgets.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from PyQt4 import QtGui, QtCore + +class AddRemoveWidget(QtGui.QWidget): + def __init__(self, parent): + #super(AddRemoveWidget, self).__init__(parent) + QtGui.QWidget.__init__(self, parent) + self.add_button = QtGui.QPushButton(QtGui.QIcon.fromTheme("list-add"), "") + self.remove_button = QtGui.QPushButton(QtGui.QIcon.fromTheme("list-remove"), "") + + sh = "QPushButton:disabled { background-color: #555555; }" + self.remove_button.setStyleSheet(sh) + self.add_button.setStyleSheet(sh) + + self._layout = QtGui.QVBoxLayout(self) + + self._layout.addWidget(self.add_button) + self._layout.addWidget(self.remove_button) + self._layout.addStretch(4) diff --git a/reflowctl/edge_widget.py b/reflowctl/edge_widget.py new file mode 100644 index 0000000..2674459 --- /dev/null +++ b/reflowctl/edge_widget.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +from PyQt4 import QtGui, QtCore + + +class Edge(QtCore.QObject): + def __init__(self, from_tl, to_tl, duration=None, rate=None): + super(Edge, self).__init__(None) + self.from_tl = from_tl + self.to_tl = to_tl + self.duration = duration + self.rate = rate + + def __repr__(self): + return "Edge(%r, %r, %r, %r)" % (self.from_tl, self.to_tl, self.duration, self.rate) + + +class EdgeModel(QtCore.QAbstractTableModel): + edge_changed = QtCore.pyqtSignal() + + def __init__(self, parent): + super(EdgeModel, self).__init__(parent) + self.edges = list() + self.headerdata = [ + u"From Temperature (°C)", + u"To Temperature (°C)", + u"Duration (s)", + u"Rate (°C/s)"] + + def headerData(self, col, orientation, role): + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return QtCore.QVariant(self.headerdata[col]) + return QtCore.QVariant() + + def rowCount(self, parent): + return len(self.edges) + + def columnCount(self, parent): + return 4 + + def data(self, index, role): + if not index.isValid(): + return QtCore.QVariant() + + col = index.column() + if role == QtCore.Qt.DisplayRole: + if col == 0: + return QtCore.QVariant(self.edges[index.row()].from_tl.name) + elif col == 1: + return QtCore.QVariant(self.edges[index.row()].to_tl.name) + elif col == 2: + return QtCore.QVariant(self.edges[index.row()].duration) + elif col == 3: + return QtCore.QVariant(self.edges[index.row()].rate) + + if index.column() < 2 and role == QtCore.Qt.DecorationRole: + p = QtGui.QPixmap(10,10) + if col == 0: + p.fill(self.edges[index.row()].from_tl.color) + elif col == 1: + p.fill(self.edges[index.row()].to_tl.color) + return p + + return QtCore.QVariant() + + def flags(self, index): + if not index.isValid(): + return 0 + return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable) + + def setData(self, index, variant, role): + if index.isValid() and role == QtCore.Qt.EditRole: + col = index.column() + if col == 2: + self.edges[index.row()].duration = variant.toFloat()[0] + elif col == 3: + self.edges[index.row()].rate = variant.toFloat()[0] + self.edge_changed.emit() + return True + return False + + def remove_edge(self, index): + tmp = self.edges[index] + del self.edges[index] + self.reset() + self.edge_changed.emit() + return tmp + + def add_edge(self, index, edge): + self.edges.insert(index.row() + 1, edge) + self.reset() + self.edge_changed.emit() + + def setTempLevels(self, edges): + assert isinstance(edges, list) + self.edges = edges + self.reset() + + def clear(self): + self.edges = list() + self.reset() + + def get_edge(self, ix): + return self.edges[ix] + +class ConstraintWidget(QtGui.QWidget): + edge_changed = QtCore.pyqtSignal() + def __init__(self, name): + super(ConstraintWidget, self).__init__() + self.name = name + + self.edge_model = EdgeModel(self) # temp_level selection pool + + self.edge_view = QtGui.QTableView(self) + self.edge_view.setModel(self.edge_model) + self.edge_view.verticalHeader().setVisible(False) + self.edge_view.resizeColumnsToContents() + self.edge_view.horizontalHeader().setStretchLastSection(True) + self.edge_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + + + h = QtGui.QHBoxLayout(self) + h.addWidget(self.edge_view) + + def edge_picked(self, ix): + print self.edge_picked + self.edge_view.setCurrentIndex(self.edge_model.index(ix, 0)) + + def setData(self, solder): + print self.setData + self.solder = solder + self.edge_model.setTempLevels(solder.edges) + self.edge_view.setCurrentIndex(self.edge_model.index(0,0)) + + self.connect( + self.edge_model, + QtCore.SIGNAL("edge_changed()"), + self._edge_changed) + + def _edge_changed(self): + print self.temp_level_added + self.edge_changed.emit() + + def temp_level_added(self, old_tl, new_tl): + print self.temp_level_added, len(self.edge_model.edges) + new_edge = None + ix = 0 + for ix, edge in enumerate(self.edge_model.edges): + print ix, edge + if edge.from_tl == old_tl: + duration = None if edge.duration is None else edge.duration / 2 + new_edge = Edge(new_tl, edge.to_tl, duration, edge.rate) + edge.duration = duration + edge.to_tl = new_tl + break + self.edge_model.edges.insert(ix+1, new_edge) + self.edge_model.reset() + print "end", self.temp_level_added + diff --git a/reflowctl/plot.py b/reflowctl/plot.py deleted file mode 100644 index fd2b910..0000000 --- a/reflowctl/plot.py +++ /dev/null @@ -1,449 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -import pprint -import random -import sys -import wx - -REFRESH_INTERVAL_MS = 1000 - -import matplotlib -matplotlib.use('WXAgg') -import matplotlib.lines -from matplotlib.figure import Figure -from matplotlib.pyplot import legend -from matplotlib.backends.backend_wxagg import \ - FigureCanvasWxAgg as FigCanvas, \ - NavigationToolbar2WxAgg as NavigationToolbar -from matplotlib.path import Path -import matplotlib.patches as patches -import wx.lib.buttons as buttons - -import numpy as np -import pylab - -from Arduino_Monitor import SerialData as DataGen -import Arduino_Monitor - - -class GraphFrame(wx.Frame): - """ The main frame of the application - """ - title = 'reflowctl gui' - - def __init__(self): - wx.Frame.__init__(self, None, -1, self.title) - - self.datagen = DataGen() - self.data = [self.datagen.next()] - self.started = False - - self.profile = [] - self.state = [] - self.count = 0 - - 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.hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.init_profile() - self.init_log() - self.init_oven_status() - self.init_plot() - self.canvas = FigCanvas(self.panel, -1, self.fig) - - self.recv_config_button = wx.Button(self.panel, -1, "Receive Config") - self.Bind(wx.EVT_BUTTON, self.on_recv_config_button, self.recv_config_button) - - self.send_button = wx.Button(self.panel, -1, "Send Config") - self.Bind(wx.EVT_BUTTON, self.on_send_button, self.send_button) - - self.start_button = buttons.GenToggleButton(self.panel, -1, "Start") - self.Bind(wx.EVT_BUTTON, self.on_start_button, self.start_button) - - #self.on_bitmap = wx.Image('burn.png', wx.BITMAP_TYPE_PNG).Scale(32, 32, wx.IMAGE_QUALITY_HIGH).ConvertToBitmap() - #self.off_bitmap = wx.Image('unburn.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap() - - - self.ctrls = wx.BoxSizer(wx.VERTICAL) - self.ctrls.Add(self.recv_config_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) - self.ctrls.Add(self.send_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) - self.ctrls.Add(self.start_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) - self.hbox1.Add(self.ctrls, border=5, flag=wx.ALL | wx.ALIGN_TOP) - - self.vbox = wx.BoxSizer(wx.VERTICAL) - self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.ALL) - 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.panel.SetSizer(self.vbox) - self.vbox.Fit(self) - - def profile_spin_changed(self, event): - print dir(event) - - - def add_profile_item(self, title, sizer, min_=1, max_=250): - - mc = 8 - - item = wx.SpinCtrl(self.panel, -1, "", (30, 50)) - item.SetRange(min_, max_) - item.SetValue(Arduino_Monitor.profile[self.count]) - - self.Bind(wx.EVT_SPIN, self.profile_spin_changed, item) - - sizer.Add(wx.StaticText(self.panel, -1, title), (self.count, 0)) - sizer.Add(item, (self.count, 1)) - - self.count += 1 - self.profile.append(item) - - def init_profile(self): - self.preheat_sizer = wx.GridBagSizer(5, 5) - self.rampup_sizer = wx.GridBagSizer(5, 5) - self.peak_sizer = wx.GridBagSizer(5, 5) - self.rampdown_sizer = wx.GridBagSizer(5, 5) - - self.add_profile_item("Ts_min (°C)", self.preheat_sizer, 0, 300) - self.add_profile_item("Ts_max (°C)", self.preheat_sizer, 0, 300) - self.add_profile_item("Ts duration min (s)", self.preheat_sizer, 0, 300) - self.add_profile_item("Ts duration max (s)", self.preheat_sizer, 0, 300) - - self.add_profile_item("ts ramp up min (°C/s)", self.preheat_sizer, 1, 100) - self.add_profile_item("ts ramp up max (°C/s)", self.preheat_sizer, 1, 100) - self.add_profile_item("tp ramp up min (°C/s)", self.peak_sizer, 1, 100) - self.add_profile_item("tp ramp up max (°C/s)", self.peak_sizer1, 100) - - self.add_profile_item("Tl duration min (s)", self.rampup_sizer, 0, 300) - self.add_profile_item("Tl duration max (s)", self.rampup_sizer, 0, 300) - - self.add_profile_item("Tp (°C)", self.peak_sizer, 0, 300) - self.add_profile_item("Tp duration min (s)", self.peak_sizer, 0, 300) - self.add_profile_item("Tp duration max (s)", self.peak_sizer, 0, 300) - - self.add_profile_item("ramp down min (°C/s)", self.rampdown_sizer, -100, 0) - self.add_profile_item("ramp down max (°C/s)", self.rampdown_sizer, -100, 0) - - self.add_profile_item("time max (s)", 0, 800) - - self.box = wx.StaticBox(self.panel, -1, "Profile Settings") - self.bsizer = wx.StaticBoxSizer(self.box, wx.VERTICAL) - self.bsizer.Add(self.profile_sizer, 0, flag=wx.ALL, border=5) - self.hbox1.Add(self.bsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) - - def init_oven_status(self): - self.oven_status_sizer = wx.GridBagSizer(5, 5) - - #set_min = 0; - #set_max = 0; - #set_dt_min = 0; - #set_dt_max = 0; - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Connected"), (0, 0)) - self.oven_connected = wx.StaticText(self.panel, -1, str(self.datagen.connected())) - self.oven_status_sizer.Add(self.oven_connected, (0, 1)) - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Temperature"), (1, 0)) - self.temperature = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[1])) - self.oven_status_sizer.Add(self.temperature, (1, 1)) - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Time"), (2, 0)) - self.time = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[0])) - self.oven_status_sizer.Add(self.time, (2, 1)) - - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "State"), (3, 0)) - self.pstate = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[3])) - self.oven_status_sizer.Add(self.pstate, (3, 1)) - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Error"), (4, 0)) - self.perror = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[4])) - self.oven_status_sizer.Add(self.perror, (4, 1)) - - self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Heating"), (5, 0)) - self.is_oven_heating = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[5])) - self.oven_status_sizer.Add(self.is_oven_heating, (5, 1)) - - self.obox = wx.StaticBox(self.panel, -1, "Oven status") - self.osizer = wx.StaticBoxSizer(self.obox, wx.VERTICAL) - self.osizer.Add(self.oven_status_sizer, 0, flag=wx.ALL, border=5) - self.hbox1.Add(self.osizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) - - - def init_log(self): - self.log_sizer = wx.GridBagSizer(5, 5) - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_start"), (0, 0)) - self.ts_time_start = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.ts_time_start, (0, 1)) - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_end"), (1, 0)) - self.ts_time_end = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.ts_time_end, (1, 1)) - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_start"), (2, 0)) - self.tl_time_start = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.tl_time_start, (2, 1)) - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_end"), (3, 0)) - self.tl_time_end = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.tl_time_end, (3, 1)) - - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_start"), (4, 0)) - self.tp_time_start = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.tp_time_start, (4, 1)) - - self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_end"), (5, 0)) - self.tp_time_end = wx.TextCtrl(self.panel, -1) - self.log_sizer.Add(self.tp_time_end, (5, 1)) - - self.lbox = wx.StaticBox(self.panel, -1, "Profile Log") - self.lsizer = wx.StaticBoxSizer(self.lbox, wx.VERTICAL) - self.lsizer.Add(self.log_sizer, 0, flag=wx.ALL, border=5) - self.hbox1.Add(self.lsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) - - def create_status_bar(self): - self.statusbar = self.CreateStatusBar() - - def init_plot(self): - self.dpi = 100 - self.fig = Figure((4.0, 4.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) - - # no 1 - 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 = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] - - # no 2 - ts_max_x_min = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MIN] - ts_max_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] - - # no t1 - ts_max_x_max = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MAX] - ts_max_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] - - # no t2 - ts_min_x_max = ts_max_x_max - (ts_max_y_max - ts_min_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - ts_min_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] - - # no 10 - t0_x_max = ts_min_x_max - (ts_max_x_max / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]) - t0_y_max = 0 - - # no 4 - tp_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - ts_max_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - tp_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - - # no 5 - tp_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MAX] - tp_y_max = tp_y_min - - # no 8 - tp5_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MIN] - tp5_y_max = tp_y_max - 5 - - # no 9 - tp5_x_min = ts_max_x_max + (tp5_y_max - ts_max_y_max) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - tp5_y_min = tp5_y_max - - # no 6 - end_x_max = tp_x_max + tp_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MIN]) - end_y_max = 0 - - self.xmax = end_x_max + 20 - self.ymax = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] + 20 - - # no 7 - end_x_min = tp5_x_max + tp5_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MAX]) - end_y_min = 0 - - tsmin = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] - tsmax = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] - tl = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] - tp = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - self.ts_line_min = matplotlib.lines.Line2D([0, self.xmax], [tsmin, tsmin], - transform=self.axes.transData, figure=self.fig, color='green') - self.ts_line_max = matplotlib.lines.Line2D([0, self.xmax], [tsmax, tsmax], - transform=self.axes.transData, figure=self.fig, label="Ts_max", color='lightgreen') - self.tl_line = matplotlib.lines.Line2D([0, self.xmax], [tl, tl], - transform=self.axes.transData, figure=self.fig, label="Tl", color='yellow') - self.tp_line = matplotlib.lines.Line2D([0, self.xmax], [tp, tp], - transform=self.axes.transData, figure=self.fig, label="Tp", color='blue') - - self.ts_line_min.set_label("Ts_min") - self.ts_line_min.set_label("Ts_max") - self.tl_line.set_label("Tl") - self.tp_line.set_label("Tp") - self.fig.lines.extend([self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line]) - - verts = [ - [0.0, 0.0], - [ts_min_x_min, ts_min_y_min], - [ts_max_x_min, ts_max_y_min], - [ts_max_x_max, ts_max_y_max], - [ts_min_x_max, ts_min_y_max], - #[tp_x_min, tp_y_min], - #[tp_x_max, tp_y_max], - #[end_x_max, end_y_max], - #[end_x_min, end_y_min], - #[tp5_x_max, tp5_y_max], - #[tp5_x_min, tp5_y_min], - [t0_x_max, t0_y_max], - [0.0, 0.0]] - - codes = [ - Path.MOVETO, - Path.LINETO, - Path.LINETO, - Path.LINETO, - 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] - - print "verts", verts - - - path = Path(verts, codes) - self.patch = patches.PathPatch(path, edgecolor="red", facecolor='orange', lw=2) - self.axes.add_patch(self.patch) - self.axes.legend( (self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line), ('Ts_min', 'Ts_max', 'Tl', 'Tp'), loc=2) - - def draw_plot(self): - """ Redraws the plot - """ - - self.axes.set_xbound(lower=0, upper=self.xmax) - self.axes.set_ybound(lower=0, upper=self.ymax) - - self.axes.grid(True, color='gray') - - pylab.setp(self.axes.get_xticklabels(), visible=True) - - self.plot_data.set_xdata(np.arange(len(self.data))) - self.plot_data.set_ydata(np.array(self.data)) - - self.canvas.draw() - - def update_config(self): - for ix, i in enumerate(self.profile): - i.SetValue(str(Arduino_Monitor.profile[i])) - - def update_state(self): - if Arduino_Monitor.status[3] > 0: - self.started = True - - self.time.SetValue(str(Arduino_Monitor.status[0])) - self.temperature.SetValue(str(Arduino_Monitor.status[1])) - self.pstate.SetValue(str(Arduino_Monitor.status[3])) - self.perror.SetValue(str(Arduino_Monitor.status[4])) - self.is_oven_heating.SetValue(str(Arduino_Monitor.status[5])) - - - def on_start_button(self, event): - self.started = self.datagen.send_start() - self.recv_config_button.Disable() - self.send_button.Disable() - self.profile = [] - for i in range(30): - self.profile_sizer.Remove(i) - self.profile_sizer.Layout() - - def on_recv_config_button(self, event): - if not self.started: - self.datagen.recv_config() - - - def on_send_button(self, event): - if not self.started: - self.datagen.send_config() - - 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 self.started: - self.data.append(self.datagen.next()) - - self.update_state() - 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() - diff --git a/reflowctl/reflowctl_gui.py b/reflowctl/reflowctl_gui.py index 5e9fb22..5303487 100755 --- a/reflowctl/reflowctl_gui.py +++ b/reflowctl/reflowctl_gui.py @@ -65,12 +65,16 @@ class Plotter(FigureCanvas): QtGui.QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) - timer = QtCore.QTimer(self) - - QtCore.QObject.connect(timer, QtCore.SIGNAL("timeout()"), self.update_figure) - timer.start(1000) self.updated = False self.fig.canvas.mpl_connect('pick_event', self.onpick) + self.update_figure() + self.started = False + + def start_timer(self): + timer = QtCore.QTimer(self) + QtCore.QObject.connect(timer, QtCore.SIGNAL("timeout()"), self.update_figure) + timer.start(1000) + def onpick(self, event): if isinstance(event.artist, Line2D): @@ -145,6 +149,8 @@ class Plotter(FigureCanvas): def solder_changed(self): self.solder.setChanged() self.updated = True + if not self.started: + self.update_figure() def setData(self, solder): self.solder = solder @@ -252,7 +258,7 @@ class ApplicationWindow(QtGui.QMainWindow): QtCore.Qt.CTRL + QtCore.Qt.Key_D) self.file_menu.addAction('S&ave plot', self.save_plot, QtCore.Qt.CTRL + QtCore.Qt.Key_A) - self.file_menu.addAction('&Quit', self.show_report, + self.file_menu.addAction('&Show Report', self.show_report, QtCore.Qt.CTRL + QtCore.Qt.Key_T) self.file_menu.addAction('&Quit', self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) @@ -313,6 +319,8 @@ class ApplicationWindow(QtGui.QMainWindow): QtCore.SIGNAL("solder_changed()"), self.plotter.solder_changed) + self.temp_level_widget.temp_level_added.connect(self.constraint_widget.temp_level_added) + self.connect( self.constraint_widget, QtCore.SIGNAL("edge_changed()"), @@ -366,11 +374,11 @@ class ApplicationWindow(QtGui.QMainWindow): self.setCentralWidget(self.splitter) self.statusBar().showMessage("Reflow GORE!1!", 10000) + self.plotter.solder_changed() def getPlotter(self): return self.plotter - def solder_selected(self, index): if index.isValid(): solder = self.solder_widget.solder_model.solder_list[index.row()] @@ -382,6 +390,8 @@ class ApplicationWindow(QtGui.QMainWindow): def edge_picked(self, ix): if self.tab_widget.currentIndex() != 1: self.tab_widget.setCurrentIndex(1) + if not self.plotter.started: + self.plotter.update_figure() def remove_solder(self): self.solder_selected(self.solder_widget.remove_solder()) @@ -433,12 +443,6 @@ class ApplicationWindow(QtGui.QMainWindow): def main(): - # numpy bug workaround - try: - numpy.log10(0.0) - except: - pass - qApp = QtGui.QApplication(sys.argv) aw = ApplicationWindow() diff --git a/reflowctl/serialmon.py b/reflowctl/serialmon.py deleted file mode 100755 index dd956fc..0000000 --- a/reflowctl/serialmon.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import serial, struct, time - -ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=2) - - -buf = "" -alles = [] - -#def parse(): - #buffer = list() - #while 1: - #try: - #i = ser.read(1) - #if ord(i) == 255: - #except Exception, e: - #print e - #else: - -def recv_config(): - ser.write(chr(255)) - ser.flush() - read(30) - ser.flushInput() - data = struct.unpack("hhhhhhhhhhhhhhh", buf) - print - print "Profile:" - print "ts_min:", data[0] - print "ts_max:", data[1] - print "tl:", data[2] - print "tp:", data[3] - print "time_max:", data[4] - print "ramp_up_min:", data[5] - print "ramp_up_max:", data[6] - print "ramp_down_min:", data[7] - print "ramp_down_max:", data[8] - - print "ts_duration_min:", data[9] - print "ts_duration_max:", data[10] - print "tl_duration_min:", data[11] - print "tl_duration_max:", data[12] - print "tp_duration_min:", data[13] - print "tp_duration_max:", data[14] - print - - -def recv_state(): - ser.write(chr(254)) - ser.flush() - read(11) - data = struct.unpack("hhhhhb", buf) - print "time: %ds, temperature: %d°C, last temperature: %d°C, state: %d, error condition: %d, heating: %d" % data - - -def send_config(): - ser.write(chr(253)) - ser.write(buf) - ser.flushInput() - - -def read(l): - global buf - global alles - buf = "" - while len(buf) < l: - try: - buf += ser.read(l) - alles.append(buf) - except Exception, e: - print e - ser.flushInput() - - -time.sleep(2) -recv_config() -while 1: - recv_state() - time.sleep(1) - diff --git a/reflowctl/solder.py b/reflowctl/solder.py new file mode 100644 index 0000000..82d2461 --- /dev/null +++ b/reflowctl/solder.py @@ -0,0 +1,259 @@ + + +import os +import os.path + +import xml.etree.ElementTree as etree + +from PyQt4 import QtGui, QtCore + +from numpy import arange, sin, pi, array, linspace, arange + +from temp_level import TempLevel, set_colors +from edge_widget import Edge +from control_widgets import AddRemoveWidget + + +def getTemperature(): + return 20. + + +class Solder(QtCore.QObject): + + log_message = QtCore.pyqtSignal(str) + + def __init__(self, filename, name=str(), description=str(), parent=None): + super(Solder, self).__init__(parent) + self.changed = False + self.filename = filename + self.name = name + self.description = description + self.temp_levels = list() + self.edges = list() + + + def __unicode__(self): + return unicode(self.name) + + def __str__(self): + return self.name + + def add_temp_level(self, name, temp, is_env): + s = TempLevel(name, temp, is_env) + self.temp_levels.append(s) + return s + + def get_temp_level_by_name(self, name): + assert isinstance(name, basestring) + for i in self.temp_levels: + if i.name == name: + return i + return None + + #def get_temp_level(self, ix): + #assert isinstance(ix, int) + #return self.temp_levels[ix] + + + def calc_profile(self): + print self.calc_profile + self.log = list() + x = list() + y = list() + duration_points = dict() + rate_points = dict() + time = 0 + + def calc(edge): + if edge.duration: + return time + edge.duration + elif edge.rate: + return time + (edge.to_tl.temp - edge.from_tl.temp) / edge.rate + + for _edge in self.edges: + x.append(time) + y.append(_edge.from_tl.temp) + time = calc(_edge) + + x.append(time) + y.append(_edge.to_tl.temp) + print "end", self.calc_profile + return array(map(float, x)), array(map(float, y)), min(x), max(x), min(y), max(y), set(), set() + + + @staticmethod + def unpack(filename, parent): + xmltree = etree.parse(filename) + root = xmltree.getroot() + solder_node = root[0] + s = Solder(filename, solder_node.attrib["name"], solder_node.attrib["description"], parent) + env_count = 0 + for temp_level in solder_node.findall("state"): + tstr = temp_level.attrib["temperature"] + is_env = False + try: + temp = int(tstr) + except ValueError: + if tstr == "$ENV": + temp = getTemperature() + is_env = True + env_count += 1 + s.add_temp_level(temp_level.attrib["name"], temp, is_env) + + set_colors(s.temp_levels) + + for edge in solder_node.findall("edge"): + from_tl = s.get_temp_level_by_name(edge.attrib["from"]) + to_tl = s.get_temp_level_by_name(edge.attrib["to"]) + duration = None + rate = None + try: + duration = float(edge.attrib["duration"]) + except ValueError: + pass + try: + rate = float(edge.attrib["rate"]) + except ValueError: + pass + + e = Edge(from_tl, to_tl, duration, rate) + s.edges.append(e) + return s + + def save(self): + if self.changed: + solder_node = etree.Element("solder_type", {"name" : self.name, "description" : self.description}) + for temp_level in self.temp_levels: + temp = temp_level.is_env and "$ENV" or str(temp_level.temp) + solder_node.append(etree.Element("state", {"name" : temp_level.name, "temperature" : temp})) + for edge in self.edges: + element = etree.Element("edge", { + "from" : edge.from_tl.name, + "to" : edge.to_tl.name, + "duration" : str(edge.duration), + "rate" : str(edge.rate)}) + solder_node.append(element) + + dirname = SolderListModel.dirname() + root = etree.Element("xml") + root.append(solder_node) + if self.filename is None: + self.filename = os.path.join(dirname, self.name + ".xml") + etree.ElementTree(root).write(self.filename, "UTF-8", True) + self.changed = False + + def setChanged(self): + self.changed = True + + +class SolderListModel(QtCore.QAbstractListModel): + def __init__(self, parent=None, *args): + """ datain: a list where each item is a row + """ + super(SolderListModel, self).__init__(parent, *args) + self._load_solder_list() + + def rowCount(self, parent=QtCore.QModelIndex()): + return len(self.solder_list) + + def _load_solder_list(self): + dirname = self.dirname() + dirlisting = filter(lambda x: os.path.splitext(x)[1] == ".xml", os.listdir(dirname)) + self.solder_list = [] + for p in dirlisting: + self.solder_list.append(Solder.unpack(os.path.join(dirname, p), self)) + self.solder_list.sort(key=lambda x: x.name) + self.reset() + + def headerData(self, col, orientation, role): + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return QtCore.QVariant("Solder Paste") + return QtCore.QVariant() + + def data(self, index, role): + if not index.isValid(): + return QtCore.QVariant() + + solder = self.solder_list[index.row()] + if role == QtCore.Qt.DisplayRole: + return QtCore.QVariant(solder.name) + + elif role == QtCore.Qt.DecorationRole and solder.changed: + return QtGui.QIcon.fromTheme("document-save") + + def setData(self, index, variant, role): + if index.isValid() and role == QtCore.Qt.EditRole: + new_name = str(variant.toString()) + if new_name and new_name != "": + self.solder_list[index.row()].name = new_name + self.solder_list[index.row()].changed = True + return True + return False + + def flags(self, index): + if not index.isValid(): + return 0 + return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable) + + def create_solder(self): + solder = Solder(None, datetime.now().strftime("%Y-%M-%D %H:%m:%s"), "") + + tl = solder.add_temp_level("environment temp", getTemperature(), True) + tl.color = QtGui.QColor(0, 0, 0) + self.solder_list.append(solder) + self.reset() + + @staticmethod + def dirname(): + return os.path.join(os.path.dirname(__file__), "solder_types") + + def check_name(self, name): + for solder in self.solder_list: + if name == solder.name: + return False + return True + + +class SolderWidget(QtGui.QWidget): + def __init__(self, parent, readonly=False): + super(SolderWidget, self).__init__(parent) + self.solder_model = SolderListModel(self) + self.solder_view = QtGui.QListView() + self.solder_view.setModel(self.solder_model) + self.solder_controls = AddRemoveWidget(self) + + layout = QtGui.QHBoxLayout(self) + layout.addWidget(self.solder_view, 3) + layout.addWidget(self.solder_controls, 1) + + self.connect( + self.solder_controls.add_button, + QtCore.SIGNAL("clicked()"), + self.solder_model.create_solder) + + self.connect( + self.solder_controls.remove_button, + QtCore.SIGNAL("clicked()"), + self.remove_solder) + + self.solder_view.setCurrentIndex(self.solder_model.index(0,0)) + + def remove_solder(self): + index = self.solder_view.currentIndex() + solder = self.solder_model.solder_list[index.row()] + try: + os.remove(solder.filename) + except OSError: + pass + del self.solder_model.solder_list[index.row()] + self.solder_model.reset() + new_index = self.solder_model.index(0) + self.solder_view.setCurrentIndex(new_index) + return new_index + + def save_solder(self, index): + self.solder_model.solder_list[index.row()].save() + self.solder_model.reset() + new_index = self.solder_model.index(self.solder_model.solder_list.index(self.plotter.solder)) + self.solder_widget.solder_view.setCurrentIndex(new_index) + diff --git a/reflowctl/temp_level.py b/reflowctl/temp_level.py new file mode 100644 index 0000000..3d1bb53 --- /dev/null +++ b/reflowctl/temp_level.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- + +from PyQt4 import QtGui, QtCore +from control_widgets import AddRemoveWidget + + +def calc_colors(count): + if count == 1: + return [QtGui.QColor(0, 255, 0),] + r = 0 + g = 255 + step = int(512. / (count-1)) + colors = list() + for i in range(count): + colors.append(QtGui.QColor(r, g, 0)) + if r < 255: + r += step + if r > 255: + g -= (r - 256) + r = 255 + g = max(0, g) + else: + g -= step + g = max(0, g) + return colors + + +def set_colors(temp_levels): + colors = calc_colors(len(temp_levels) - 1) + ix = 0 + for temp_level in temp_levels: + if not temp_level.is_env: + temp_level.color = colors[ix] + ix += 1 + else: + temp_level.color = QtGui.QColor("black") + + +class TempLevel(QtCore.QObject): + def __init__(self, name, temp, is_env=False): + super(TempLevel, self).__init__() + self.name = name + self.temp = temp + self.is_env = is_env + self.color = None + + + def __str__(self): + return "" % (self.name, self.temp) + + + def __lt__(self, other): + return self.temp < other.temp + + + def __le__(self, other): + return self.temp <= other.temp + + + def __eq__(self, other): + return self.temp == other.temp + + + def is_between(self, tl1, tl2): + tmp = [tl1, tl2, self] + tmp.sort() + return self == tmp[1] + + + def set_next(self, temp_level): + if temp_level is self: + raise ValueError("same temp_level") + self.next = temp_level + + + def __repr__(self): + return "TempLevel(%r, %r, %r)" % (self.name, self.temp, self.is_env) + + +class TempLevelModel(QtCore.QAbstractTableModel): + solder_changed = QtCore.pyqtSignal() + + def __init__(self, parent): + super(TempLevelModel, self).__init__(parent) + self._changed = False + self.temp_levels = list() + self.headerdata = [u"Name", u"Temperature (°C)"] + + + def headerData(self, col, orientation, role): + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return QtCore.QVariant(self.headerdata[col]) + return QtCore.QVariant() + + + def rowCount(self, parent): + return len(self.temp_levels) + + + def columnCount(self, parent): + return 2 + + + def data(self, index, role): + if not index.isValid(): + return QtCore.QVariant() + + if role == QtCore.Qt.DisplayRole: + col = index.column() + if col == 0: + return QtCore.QVariant(self.temp_levels[index.row()].name) + else: + return QtCore.QVariant(self.temp_levels[index.row()].temp) + + if index.column() == 0 and role == QtCore.Qt.DecorationRole: + p = QtGui.QPixmap(10, 10) + color = self.temp_levels[index.row()].color + p.fill(color) + return p + + return QtCore.QVariant() + + + def flags(self, index): + if not index.isValid(): + return 0 + + if index.row() != 0: + return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable) + else: + return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + + + def neigbors(self, temp_level): + ix = self.temp_levels.index(temp_level) + return self.temp_levels[ix-1], self.temp_levels[ix+1] + + + def setData(self, index, variant, role): + if index.isValid() and role == QtCore.Qt.EditRole: + col = index.column() + if col == 0: + self.temp_levels[index.row()].name = str(variant.toString()) + elif col == 1: + temp, res = variant.toInt() + tl = self.temp_levels[index.row()] + try: + tl0 = self.temp_levels[index.row() - 1] + if tl0.temp >= temp: + return False + except Exception, e: + pass + try: + tl1 = self.temp_levels[index.row() + 1] + if tl1.temp <= temp: + return False + except Exception, e: + pass + tl.temp = temp + + self.solder_changed.emit() + return True + return False + + + def remove_temp_level(self, index): + tmp = self.temp_levels[index] + del self.temp_levels[index] + self.reset() + self.solder_changed.emit() + return tmp + + + def add_temp_level(self, index, temp_level): + self.temp_levels.insert(index.row() + 1, temp_level) + set_colors(self.temp_levels) + self.reset() + self.solder_changed.emit() + + + def setTempLevels(self, temp_levels): + assert isinstance(temp_levels, list) + self.temp_levels = temp_levels + self.reset() + + + def clear(self): + self.temp_levels = list() + self.reset() + + + def get_temp_level(self, ix): + return self.temp_levels[ix] + + +class TempLevelWidget(QtGui.QWidget): + temp_level_removed = QtCore.pyqtSignal(TempLevel) + temp_level_added = QtCore.pyqtSignal(TempLevel, TempLevel) + temp_levels_changed = QtCore.pyqtSignal() + solder_changed = QtCore.pyqtSignal() + + def __init__(self, parent, readonly=False): + super(TempLevelWidget, self).__init__(parent) + self.readonly = readonly + self.temp_level_model = TempLevelModel(self) + + self.temp_level_view = QtGui.QTableView() + self.temp_level_view.setModel(self.temp_level_model) + self.temp_level_view.verticalHeader().setVisible(False) + self.temp_level_view.resizeColumnsToContents() + self.temp_level_view.horizontalHeader().setStretchLastSection(True) + self.temp_level_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + + h = QtGui.QHBoxLayout() + h.addWidget(self.temp_level_view) + self.setLayout(h) + + self.connect( + self.temp_level_view, + QtCore.SIGNAL("clicked(QModelIndex)"), + self.temp_level_selected) + + if not readonly: + self.controls = AddRemoveWidget(self) + h.addWidget(self.controls) + + self.connect( + self.controls.add_button, + QtCore.SIGNAL("clicked()"), + self.add_temp_level) + + self.connect( + self.controls.remove_button, + QtCore.SIGNAL("clicked()"), + self.remove_temp_level) + + self.connect( + self.temp_level_model, + QtCore.SIGNAL("solder_changed()"), + self._solder_changed) + + def _solder_changed(self): + self.solder_changed.emit() + + + def setData(self, solder): + self.temp_level_model.setTempLevels(solder.temp_levels) + self.temp_level_view.resizeColumnsToContents() + self.temp_level_view.setCurrentIndex(self.temp_level_model.index(0, 0)) + + + def add_temp_level(self): + index = self.temp_level_view.currentIndex() + old_tl = self.temp_level_model.temp_levels[index.row()] + print "next temp", self.temp_level_model.temp_levels[index.row() + 1].temp + new_temp = old_tl.temp + (self.temp_level_model.temp_levels[index.row() + 1].temp - old_tl.temp) / 2 + print "new_temp", new_temp + new_tl = TempLevel("new " + str(self.temp_level_model.rowCount(None)), new_temp) + self.temp_level_model.add_temp_level(index, new_tl) + self.temp_level_view.setCurrentIndex(self.temp_level_model.index(index.row() + 1, 0)) + self.temp_levels_changed.emit() + print "TempLevelWidget.add_temp_level 1", old_tl, new_tl + self.temp_level_added.emit(old_tl, new_tl) + print "TempLevelWidget.add_temp_level 2" + + + def remove_temp_level(self): + self.temp_level_removed.emit( + self.temp_level_model.remove_temp_level( + self.temp_level_view.currentIndex().row())) + self.solder_changed.emit() + + + def temp_level_selected(self, index): + if index.isValid(): + row = index.row() + if not self.readonly: + self.controls.remove_button.setEnabled(not self.temp_level_model.temp_levels[row].is_env)