474 lines
16 KiB
Python
Executable File
474 lines
16 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import sys, os, struct
|
|
|
|
import numpy
|
|
from PyQt4 import QtGui, QtCore
|
|
|
|
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
|
|
from matplotlib.figure import Figure
|
|
from matplotlib.lines import Line2D
|
|
import matplotlib.pyplot as plt
|
|
from mpltools import annotation
|
|
|
|
from solder import Solder, SolderWidget
|
|
from temp_level import TempLevelWidget, TempLevel
|
|
from edge_widget import EdgeWidget
|
|
|
|
progname = os.path.basename(sys.argv[0])
|
|
progversion = "0.1"
|
|
|
|
|
|
def create_profile_packet(x, y):
|
|
tmp = [len(x) * 2 * 2,]
|
|
|
|
for a, b in zip(x, y):
|
|
tmp.append(a)
|
|
tmp.append(b)
|
|
|
|
tpl = "H" + (len(x) * 2) * "H"
|
|
res = struct.pack(tpl, *tmp)
|
|
return res
|
|
|
|
|
|
class Plotter(FigureCanvas):
|
|
"""A canvas that updates itself every second with a new plot."""
|
|
|
|
edge_picked = QtCore.pyqtSignal(int)
|
|
|
|
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('white')
|
|
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.selx = list()
|
|
self.sely = list()
|
|
|
|
self.selected_ix = 0
|
|
|
|
self.setParent(parent)
|
|
self.myapp = myapp
|
|
self.solder = None
|
|
|
|
self.plot_data, = self.axes.plot([], linewidth=2.0, color=(0, 0, 1), zorder=10, picker=5)
|
|
self.selection_data, = self.axes.plot([], linewidth=5.0, color=(1,0,0), zorder=5)
|
|
|
|
FigureCanvas.setSizePolicy(self,
|
|
QtGui.QSizePolicy.Expanding,
|
|
QtGui.QSizePolicy.Expanding)
|
|
FigureCanvas.updateGeometry(self)
|
|
|
|
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):
|
|
self.set_picked(event.ind[0])
|
|
else:
|
|
raise Exception("fixme: pickme")
|
|
|
|
|
|
def set_picked(self, ix):
|
|
if isinstance(ix, QtCore.QModelIndex):
|
|
ix = ix.row()
|
|
self.edge_picked.emit(ix)
|
|
self.selected_ix = ix
|
|
self.updated = True
|
|
if not self.started:
|
|
self.update_figure()
|
|
|
|
def update_figure(self):
|
|
if self.updated:
|
|
self.fig.lines = lines = list()
|
|
self.updated = False
|
|
self.axes.patches = list()
|
|
self.axes.texts = list()
|
|
self.x = list()
|
|
self.y = list()
|
|
|
|
try:
|
|
(self.x, self.y, self.xmin, self.xmax, self.ymin, self.ymax) = self.solder.calc_profile()
|
|
except ValueError:
|
|
(self.x, self.y, self.xmin, self.xmax, self.ymin, self.ymax) = ([], [], 0., 300., 0., 300.)
|
|
|
|
self.plot_data.set_xdata(self.x)
|
|
self.plot_data.set_ydata(self.y)
|
|
|
|
if self.selected_ix is not None:
|
|
self.selx = numpy.array(map(float, self.x[self.selected_ix:self.selected_ix + 2]))
|
|
self.sely = numpy.array(map(float, self.y[self.selected_ix:self.selected_ix + 2]))
|
|
|
|
self.selection_data.set_xdata(self.selx)
|
|
self.selection_data.set_ydata(self.sely)
|
|
|
|
self.axes.set_xbound(lower=self.xmin, upper=self.xmax + 20)
|
|
self.axes.set_ybound(lower=self.ymin, upper=self.ymax + 20)
|
|
self.axes.set_xlim(self.xmin, self.xmax + 20, auto=True)
|
|
|
|
self.axes.set_yticks(self.y)
|
|
self.axes.set_xticks(self.x)
|
|
|
|
labels = self.axes.get_xticklabels()
|
|
for label in labels:
|
|
label.set_rotation(30)
|
|
|
|
for ix, (a, b) in enumerate(zip(self.x[:-1], self.y[:-1])):
|
|
slope = (self.y[ix+1] - b) / (self.x[ix+1] - a)
|
|
if not (numpy.isnan(slope) or numpy.isinf(slope)):
|
|
origin = (a + 10, b)
|
|
annotation.slope_marker(origin, slope, ax=self.axes)
|
|
|
|
self.axes.legend(("Estimated profile",), loc=2)
|
|
|
|
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.draw()
|
|
|
|
def solder_changed(self):
|
|
self.selected_ix = None
|
|
self.solder.setChanged()
|
|
self.updated = True
|
|
if not self.started:
|
|
self.update_figure()
|
|
|
|
def setData(self, solder):
|
|
self.solder = solder
|
|
self.updated = True
|
|
self.selx = numpy.array(list())
|
|
self.sely = numpy.array(list())
|
|
if not self.started:
|
|
self.update_figure()
|
|
|
|
|
|
|
|
class OvenControlsWidget(QtGui.QWidget):
|
|
def __init__(self, parent=None):
|
|
super(OvenControlsWidget, self).__init__(parent)
|
|
|
|
self.left = QtGui.QWidget(self)
|
|
|
|
self.temp_lcd = QtGui.QLCDNumber(self)
|
|
self.time_lcd = QtGui.QLCDNumber(self)
|
|
|
|
self.time_lcd.setDigitCount(3)
|
|
self.temp_lcd.setDigitCount(3)
|
|
|
|
palette = QtGui.QPalette()
|
|
palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.black)
|
|
self.temp_lcd.setPalette(palette)
|
|
self.time_lcd.setPalette(palette)
|
|
self.time_lcd.setSmallDecimalPoint(False)
|
|
self.temp_lcd.setSmallDecimalPoint(False)
|
|
|
|
self.temp_lcd.setSegmentStyle(2)
|
|
self.time_lcd.setSegmentStyle(2)
|
|
self.time_lcd.display("000")
|
|
self.temp_lcd.display("142")
|
|
|
|
self.temp_label = QtGui.QLabel("Time", self)
|
|
self.time_label = QtGui.QLabel("Temp", self)
|
|
self.temp_label.setBuddy(self.temp_lcd)
|
|
self.time_label.setBuddy(self.time_lcd)
|
|
|
|
self.sicon = QtGui.QIcon.fromTheme("media-playback-start")
|
|
self.picon = QtGui.QIcon.fromTheme("media-playback-pause")
|
|
self.start_button = QtGui.QPushButton(self.sicon, "start")
|
|
self.start_button.setCheckable(True)
|
|
|
|
layout = QtGui.QHBoxLayout(self.left)
|
|
layout.addWidget(self.time_label)
|
|
layout.addWidget(self.time_lcd)
|
|
layout.addWidget(self.temp_label)
|
|
layout.addWidget(self.temp_lcd)
|
|
layout.addWidget(self.start_button)
|
|
|
|
layout2 = QtGui.QHBoxLayout()
|
|
self.temp_level_widget = TempLevelWidget(self, True)
|
|
layout2.addWidget(self.temp_level_widget, 2)
|
|
layout2.addWidget(self.left, 4)
|
|
self.setLayout(layout2)
|
|
|
|
|
|
self.connect(
|
|
self.start_button,
|
|
QtCore.SIGNAL("clicked()"),
|
|
self.toggle_button)
|
|
|
|
def toggle_button(self):
|
|
if self.start_button.isChecked():
|
|
self.start_button.setIcon(self.picon)
|
|
else:
|
|
self.start_button.setIcon(self.sicon)
|
|
|
|
|
|
class Report(QtGui.QWidget):
|
|
def __init__(self, parent=None):
|
|
super(Report, self).__init__(parent)
|
|
|
|
self.operator = QtGui.QLineEdit(self)
|
|
self.date_begin = QtGui.QDateTimeEdit(self)
|
|
self.date_end = QtGui.QDateTimeEdit(self)
|
|
self.durations = list()
|
|
|
|
self.plot_file = ""
|
|
|
|
layout = QtGui.QGridLayout(self)
|
|
layout.addWidget(QtGui.QLabel("operator"), 0, 0)
|
|
layout.addWidget(QtGui.QLabel("date begin"), 1, 0)
|
|
layout.addWidget(QtGui.QLabel("date end"), 2, 0)
|
|
layout.addWidget(self.operator, 0, 1)
|
|
layout.addWidget(self.date_begin, 1, 1)
|
|
layout.addWidget(self.date_end, 2, 1)
|
|
|
|
def export_pdf(self):
|
|
pass
|
|
|
|
|
|
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('&Save solder', self.save_solder,
|
|
QtCore.Qt.CTRL + QtCore.Qt.Key_S)
|
|
self.file_menu.addAction('&Reload solder', self.reload_solder,
|
|
QtCore.Qt.CTRL + QtCore.Qt.Key_R)
|
|
self.file_menu.addAction('&Delete solder', self.remove_solder,
|
|
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('&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)
|
|
|
|
self.menuBar().addMenu(self.file_menu)
|
|
|
|
self.view_menu = QtGui.QMenu('&View', self)
|
|
self.profile_view_action = self.view_menu.addAction("&Profile View",
|
|
self.open_profile_view,
|
|
QtCore.Qt.CTRL + QtCore.Qt.Key_P)
|
|
self.controls_view_action = self.view_menu.addAction(
|
|
"&Oven Controls View", self.open_controls_view,
|
|
QtCore.Qt.CTRL + QtCore.Qt.Key_O)
|
|
|
|
self.view_group = QtGui.QActionGroup(self)
|
|
self.view_group.setExclusive(True)
|
|
|
|
self.view_group.addAction(self.controls_view_action)
|
|
self.view_group.addAction(self.profile_view_action)
|
|
|
|
self.profile_view_action.setCheckable(True)
|
|
self.controls_view_action.setCheckable(True)
|
|
|
|
#self.report = Report(self)
|
|
|
|
#self.report.hide()
|
|
|
|
self.connect(self.profile_view_action,
|
|
QtCore.SIGNAL("triggered()"),
|
|
self.open_profile_view)
|
|
|
|
self.connect(self.controls_view_action,
|
|
QtCore.SIGNAL("triggered()"),
|
|
self.open_controls_view)
|
|
|
|
self.profile_view_action.setChecked(True)
|
|
|
|
self.menuBar().addMenu(self.view_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.tab_widget = QtGui.QTabWidget(self)
|
|
self.temp_level_widget = TempLevelWidget(self)
|
|
self.constraint_widget = EdgeWidget(u"Constraints")
|
|
self.tab_widget.addTab(self.temp_level_widget, u"Temperature Levels (°C)")
|
|
self.tab_widget.addTab(self.constraint_widget, u"Constraints")
|
|
|
|
self.plotter = Plotter(self, self, width=5, height=4, dpi=self.dpi)
|
|
self.controls_widget = OvenControlsWidget(self)
|
|
self.controls_widget.setVisible(False)
|
|
|
|
self.connect(
|
|
self.temp_level_widget,
|
|
QtCore.SIGNAL("solder_changed()"),
|
|
self.plotter.solder_changed)
|
|
|
|
|
|
self.temp_level_widget.temp_level_added.connect(self.constraint_widget.temp_level_added)
|
|
self.temp_level_widget.temp_level_removed.connect(self.constraint_widget.temp_level_removed)
|
|
|
|
self.connect(
|
|
self.constraint_widget,
|
|
QtCore.SIGNAL("edge_changed()"),
|
|
self.plotter.solder_changed)
|
|
|
|
self.connect(
|
|
self.constraint_widget.edge_view,
|
|
QtCore.SIGNAL("clicked(QModelIndex)"),
|
|
self.plotter.set_picked)
|
|
|
|
self.solder_widget = SolderWidget(self)
|
|
|
|
self.connect(
|
|
self.solder_widget.solder_view,
|
|
QtCore.SIGNAL("clicked(QModelIndex)"),
|
|
self.solder_selected)
|
|
|
|
self.connect(
|
|
self.solder_widget,
|
|
QtCore.SIGNAL("solder_selected(QModelIndex)"),
|
|
self.solder_selected)
|
|
|
|
self.connect(
|
|
self.plotter,
|
|
QtCore.SIGNAL("edge_picked(int)"),
|
|
self.constraint_widget.edge_picked)
|
|
|
|
self.connect(
|
|
self.plotter,
|
|
QtCore.SIGNAL("edge_picked(int)"),
|
|
self.edge_picked)
|
|
|
|
self.connect(
|
|
self.temp_level_widget.temp_level_model,
|
|
QtCore.SIGNAL("status_message(QString, int)"),
|
|
self.statusBar().showMessage)
|
|
|
|
self.settings_widget = QtGui.QWidget(self)
|
|
pl = QtGui.QHBoxLayout(self.settings_widget)
|
|
pl.addWidget(self.solder_widget, 1)
|
|
pl.addWidget(self.tab_widget, 3)
|
|
|
|
self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self)
|
|
|
|
self.plotter_splitter = QtGui.QSplitter(self)
|
|
self.profile_log = QtGui.QTextEdit(self)
|
|
|
|
self.solder_selected(self.solder_widget.solder_model.index(0, 0))
|
|
|
|
|
|
self.plotter_splitter.addWidget(self.plotter)
|
|
self.plotter_splitter.addWidget(self.profile_log)
|
|
self.plotter_splitter.setStretchFactor(0, 8)
|
|
self.plotter_splitter.setStretchFactor(1, 2)
|
|
|
|
self.splitter.addWidget(self.settings_widget)
|
|
self.splitter.addWidget(self.controls_widget)
|
|
self.splitter.addWidget(self.plotter_splitter)
|
|
self.splitter.setStretchFactor(0, 2)
|
|
self.splitter.setStretchFactor(1, 2)
|
|
self.splitter.setStretchFactor(2, 8)
|
|
|
|
self.setCentralWidget(self.splitter)
|
|
|
|
self.statusBar().showMessage("Reflow GORE!1!", 10000)
|
|
self.plotter.update_figure()
|
|
|
|
def getPlotter(self):
|
|
return self.plotter
|
|
|
|
def solder_selected(self, index):
|
|
if index.isValid():
|
|
solder = self.solder_widget.solder_model.solder_list[index.row()]
|
|
self.temp_level_widget.setData(solder)
|
|
self.constraint_widget.setData(solder)
|
|
self.plotter.setData(solder)
|
|
solder.log_message.connect(self.profile_log.setPlainText)
|
|
|
|
def edge_picked(self, ix):
|
|
if self.tab_widget.currentIndex() != 1:
|
|
self.tab_widget.setCurrentIndex(1)
|
|
|
|
def remove_solder(self):
|
|
self.solder_selected(self.solder_widget.remove_solder())
|
|
|
|
def open_controls_view(self):
|
|
self.controls_widget.show()
|
|
self.settings_widget.hide()
|
|
|
|
def open_profile_view(self):
|
|
self.controls_widget.hide()
|
|
self.settings_widget.show()
|
|
|
|
def show_report(self):
|
|
pass
|
|
#self.report.exec()
|
|
|
|
def save_plot(self):
|
|
file_choices = "PNG (*.png)|*.png"
|
|
|
|
filename = QtGui.QFileDialog.getSaveFileName(self, 'Save File', 'qtplot.png')
|
|
self.plotter.print_figure(str(filename), dpi=self.dpi)
|
|
|
|
def save_solder(self):
|
|
self.plotter.solder.save()
|
|
self.solder_widget.solder_model.solder_list.sort(key=lambda x: x.name)
|
|
self.solder_widget.solder_model.reset()
|
|
new_index = self.solder_widget.solder_model.index(self.solder_widget.solder_model.solder_list.index(self.plotter.solder))
|
|
self.solder_widget.solder_view.setCurrentIndex(new_index)
|
|
|
|
def reload_solder(self):
|
|
old_index = self.solder_widget.solder_view.currentIndex()
|
|
solder = Solder.unpack(self.plotter.solder.filename, self.solder_widget.solder_model)
|
|
self.solder_widget.solder_model.solder_list[old_index.row()] = solder
|
|
self.solder_widget.solder_model.reset()
|
|
new_index = self.solder_widget.solder_model.index(old_index.row(), 0)
|
|
self.solder_widget.solder_view.setCurrentIndex(new_index)
|
|
self.solder_selected(new_index)
|
|
|
|
def fileQuit(self):
|
|
self.close()
|
|
|
|
def closeEvent(self, ce):
|
|
self.fileQuit()
|
|
|
|
def about(self):
|
|
QtGui.QMessageBox.about(self, "About %s" % progname,
|
|
QtCore.QString(u"<html>%(prog)s version %(version)s<br/>Copyright \N{COPYRIGHT SIGN} 2012 Stefan Kögl<br/><br/>reflowctl frontend<br/><br/>Special thanks to <a href='http://www.chaostreff-dortmund.de/'>Chaostreff Dortmund</a></html>" % {"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()
|