reflow/reflowctl/reflowctl_gui.py

772 lines
26 KiB
Python
Executable File

#!/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()