#!/usr/bin/python # -*- coding: utf-8 -*- import sys, os, random, copy from operator import attrgetter import xml.etree.ElementTree as etree import pylab from PyQt4 import QtGui, QtCore from numpy import arange, sin, pi, array, linspace, arange from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure from matplotlib.lines import Line2D from matplotlib.path import Path import matplotlib.patches as patches #from mpltools import annotation progname = os.path.basename(sys.argv[0]) progversion = "0.1" PSTEP_COLORS = ("lightgreen", "yellow", "orange", "red") class State(object): def __init__(self, name, temp): self.name = name self.temp = temp class Solder(object): def __init__(self): self.psteps = [] self.durations = list() self.rates = list() self.name = None #start = self.add_state("start", 25) #ps = self.add_state("preheat start", 150) #pe = self.add_state("preheat end", 185) #tal = self.add_state("tal", 220) #peak = self.add_state("peak", 250) #end = self.add_state("end", 25) #self.add_duration((ps, pe), 100) #self.add_duration((tal, peak, tal), 100) #self.add_rate((start, ps), 1) #self.add_rate((ps, pe), 1) #self.add_rate((pe, tal), 1) #self.add_rate((tal, end), -2) def __unicode__(self): return unicode(self.name) def __str__(self): return self.name def color(self, index): return PSTEP_COLORS[index] def add_state(self, name, temp): s = State(name, temp) self.psteps.append(s) return s def add_rate(self, states, rate): self.rates.append((states, rate)) def add_duration(self, states, duration): self.durations.append((states, duration)) def get_state(self, name): for i in self.psteps: if i.name == name: return i return None def calc_profile(self): x = list() y = list() duration_points = dict() rate_points = dict() self.time = 0 used_steps = set() for ix, pstep in enumerate(self.psteps): #print "-- ", repr(pstep.name), pstep.temp, pstep.used, self.time, x, y if pstep != self.psteps[0] and pstep not in used_steps: ix = self.psteps.index(pstep) raise Exception("step %r not connected to step %r or step %r" % (pstep.name, self.psteps[ix-1].name, self.psteps[ix+1].name)) psteps = None duration = None for sts, dur in self.durations: if sts[0] == pstep: duration = dur psteps = sts break if pstep not in used_steps: used_steps.add(pstep) x.append(self.time) y.append(pstep.temp) if duration is not None: if len(psteps) == 3: used_steps.add(psteps[1]) used_steps.add(psteps[2]) self.time += duration / 2 x.append(self.time) y.append(psteps[1].temp) #print "3er duration", (self.time, psteps[1].temp) self.time += duration / 2 x.append(self.time) y.append(psteps[2].temp) duration_points[ix] = (x[-3:], y[-3:]) #print "3er duration", (self.time, psteps[2].temp) else: y.append(psteps[1].temp) used_steps.add(psteps[1]) self.time += duration x.append(self.time) duration_points[ix] = (x[-2:], y[-2:]) #print "2er duration", (self.time, psteps[1].temp) else: for ex, (sts, rate) in enumerate(self.rates): if sts[0] == pstep: used_steps.add(sts[1]) duration = (sts[1].temp - pstep.temp) / rate self.time += duration x.append(self.time) y.append(sts[1].temp) #print "rate", (self.time, sts[1].temp) rate_points[ex] = (x[-2:], y[-2:]) return array(map(float, x)), array(map(float, y)), max(x), max(y), duration_points, rate_points @staticmethod def unpack(filename): xmltree = etree.parse(filename) root = xmltree.getroot() s = Solder() s.name = root[0].attrib["name"] for state in root[0].findall("state"): s.add_state(state.attrib["name"], int(state.attrib["temperature"])) for duration in root[0].findall("duration"): states = list() for state in duration: states.append(s.get_state(state.attrib["name"])) s.add_duration(states, int(duration.attrib["value"])) for rate in root[0].findall("rate"): #print rate states = list() for state in rate: states.append(s.get_state(state.attrib["name"])) s.add_rate(states, int(rate.attrib["value"])) return s def serialize(self, pstep_list): return ", ".join(map(attrgetter("name"), pstep_list)) 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) dirname = os.path.join(os.path.dirname(__file__), "solder_types") dirlisting = os.listdir(dirname) self.listdata = [] for p in dirlisting: #try: self.listdata.append(Solder.unpack(os.path.join(dirname, p))) #except Exception: #pass def rowCount(self, parent=QtCore.QModelIndex()): return len(self.listdata) 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 index.isValid() and role == QtCore.Qt.DisplayRole: return QtCore.QVariant(self.listdata[index.row()].name) else: return QtCore.QVariant() class PStepModel(QtCore.QAbstractTableModel): def __init__(self, parent=None, *args): super(PStepModel, self).__init__(parent, *args) self.psteps = 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.psteps) 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.psteps[index.row()].name) else: return QtCore.QVariant(self.psteps[index.row()].temp) if index.column() == 0 and role == QtCore.Qt.DecorationRole: p = QtGui.QPixmap(10,10) cr = row = index.row() color = index.row() in (0, len(self.psteps)-1) and QtGui.QColor("black") or QtGui.QColor(PSTEP_COLORS[cr-1]) p.fill(color) return p return QtCore.QVariant() def remove_pstep(self, index): del self.psteps[index] self.reset() def add_pstep(self, index, pstep): self.psteps.insert(index, pstep) self.reset() def setSteps(self, steps): assert isinstance(steps, list) self.psteps = steps self.reset() class MyDynamicMplCanvas(FigureCanvas): """A canvas that updates itself every second with a new plot.""" def __init__(self, parent, myapp, width, height, dpi): self.fig = Figure(figsize=(width, height), dpi=dpi) super(MyDynamicMplCanvas, self).__init__(self.fig) self.axes = self.fig.add_subplot(111) ## We want the axes cleared every time plot() is called self.axes.set_axis_bgcolor('black') self.axes.set_title(u'reflow profile', 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) self.setParent(parent) self.myapp = myapp self.solder = None self.plot_data, = self.axes.plot([], linewidth=1.0, color=(0,0,1), zorder=10) self.selection_data, = self.axes.plot([], linewidth=1.0, color=(1,1,1), zorder=5) FigureCanvas.setSizePolicy(self, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) timer = QtCore.QTimer(self) self.counter = list() QtCore.QObject.connect(timer, QtCore.SIGNAL("timeout()"), self.update_figure) timer.start(1000) def update_figure(self): #Build a list of 4 random integers between 0 and 10 (both inclusive) x, y, xmax, ymax, duration_points, rate_points = self.solder.calc_profile() lines = list() legend = list() #for states, value in self.durations.iteritems(): #annotation.slope_marker((states[0]) self.fig.lines = lines self.axes.set_xbound(lower=0, upper=xmax + 20) self.axes.set_ybound(lower=0, upper=ymax + 20) self.axes.set_ymargin(0) self.axes.set_xmargin(0) self.axes.set_yticks([state.temp for state in self.solder.psteps]) self.axes.set_xticks(x) self.plot_data.set_xdata(x) self.plot_data.set_ydata(y) self.plot_data.set_zorder(20) duration_widget = self.myapp.duration_widget #self.selection_data.set_xdata(array(da)) #self.selection_data.set_ydata(array(db)) for ix, i in enumerate(self.solder.psteps[1:-1]): line = Line2D([0, xmax + 20], [i.temp, i.temp], transform=self.axes.transData, figure=self.fig, color=self.solder.color(ix), label="name", zorder=1) lines.append(line) self.axes.legend(("Estimated profile",)) self.draw() # contraint_list | label - checkboxes # label - value class ConstraintWidget(QtGui.QWidget): def __init__(self, name): super(ConstraintWidget, self).__init__() self.name = name self.solder = None self.value = QtGui.QSpinBox(self) self.value.setRange(-300, 400) self.constraint_model = QtGui.QStringListModel(self) # constraint selection self.all_psteps = PStepModel(self) # pstep selection pool self.selected_psteps = PStepModel(self) # selected psteps #self.all_psteps.setSteps(self.solder.psteps) self.add_button = QtGui.QPushButton("Add", self) self.remove_button = QtGui.QPushButton("Remove", self) bg = QtGui.QWidget(self) gbl = QtGui.QVBoxLayout(bg) gbl.addWidget(self.add_button) gbl.addWidget(self.remove_button) gbl.addStretch(5) gbl.addWidget(self.value) self.constraint_list_view = QtGui.QListView(self) self.constraint_list_view.setModel(self.constraint_model) self.all_psteps_view = QtGui.QListView(self) self.all_psteps_view.setModel(self.all_psteps) self.selected_psteps_view = QtGui.QListView(self) self.selected_psteps_view.setModel(self.selected_psteps) #self.left = QtGui.QWidget(self) #gl = QtGui.QGridLayout(self.left) #gl.addWidget(QtGui.QLabel(u"Steps"), 0,0) #gl.addWidget(QtGui.QLabel(name), 1,0) #gl.addWidget(self.checkboxes_group, 0, 1) #gl.addWidget(self.value, 1, 1) #self.gl = gl h = QtGui.QHBoxLayout() h.addWidget(self.constraint_list_view) h.addWidget(self.all_psteps_view) h.addWidget(bg) h.addWidget(self.selected_psteps_view) self.setLayout(h) self.connect( self.constraint_list_view, QtCore.SIGNAL("clicked(QModelIndex)"), self.constraint_clicked) self.connect( self.add_button, QtCore.SIGNAL("clicked()"), self.add_constraint) self.connect( self.remove_button, QtCore.SIGNAL("clicked()"), self.remove_constraint) def setData(self, solder): self.solder = solder self.all_psteps.setSteps(self.solder.psteps) self.getConstraints() def constraint_clicked(self, index): raise NotImplementedError() def getConstraints(self): raise NotImplementedError() def add_constraint(self): raise NotImplementedError() def remove_constraint(self): raise NotImplementedError() class DurationConstraintWidget(ConstraintWidget): def getConstraints(self): tmp = QtCore.QStringList() for ix in xrange(len(self.solder.durations)): tmp << unicode(ix + 1) self.constraint_model.setStringList(tmp) k, t = self.solder.durations[0] self.value.setValue(t) self.constraint_list_view.setCurrentIndex(self.constraint_model.index(0, 0)) self.constraint_clicked(self.constraint_model.index(0, 0)) def add_constraint(self): self.selected_psteps.psteps.append(self.all_psteps.psteps[self.all_psteps_view.currentIndex().row()]) self.selected_psteps.reset() #self.selected_psteps_view.setCurrentIndex(QtCore.QModelIndex()) self.selected_psteps_view.clearSelection() def remove_constraint(self): del self.selected_psteps.psteps[self.selected_psteps_view.currentIndex().row()] self.selected_psteps.reset() #self.selected_psteps_view.setCurrentIndex(QtCore.QModelIndex()) self.selected_psteps_view.clearSelection() def constraint_clicked(self, index): psteps, value = self.solder.durations[index.row()] self.selected_psteps.setSteps(psteps) self.value.setValue(value) class RateConstraintWidget(ConstraintWidget): def getConstraints(self): tmp = QtCore.QStringList() for ix in xrange(len(self.solder.durations)): tmp << unicode(ix + 1) self.constraint_model.setStringList(tmp) k, t = self.solder.rates[0] self.value.setValue(t) self.constraint_list_view.setCurrentIndex(self.constraint_model.index(0, 0)) self.constraint_clicked(self.constraint_model.index(0, 0)) def constraint_clicked(self, index): psteps, value = self.solder.durations[index.row()] self.selected_psteps.setSteps(psteps) self.value.setValue(value) class ApplicationWindow(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.setWindowTitle("application main window") self.file_menu = QtGui.QMenu('&File', self) self.file_menu.addAction('&Quit', self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) self.file_menu.addAction('&Save plot', self.save_plot, QtCore.Qt.CTRL + QtCore.Qt.Key_S) self.menuBar().addMenu(self.file_menu) self.help_menu = QtGui.QMenu('&Help', self) self.menuBar().addSeparator() self.menuBar().addMenu(self.help_menu) self.help_menu.addAction('&About', self.about) self.main_widget = QtGui.QWidget(self) self.profile_widget = QtGui.QWidget(self) self.tab_widget = QtGui.QTabWidget(self) self.duration_widget = DurationConstraintWidget(u"Duration (s)") self.rate_widget = RateConstraintWidget(u"Rate (°C/s)") self.dpi = 100 pl = QtGui.QHBoxLayout(self.profile_widget) self.solder_model = SolderListModel(self) self.pstep_model = PStepModel(self) self.pstep_view = QtGui.QTableView() self.pstep_view.setModel(self.pstep_model) self.pstep_view.verticalHeader().setVisible(False) self.pstep_view.resizeColumnsToContents() self.solder_view = QtGui.QListView() self.solder_view.setModel(self.solder_model) self.connect( self.solder_view, QtCore.SIGNAL("clicked(QModelIndex)"), self.solder_selected) self.tab_widget.addTab(self.pstep_view, u"Temperature Steps") self.tab_widget.addTab(self.duration_widget, u"Duration (s)") self.tab_widget.addTab(self.rate_widget, u"Rate (°C/s)") pl.addWidget(self.solder_view, 1) pl.addWidget(self.tab_widget, 7) #pl.addWidget(self.duration_widget) #pl.addWidget(self.rate_widget) l = QtGui.QVBoxLayout(self.main_widget) self.dc = MyDynamicMplCanvas(self, self, width=5, height=4, dpi=self.dpi) self.solder_view.setCurrentIndex(self.solder_model.index(0,0)) self.solder_selected(self.solder_model.index(0,0)) l.addWidget(self.profile_widget, 2) l.addWidget(self.dc, 8) self.main_widget.setFocus() self.setCentralWidget(self.main_widget) self.statusBar().showMessage("I'm in reflow heaven", 2000) def solder_selected(self, index): if index.isValid(): self.dc.solder = self.solder_model.listdata[index.row()] self.pstep_model.setSteps(self.dc.solder.psteps) self.pstep_view.resizeColumnsToContents() self.duration_widget.setData(self.dc.solder) self.rate_widget.setData(self.dc.solder) def save_plot(self): file_choices = "PNG (*.png)|*.png" filename = QtGui.QFileDialog.getSaveFileName(self, 'Save File', 'qtplot.png') print type(filename), dir(filename) self.dc.print_figure(str(filename), dpi=self.dpi) def fileQuit(self): self.close() def closeEvent(self, ce): self.fileQuit() def about(self): QtGui.QMessageBox.about(self, "About %s" % progname, u"%(prog)s version %(version)s\n" \ u"Copyright \N{COPYRIGHT SIGN} 2012 Stefan Kögl\n\n" \ u"reflowctl frontend" % {"prog": progname, "version": progversion}) def main(): qApp = QtGui.QApplication(sys.argv) aw = ApplicationWindow() aw.setWindowTitle("%s" % progname) aw.show() sys.exit(qApp.exec_()) if __name__ == '__main__': main()