#!/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" def calc_colors(count): r = 0 g = 255 step = int(512. / (count-1)) colors = list() print "step", step for i in range(count): colors.append(QtGui.QColor(r, g, 0)) if r < 255: r += step if r > 255: g -= r - 255 r = 255 else: g -= step g = max(0, g) return colors def getTemperature(): return 20. class TempLevel(object): def __init__(self, name, temp, is_env=False): self.name = name self.temp = temp self.is_env = is_env self.color = None class Solder(object): def __init__(self, name=str(), description=str()): self.name = name self.description = description self.temp_levels = list() self.durations = list() self.rates = 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 add_rate(self, states, rate): self.rates.append([states, rate]) def add_duration(self, states, duration): self.durations.append([states, duration]) 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): x = list() y = list() duration_points = dict() rate_points = dict() self.time = 0 used_temp_levels = set() for ix, temp_level in enumerate(self.temp_levels): if temp_level != self.temp_levels[0] and temp_level not in used_temp_levels: ix = self.temp_levels.index(temp_level) raise ValueError("TempLevel %r not connected to %r" % (temp_level.name[ix-1], self.temp_levels[ix].name)) temp_levels = None duration = None for sts, dur in self.durations: if sts[0] == temp_level: duration = dur temp_levels = sts break if temp_level not in used_temp_levels: used_temp_levels.add(temp_level) x.append(self.time) y.append(temp_level.temp) if duration is not None: if len(temp_levels) == 3: used_temp_levels.add(temp_levels[1]) used_temp_levels.add(temp_levels[2]) self.time += duration / 2 x.append(self.time) y.append(temp_levels[1].temp) self.time += duration / 2 x.append(self.time) y.append(temp_levels[2].temp) duration_points[ix] = (x[-3:], y[-3:]) else: y.append(temp_levels[1].temp) used_temp_levels.add(temp_levels[1]) self.time += duration x.append(self.time) duration_points[ix] = (x[-2:], y[-2:]) else: for ex, (sts, rate) in enumerate(self.rates): if sts[0] == temp_level: used_temp_levels.add(sts[1]) duration = (sts[1].temp - temp_level.temp) / rate self.time += duration x.append(self.time) y.append(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() solder_node = root[0] s = Solder(solder_node.attrib["name"], solder_node) 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) colors = calc_colors(len(s.temp_levels) - 1) ix = 0 for temp_level in s.temp_levels: if not temp_level.is_env: temp_level.color = colors[ix] ix += 1 else: temp_level.color = QtGui.QColor("black") for duration in solder_node.findall("duration"): temp_levels = list() for temp_level in duration: temp_levels.append(s.get_temp_level_by_name(temp_level.attrib["name"])) s.add_duration(temp_levels, int(duration.attrib["value"])) for rate in solder_node.findall("rate"): temp_levels = list() for temp_level in rate: temp_levels.append(s.get_temp_level_by_name(temp_level.attrib["name"])) s.add_rate(temp_levels, int(rate.attrib["value"])) return s 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 = filter(lambda x: os.path.splitext(x)[1] == ".xml", os.listdir(dirname)) self.listdata = [] for p in dirlisting: #try: self.listdata.append(Solder.unpack(os.path.join(dirname, p))) #except Exception, e: #print e #pass print len(self.listdata) 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 TempLevelModel(QtCore.QAbstractTableModel): 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 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 == 0: self.temp_levels[index.row()].name = str(variant.toString()) elif col == 1: self.temp_levels[index.row()].temp = variant.toInt()[0] self._changed = True return True return False def remove_temp_level(self, index): tmp = self.temp_levels[index] del self.temp_levels[index] self.reset() self._changed = True return tmp def add_temp_level(self, index, temp_level): self.beginInsertRows(QtGui.QModelIndex(), index.row(), 1) self.temp_levels.temp_levels.insert(index.row(), temp_level) self.endInsertRows() self.reset() self._changed = True 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() class Plotter(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(Plotter, self).__init__(self.fig) self.axes = self.fig.add_subplot(111) 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) self.axes.set_ymargin(0) self.axes.set_xmargin(0) 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) self.updated = True def update_figure(self): if self.updated: updated = False self.x, self.y, self.xmax, self.ymax, self.duration_points, self.rate_points = self.solder.calc_profile() #for states, value in self.durations.iteritems(): #annotation.slope_marker((states[0]) self.axes.set_xbound(lower=0, upper=self.xmax + 20) self.axes.set_ybound(lower=0, upper=self.ymax + 20) self.axes.set_yticks([state.temp for state in self.solder.temp_levels]) self.axes.set_xticks(self.x) self.plot_data.set_xdata(self.x) self.plot_data.set_ydata(self.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)) self.fig.lines = lines = list() for temp_level in self.solder.temp_levels: if not temp_level.is_env: line = Line2D([0, self.xmax + 20], [temp_level.temp, temp_level.temp], transform=self.axes.transData, figure=self.fig, color=str(temp_level.color.name()), label="name", zorder=1) lines.append(line) self.axes.legend(("Estimated profile",)) self.draw() class AddRemoveWidget(QtGui.QWidget): def __init__(self, parent, with_upown=True): #super(AddRemoveWidget, self).__init__(parent) QtGui.QWidget.__init__(self, parent) self.add_button = QtGui.QPushButton("Add") self.remove_button = QtGui.QPushButton("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) if with_upown: self.up_button = QtGui.QPushButton("Up") self.down_button = QtGui.QPushButton("Down") self._layout.addWidget(self.up_button) self._layout.addWidget(self.down_button) self._layout.addStretch(4) class AddRemoveValueWidget(AddRemoveWidget): def __init__(self, parent): super(AddRemoveValueWidget, self).__init__(parent) self.value = QtGui.QSpinBox(self) self.value.setRange(-500, 500) self._layout.addWidget(self.value) class ConstraintListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, *args): """ datain: a list where each item is a row """ super(ConstraintListModel, self).__init__(parent, *args) self.constraint_list = list() def rowCount(self, parent=QtCore.QModelIndex()): return len(self.constraint_list) 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(index.row() + 1) else: return QtCore.QVariant() def append_constraint(self): self.constraint_list.append([[], 0]) self.reset() def remove_constraint(self): del self.constraint_list[self.currentIndex().row()] self.reset() class ConstraintWidget(QtGui.QWidget): def __init__(self, name, solder): super(ConstraintWidget, self).__init__() self.name = name #self.solder = solder self.spinbox_block = False self.constraint_model = ConstraintListModel(self) # constraint selection self.all_temp_levels = TempLevelModel(self) # temp_level selection pool self.selected_temp_levels = TempLevelModel(self) # selected temp_levels self.controls = AddRemoveValueWidget(self) self.constraint_controls = AddRemoveWidget(self, False) self.controls.add_button.setText(u"Add TempLevel") self.controls.remove_button.setText(u"Remove TempLevel") self.constraint_controls.add_button.setText(u"Add Constraint") self.constraint_controls.remove_button.setText(u"Remove Constraint") self.constraint_view = QtGui.QListView(self) self.constraint_view.setModel(self.constraint_model) self.all_temp_levels_view = QtGui.QListView(self) self.all_temp_levels_view.setModel(self.all_temp_levels) self.selected_temp_levels_view = QtGui.QListView(self) self.selected_temp_levels_view.setModel(self.selected_temp_levels) h = QtGui.QHBoxLayout() h.addWidget(self.constraint_view, 1) h.addWidget(self.constraint_controls, 1) h.addWidget(self.all_temp_levels_view, 3) h.addWidget(self.controls, 1) h.addWidget(self.selected_temp_levels_view, 3) self.setLayout(h) self.connect( self.constraint_view, QtCore.SIGNAL("clicked(QModelIndex)"), self._constraint_selected) self.connect( self.controls.add_button, QtCore.SIGNAL("clicked()"), self.add_temp_level_to_constraint) self.connect( self.controls.remove_button, QtCore.SIGNAL("clicked()"), self.remove_temp_level_from_constraint) self.connect( self.controls.up_button, QtCore.SIGNAL("clicked()"), self.temp_level_up) self.connect( self.controls.down_button, QtCore.SIGNAL("clicked()"), self.temp_level_down) self.connect( self.constraint_controls.add_button, QtCore.SIGNAL("clicked()"), self.constraint_model.append_constraint) self.connect( self.constraint_controls.remove_button, QtCore.SIGNAL("clicked()"), self.remove_constraint) self.connect( self.controls.value, QtCore.SIGNAL("valueChanged(int)"), self.constraint_value_changed) def setData(self, solder): print self.setData, 1 self.solder = solder print self.setData, 2 self.all_temp_levels.setTempLevels(self.solder.temp_levels) print self.setData, 3 self._set_data() print self.setData, 4 def _constraint_selected(self, index): raise NotImplementedError() def _set_data(self): raise NotImplementedError() def add_temp_level_to_constraint(self): src_row = self.all_temp_levels_view.currentIndex().row() dst_row = self.selected_temp_levels_view.currentIndex().row() temp_level = self.all_temp_levels.temp_levels[src_row] self.selected_temp_levels.temp_levels.insert(dst_row, temp_level) #tls.append(temp_level) #tls.sort(key=attrgetter("temp")) self.selected_temp_levels.reset() self.selected_temp_levels_view.clearSelection() def remove_temp_level_from_constraint(self): del self.selected_temp_levels.temp_levels[self.selected_temp_levels_view.currentIndex().row()] self.selected_temp_levels.reset() self.selected_temp_levels_view.clearSelection() def remove_constraint(self): src_row = self.all_temp_levels_view.currentIndex().row() del self.constraint_model.constraint_list[src_row] self.constraint_model.reset() self.selected_temp_levels.clear() def constraint_value_changed(self, value): print self.constraint_value_changed if self.spinbox_block: self.spinbox_block = False return print print print "IIIIEK" print src_index = self.constraint_view.currentIndex().row() self.constraint_model.constraint_list[src_index][1] = value def slot_temp_level_removed(self, temp_level): print "temp_level" self.reset() def temp_level_up(self): dst_row = self.selected_temp_levels_view.currentIndex().row() self.selected_temp_levels.temp_levels[dst_row - 1], self.selected_temp_levels.temp_levels[dst_row] = self.selected_temp_levels.temp_levels[dst_row], self.selected_temp_levels.temp_levels[dst_row - 1] self.selected_temp_levels.reset() def temp_level_down(self): dst_row = self.selected_temp_levels_view.currentIndex().row() self.selected_temp_levels.temp_levels[dst_row], self.selected_temp_levels.temp_levels[dst_row + 1] = self.selected_temp_levels.temp_levels[dst_row + 1], self.selected_temp_levels.temp_levels[dst_row] self.selected_temp_levels.reset() class DurationConstraintWidget(ConstraintWidget): def _set_data(self): self.spinbox_block = True print self._set_data, 1 self.constraint_model.constraint_list.extend(self.solder.durations) print self._set_data, 2 ix = self.constraint_model.index(0, 0) print self._set_data, 3 self.constraint_view.setCurrentIndex(ix) print self._set_data, 4 self._constraint_selected(ix) print self._set_data, 5 def _constraint_selected(self, index): print self._constraint_selected self.spinbox_block = True temp_levels, value = self.constraint_model.constraint_list[index.row()] self.selected_temp_levels.setTempLevels(temp_levels) self.controls.value.setValue(value) class RateConstraintWidget(ConstraintWidget): def _set_data(self): print self._set_data self.spinbox_block = True self.constraint_model.constraint_list.extend(self.solder.rates) ix = self.constraint_model.index(0, 0) self.constraint_view.setCurrentIndex(ix) self._constraint_selected(ix) def _constraint_selected(self, index): print self._constraint_selected self.spinbox_block = True temp_levels, value = self.constraint_model.constraint_list[index.row()] self.selected_temp_levels.setTempLevels(temp_levels) self.controls.value.setValue(value) class TempLevelWidget(QtGui.QWidget): temp_level_removed = QtCore.pyqtSignal(TempLevel) def __init__(self, parent, solder): super(TempLevelWidget, self).__init__(parent) 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) self.controls = AddRemoveWidget(self) h = QtGui.QHBoxLayout() h.addWidget(self.temp_level_view) h.addWidget(self.controls) self.setLayout(h) self.connect( self.temp_level_view, QtCore.SIGNAL("clicked(QModelIndex)"), self.temp_level_selected) self.connect( self.controls.remove_button, QtCore.SIGNAL("clicked()"), self.remove_temp_level) def add_temp_level(self): index = self.temp_level_view.currentIndex() self.temp_level_model.add_temp_level(index,TempLevel("new", 0)) def remove_temp_level(self): self.temp_level_removed.emit( self.temp_level_model.remove_temp_level( self.temp_level_view.currentIndex().row())) def temp_level_selected(self, index): if index.isValid(): row = index.row() is_env = self.temp_level_model.temp_levels[row].is_env is_end = row == len(self.temp_level_model.temp_levels) - 1 self.controls.add_button.setEnabled(not is_end) self.controls.remove_button.setEnabled(not is_env) print "remove_button state", self.controls.remove_button.isEnabled() class ApplicationWindow(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.dpi = 100 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.solder_model = SolderListModel(self) self.solder_view = QtGui.QListView() self.solder_view.setModel(self.solder_model) self.solder_controls = AddRemoveWidget(self, False) self.tab_widget = QtGui.QTabWidget(self) self.temp_level_widget = TempLevelWidget(self, self.solder_model.listdata[0]) self.duration_widget = DurationConstraintWidget(u"Duration (s)", self.solder_model.listdata[0]) self.rate_widget = RateConstraintWidget(u"Rate (°C/s)", self.solder_model.listdata[0]) self.tab_widget.addTab(self.temp_level_widget, u"Temperature Levels") self.tab_widget.addTab(self.duration_widget, u"Duration (s)") self.tab_widget.addTab(self.rate_widget, u"Rate (°C/s)") self.connect( self.solder_view, QtCore.SIGNAL("clicked(QModelIndex)"), self.solder_selected) self.settings_widget = QtGui.QWidget(self) pl = QtGui.QHBoxLayout(self.settings_widget) pl.addWidget(self.solder_view, 1) pl.addWidget(self.solder_controls, 1) pl.addWidget(self.tab_widget, 6) self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self) self.plotter = Plotter(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)) self.splitter.addWidget(self.settings_widget) self.splitter.addWidget(self.plotter) self.splitter.setStretchFactor(0, 2) self.splitter.setStretchFactor(1, 8) self.setCentralWidget(self.splitter) self.statusBar().showMessage("I'm in reflow heaven", 2000) self.temp_level_widget.temp_level_removed.connect(self.duration_widget.slot_temp_level_removed) self.temp_level_widget.temp_level_removed.connect(self.rate_widget.slot_temp_level_removed) def solder_selected(self, index): if index.isValid(): solder = self.plotter.solder = self.solder_model.listdata[index.row()] self.temp_level_widget.temp_level_model.setTempLevels(solder.temp_levels) self.temp_level_widget.temp_level_view.resizeColumnsToContents() self.duration_widget.setData(solder) self.rate_widget.setData(solder) self.plotter.updated = True def save_plot(self): file_choices = "PNG (*.png)|*.png" filename = QtGui.QFileDialog.getSaveFileName(self, 'Save File', 'qtplot.png') print type(filename), dir(filename) self.plotter.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()