reflow/reflowctl_gui.py

414 lines
13 KiB
Python
Executable File

# -*- coding: utf-8 -*-
#!/usr/bin/python
import sys, os, random
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 scipy.interpolate import *
progname = os.path.basename(sys.argv[0])
progversion = "0.1"
class State(object):
def __init__(self, name, temp):
self.name = name
self.temp = temp
class Solder(object):
def __init__(self):
self.psteps = []
self.durations = dict()
self.rates = dict()
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 add_state(self, name, temp):
s = State(name, temp)
self.psteps.append(s)
return s
def add_rate(self, states, rate):
self.rates[states] = rate
def add_duration(self, states, duration):
self.durations[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()
self.time = 0
used_steps = set()
for pstep in 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.iteritems():
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)
#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)
#print "2er duration", (self.time, psteps[1].temp)
else:
for sts, rate in self.rates.iteritems():
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)
return array(map(float, x)), array(map(float, y)), max(x), max(y)
@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(tuple(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(tuple(states), 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)
self.listdata = [Solder.unpack(os.path.join("solder_types", p)) for p in os.listdir("solder_types")]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.listdata)
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()
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()
elif role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
col = index.column()
if col == 0:
return QtCore.QVariant(self.psteps[index.row()].name)
else:
return QtCore.QVariant(self.psteps[index.row()].temperature)
def removeRows(self, start, _count, _parent):
print type(start), type(_count)
self.beginRemoveRows(_parent, start, start + _count)
self.psteps[:start].extend(self.psteps[start + _count:])
self.endRemoveRows()
class MyDynamicMplCanvas(FigureCanvas):
"""A canvas that updates itself every second with a new plot."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
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)
super(MyDynamicMplCanvas, self).__init__(self.fig)
self.setParent(parent)
self.solder = None
self.compute_initial_figure()
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 compute_initial_figure(self):
"""test foo bar"""
#start_rate = 1
#start_temp = 25
#start_period = None
## warmup
#warmup_rate = 1
#warmup_temp = 155
#preheat_period = None
## preheat start
#preheat_start_rate = 1
#preheat_start_temp = 155
#preheat_start_period = 100
## preheat end
#preheat_end_rate = 1
#preheat_end_temp = 185
#preheat_end_period = None
#tal_rate = 1
#tal_temp = 220
#tal_duration = 60
#peak_temp = 250
#peak_rate = 1
#x, y = self.solder.calc_profile()
#print "x", repr(x)
#print "y", repr(y)
#dtp_min = 99999.
#dtp_max = 0.
#dtpi = -1
#dtn_min = -99999.
#dtn_max = 0.
#dtni = -1
#for i in xrange(1, len(y)):
#tmp = (y[i] - y[i-1]) / (x[i] - x[i-1])
#if tmp > 0:
#if tmp < dtp_min:
#dtp_min = tmp
#dtpi = i
#elif tmp > dtp_max:
#dtp_max = tmp
#dtpi = i
#elif tmp < 0:
#if tmp > dtn_min:
#dtn_min = tmp
#dtni = i
#elif tmp < dtn_max:
#dtn_max = tmp
#dtni = i
#print "max negative", dtn_min, dtn_max, dtni
#print "max positive", dtp_min, dtp_max, dtpi
self.plot_data, = self.axes.plot([], linewidth=1.0, color=(0,0,1))
#self.axes.plot(p1x, p1y, 'r-o')
def update_figure(self):
# Build a list of 4 random integers between 0 and 10 (both inclusive)
x, y, xmax, ymax = self.solder.calc_profile()
lines = list()
legend = list()
cols = ("lightgreen", "yellow", "orange", "red")
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=cols[ix], label="name")
lines.append(line)
self.fig.lines = lines
self.axes.set_xbound(lower=0, upper=xmax + 20)
self.axes.set_ybound(lower=0, upper=ymax + 20)
#self.axes.grid(True, color='gray')
pylab.setp(self.axes.get_xticklabels(), visible=True)
self.plot_data.set_xdata(x)
self.plot_data.set_ydata(y)
self.axes.legend(("Estimated profile",))
self.draw()
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.steps_box = QtGui.QGroupBox(self)
self.dpi = 100
pl = QtGui.QHBoxLayout(self.profile_widget)
sl = QtGui.QVBoxLayout(self.steps_box)
self.solder_model = SolderListModel(self)
self.pstep_model = PStepModel(self)
self.solder_view = QtGui.QListView()
self.pstep_view = QtGui.QTableView()
self.solder_view.setModel(self.solder_model)
self.pstep_view.setModel(self.pstep_model)
self.connect(self.solder_view, QtCore.SIGNAL("clicked(QModelIndex)"), self.solder_selected)
pl.addWidget(self.solder_view)
pl.addWidget(self.pstep_view)
l = QtGui.QVBoxLayout(self.main_widget)
self.dc = MyDynamicMplCanvas(self.main_widget, width=5, height=4, dpi=self.dpi)
self.dc.solder = self.solder_model.listdata[0]
l.addWidget(self.profile_widget, 1)
l.addWidget(self.dc, 10)
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.removeRows(0, self.pstep_model.rowCount(QtCore.QModelIndex()))
self.pstep_model.insertRows()
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
Copyright \N{COPYRIGHT SIGN} 2012 Stefan Kögl
reflowctl frontend"""
% {"prog": progname, "version": progversion})
qApp = QtGui.QApplication(sys.argv)
aw = ApplicationWindow()
aw.setWindowTitle("%s" % progname)
aw.show()
sys.exit(qApp.exec_())