added missing files, replotting in config mode on demand and not via timer thread anymore

This commit is contained in:
Stefan Kögl 2012-11-28 17:41:35 +01:00
parent 40b2130561
commit ef6298cc44
7 changed files with 732 additions and 542 deletions

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
class AddRemoveWidget(QtGui.QWidget):
def __init__(self, parent):
#super(AddRemoveWidget, self).__init__(parent)
QtGui.QWidget.__init__(self, parent)
self.add_button = QtGui.QPushButton(QtGui.QIcon.fromTheme("list-add"), "")
self.remove_button = QtGui.QPushButton(QtGui.QIcon.fromTheme("list-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)
self._layout.addStretch(4)

159
reflowctl/edge_widget.py Normal file
View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
class Edge(QtCore.QObject):
def __init__(self, from_tl, to_tl, duration=None, rate=None):
super(Edge, self).__init__(None)
self.from_tl = from_tl
self.to_tl = to_tl
self.duration = duration
self.rate = rate
def __repr__(self):
return "Edge(%r, %r, %r, %r)" % (self.from_tl, self.to_tl, self.duration, self.rate)
class EdgeModel(QtCore.QAbstractTableModel):
edge_changed = QtCore.pyqtSignal()
def __init__(self, parent):
super(EdgeModel, self).__init__(parent)
self.edges = list()
self.headerdata = [
u"From Temperature (°C)",
u"To Temperature (°C)",
u"Duration (s)",
u"Rate (°C/s)"]
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.edges)
def columnCount(self, parent):
return 4
def data(self, index, role):
if not index.isValid():
return QtCore.QVariant()
col = index.column()
if role == QtCore.Qt.DisplayRole:
if col == 0:
return QtCore.QVariant(self.edges[index.row()].from_tl.name)
elif col == 1:
return QtCore.QVariant(self.edges[index.row()].to_tl.name)
elif col == 2:
return QtCore.QVariant(self.edges[index.row()].duration)
elif col == 3:
return QtCore.QVariant(self.edges[index.row()].rate)
if index.column() < 2 and role == QtCore.Qt.DecorationRole:
p = QtGui.QPixmap(10,10)
if col == 0:
p.fill(self.edges[index.row()].from_tl.color)
elif col == 1:
p.fill(self.edges[index.row()].to_tl.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 == 2:
self.edges[index.row()].duration = variant.toFloat()[0]
elif col == 3:
self.edges[index.row()].rate = variant.toFloat()[0]
self.edge_changed.emit()
return True
return False
def remove_edge(self, index):
tmp = self.edges[index]
del self.edges[index]
self.reset()
self.edge_changed.emit()
return tmp
def add_edge(self, index, edge):
self.edges.insert(index.row() + 1, edge)
self.reset()
self.edge_changed.emit()
def setTempLevels(self, edges):
assert isinstance(edges, list)
self.edges = edges
self.reset()
def clear(self):
self.edges = list()
self.reset()
def get_edge(self, ix):
return self.edges[ix]
class ConstraintWidget(QtGui.QWidget):
edge_changed = QtCore.pyqtSignal()
def __init__(self, name):
super(ConstraintWidget, self).__init__()
self.name = name
self.edge_model = EdgeModel(self) # temp_level selection pool
self.edge_view = QtGui.QTableView(self)
self.edge_view.setModel(self.edge_model)
self.edge_view.verticalHeader().setVisible(False)
self.edge_view.resizeColumnsToContents()
self.edge_view.horizontalHeader().setStretchLastSection(True)
self.edge_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
h = QtGui.QHBoxLayout(self)
h.addWidget(self.edge_view)
def edge_picked(self, ix):
print self.edge_picked
self.edge_view.setCurrentIndex(self.edge_model.index(ix, 0))
def setData(self, solder):
print self.setData
self.solder = solder
self.edge_model.setTempLevels(solder.edges)
self.edge_view.setCurrentIndex(self.edge_model.index(0,0))
self.connect(
self.edge_model,
QtCore.SIGNAL("edge_changed()"),
self._edge_changed)
def _edge_changed(self):
print self.temp_level_added
self.edge_changed.emit()
def temp_level_added(self, old_tl, new_tl):
print self.temp_level_added, len(self.edge_model.edges)
new_edge = None
ix = 0
for ix, edge in enumerate(self.edge_model.edges):
print ix, edge
if edge.from_tl == old_tl:
duration = None if edge.duration is None else edge.duration / 2
new_edge = Edge(new_tl, edge.to_tl, duration, edge.rate)
edge.duration = duration
edge.to_tl = new_tl
break
self.edge_model.edges.insert(ix+1, new_edge)
self.edge_model.reset()
print "end", self.temp_level_added

View File

@ -1,449 +0,0 @@
# -*- coding: utf-8 -*-
import os
import pprint
import random
import sys
import wx
REFRESH_INTERVAL_MS = 1000
import matplotlib
matplotlib.use('WXAgg')
import matplotlib.lines
from matplotlib.figure import Figure
from matplotlib.pyplot import legend
from matplotlib.backends.backend_wxagg import \
FigureCanvasWxAgg as FigCanvas, \
NavigationToolbar2WxAgg as NavigationToolbar
from matplotlib.path import Path
import matplotlib.patches as patches
import wx.lib.buttons as buttons
import numpy as np
import pylab
from Arduino_Monitor import SerialData as DataGen
import Arduino_Monitor
class GraphFrame(wx.Frame):
""" The main frame of the application
"""
title = 'reflowctl gui'
def __init__(self):
wx.Frame.__init__(self, None, -1, self.title)
self.datagen = DataGen()
self.data = [self.datagen.next()]
self.started = False
self.profile = []
self.state = []
self.count = 0
self.create_menu()
self.create_status_bar()
self.create_main_panel()
self.redraw_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer)
self.redraw_timer.Start(REFRESH_INTERVAL_MS)
def create_menu(self):
self.menubar = wx.MenuBar()
menu_file = wx.Menu()
m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file")
self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt)
menu_file.AppendSeparator()
m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit")
self.Bind(wx.EVT_MENU, self.on_exit, m_exit)
self.menubar.Append(menu_file, "&File")
self.SetMenuBar(self.menubar)
def create_main_panel(self):
self.panel = wx.Panel(self)
self.hbox1 = wx.BoxSizer(wx.HORIZONTAL)
self.init_profile()
self.init_log()
self.init_oven_status()
self.init_plot()
self.canvas = FigCanvas(self.panel, -1, self.fig)
self.recv_config_button = wx.Button(self.panel, -1, "Receive Config")
self.Bind(wx.EVT_BUTTON, self.on_recv_config_button, self.recv_config_button)
self.send_button = wx.Button(self.panel, -1, "Send Config")
self.Bind(wx.EVT_BUTTON, self.on_send_button, self.send_button)
self.start_button = buttons.GenToggleButton(self.panel, -1, "Start")
self.Bind(wx.EVT_BUTTON, self.on_start_button, self.start_button)
#self.on_bitmap = wx.Image('burn.png', wx.BITMAP_TYPE_PNG).Scale(32, 32, wx.IMAGE_QUALITY_HIGH).ConvertToBitmap()
#self.off_bitmap = wx.Image('unburn.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap()
self.ctrls = wx.BoxSizer(wx.VERTICAL)
self.ctrls.Add(self.recv_config_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT)
self.ctrls.Add(self.send_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT)
self.ctrls.Add(self.start_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT)
self.hbox1.Add(self.ctrls, border=5, flag=wx.ALL | wx.ALIGN_TOP)
self.vbox = wx.BoxSizer(wx.VERTICAL)
self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.ALL)
self.vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)
#self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP)
self.panel.SetSizer(self.vbox)
self.vbox.Fit(self)
def profile_spin_changed(self, event):
print dir(event)
def add_profile_item(self, title, sizer, min_=1, max_=250):
mc = 8
item = wx.SpinCtrl(self.panel, -1, "", (30, 50))
item.SetRange(min_, max_)
item.SetValue(Arduino_Monitor.profile[self.count])
self.Bind(wx.EVT_SPIN, self.profile_spin_changed, item)
sizer.Add(wx.StaticText(self.panel, -1, title), (self.count, 0))
sizer.Add(item, (self.count, 1))
self.count += 1
self.profile.append(item)
def init_profile(self):
self.preheat_sizer = wx.GridBagSizer(5, 5)
self.rampup_sizer = wx.GridBagSizer(5, 5)
self.peak_sizer = wx.GridBagSizer(5, 5)
self.rampdown_sizer = wx.GridBagSizer(5, 5)
self.add_profile_item("Ts_min (°C)", self.preheat_sizer, 0, 300)
self.add_profile_item("Ts_max (°C)", self.preheat_sizer, 0, 300)
self.add_profile_item("Ts duration min (s)", self.preheat_sizer, 0, 300)
self.add_profile_item("Ts duration max (s)", self.preheat_sizer, 0, 300)
self.add_profile_item("ts ramp up min (°C/s)", self.preheat_sizer, 1, 100)
self.add_profile_item("ts ramp up max (°C/s)", self.preheat_sizer, 1, 100)
self.add_profile_item("tp ramp up min (°C/s)", self.peak_sizer, 1, 100)
self.add_profile_item("tp ramp up max (°C/s)", self.peak_sizer1, 100)
self.add_profile_item("Tl duration min (s)", self.rampup_sizer, 0, 300)
self.add_profile_item("Tl duration max (s)", self.rampup_sizer, 0, 300)
self.add_profile_item("Tp (°C)", self.peak_sizer, 0, 300)
self.add_profile_item("Tp duration min (s)", self.peak_sizer, 0, 300)
self.add_profile_item("Tp duration max (s)", self.peak_sizer, 0, 300)
self.add_profile_item("ramp down min (°C/s)", self.rampdown_sizer, -100, 0)
self.add_profile_item("ramp down max (°C/s)", self.rampdown_sizer, -100, 0)
self.add_profile_item("time max (s)", 0, 800)
self.box = wx.StaticBox(self.panel, -1, "Profile Settings")
self.bsizer = wx.StaticBoxSizer(self.box, wx.VERTICAL)
self.bsizer.Add(self.profile_sizer, 0, flag=wx.ALL, border=5)
self.hbox1.Add(self.bsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP)
def init_oven_status(self):
self.oven_status_sizer = wx.GridBagSizer(5, 5)
#set_min = 0;
#set_max = 0;
#set_dt_min = 0;
#set_dt_max = 0;
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Connected"), (0, 0))
self.oven_connected = wx.StaticText(self.panel, -1, str(self.datagen.connected()))
self.oven_status_sizer.Add(self.oven_connected, (0, 1))
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Temperature"), (1, 0))
self.temperature = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[1]))
self.oven_status_sizer.Add(self.temperature, (1, 1))
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Time"), (2, 0))
self.time = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[0]))
self.oven_status_sizer.Add(self.time, (2, 1))
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "State"), (3, 0))
self.pstate = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[3]))
self.oven_status_sizer.Add(self.pstate, (3, 1))
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Error"), (4, 0))
self.perror = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[4]))
self.oven_status_sizer.Add(self.perror, (4, 1))
self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Heating"), (5, 0))
self.is_oven_heating = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[5]))
self.oven_status_sizer.Add(self.is_oven_heating, (5, 1))
self.obox = wx.StaticBox(self.panel, -1, "Oven status")
self.osizer = wx.StaticBoxSizer(self.obox, wx.VERTICAL)
self.osizer.Add(self.oven_status_sizer, 0, flag=wx.ALL, border=5)
self.hbox1.Add(self.osizer, border=5, flag=wx.ALL | wx.ALIGN_TOP)
def init_log(self):
self.log_sizer = wx.GridBagSizer(5, 5)
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_start"), (0, 0))
self.ts_time_start = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.ts_time_start, (0, 1))
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_end"), (1, 0))
self.ts_time_end = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.ts_time_end, (1, 1))
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_start"), (2, 0))
self.tl_time_start = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.tl_time_start, (2, 1))
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_end"), (3, 0))
self.tl_time_end = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.tl_time_end, (3, 1))
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_start"), (4, 0))
self.tp_time_start = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.tp_time_start, (4, 1))
self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_end"), (5, 0))
self.tp_time_end = wx.TextCtrl(self.panel, -1)
self.log_sizer.Add(self.tp_time_end, (5, 1))
self.lbox = wx.StaticBox(self.panel, -1, "Profile Log")
self.lsizer = wx.StaticBoxSizer(self.lbox, wx.VERTICAL)
self.lsizer.Add(self.log_sizer, 0, flag=wx.ALL, border=5)
self.hbox1.Add(self.lsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP)
def create_status_bar(self):
self.statusbar = self.CreateStatusBar()
def init_plot(self):
self.dpi = 100
self.fig = Figure((4.0, 4.0), dpi=self.dpi)
self.axes = self.fig.add_subplot(111)
self.axes.set_axis_bgcolor('black')
self.axes.set_title(u'Reflow Temperature', 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)
# no 1
ts_min_x_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]
ts_min_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN]
# no 2
ts_max_x_min = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MIN]
ts_max_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]
# no t1
ts_max_x_max = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MAX]
ts_max_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]
# no t2
ts_min_x_max = ts_max_x_max - (ts_max_y_max - ts_min_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]
ts_min_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN]
# no 10
t0_x_max = ts_min_x_max - (ts_max_x_max / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX])
t0_y_max = 0
# no 4
tp_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - ts_max_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]
tp_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TP]
# no 5
tp_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MAX]
tp_y_max = tp_y_min
# no 8
tp5_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MIN]
tp5_y_max = tp_y_max - 5
# no 9
tp5_x_min = ts_max_x_max + (tp5_y_max - ts_max_y_max) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]
tp5_y_min = tp5_y_max
# no 6
end_x_max = tp_x_max + tp_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MIN])
end_y_max = 0
self.xmax = end_x_max + 20
self.ymax = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] + 20
# no 7
end_x_min = tp5_x_max + tp5_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MAX])
end_y_min = 0
tsmin = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN]
tsmax = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]
tl = Arduino_Monitor.profile[Arduino_Monitor.PI_TL]
tp = Arduino_Monitor.profile[Arduino_Monitor.PI_TP]
self.ts_line_min = matplotlib.lines.Line2D([0, self.xmax], [tsmin, tsmin],
transform=self.axes.transData, figure=self.fig, color='green')
self.ts_line_max = matplotlib.lines.Line2D([0, self.xmax], [tsmax, tsmax],
transform=self.axes.transData, figure=self.fig, label="Ts_max", color='lightgreen')
self.tl_line = matplotlib.lines.Line2D([0, self.xmax], [tl, tl],
transform=self.axes.transData, figure=self.fig, label="Tl", color='yellow')
self.tp_line = matplotlib.lines.Line2D([0, self.xmax], [tp, tp],
transform=self.axes.transData, figure=self.fig, label="Tp", color='blue')
self.ts_line_min.set_label("Ts_min")
self.ts_line_min.set_label("Ts_max")
self.tl_line.set_label("Tl")
self.tp_line.set_label("Tp")
self.fig.lines.extend([self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line])
verts = [
[0.0, 0.0],
[ts_min_x_min, ts_min_y_min],
[ts_max_x_min, ts_max_y_min],
[ts_max_x_max, ts_max_y_max],
[ts_min_x_max, ts_min_y_max],
#[tp_x_min, tp_y_min],
#[tp_x_max, tp_y_max],
#[end_x_max, end_y_max],
#[end_x_min, end_y_min],
#[tp5_x_max, tp5_y_max],
#[tp5_x_min, tp5_y_min],
[t0_x_max, t0_y_max],
[0.0, 0.0]]
codes = [
Path.MOVETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
#Path.LINETO,
#Path.LINETO,
#Path.LINETO,
#Path.LINETO,
Path.CLOSEPOLY]
self.plot_data = self.axes.plot(
self.data,
linewidth=1,
color=(1, 1, 0),
)[0]
print "verts", verts
path = Path(verts, codes)
self.patch = patches.PathPatch(path, edgecolor="red", facecolor='orange', lw=2)
self.axes.add_patch(self.patch)
self.axes.legend( (self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line), ('Ts_min', 'Ts_max', 'Tl', 'Tp'), loc=2)
def draw_plot(self):
""" Redraws the plot
"""
self.axes.set_xbound(lower=0, upper=self.xmax)
self.axes.set_ybound(lower=0, upper=self.ymax)
self.axes.grid(True, color='gray')
pylab.setp(self.axes.get_xticklabels(), visible=True)
self.plot_data.set_xdata(np.arange(len(self.data)))
self.plot_data.set_ydata(np.array(self.data))
self.canvas.draw()
def update_config(self):
for ix, i in enumerate(self.profile):
i.SetValue(str(Arduino_Monitor.profile[i]))
def update_state(self):
if Arduino_Monitor.status[3] > 0:
self.started = True
self.time.SetValue(str(Arduino_Monitor.status[0]))
self.temperature.SetValue(str(Arduino_Monitor.status[1]))
self.pstate.SetValue(str(Arduino_Monitor.status[3]))
self.perror.SetValue(str(Arduino_Monitor.status[4]))
self.is_oven_heating.SetValue(str(Arduino_Monitor.status[5]))
def on_start_button(self, event):
self.started = self.datagen.send_start()
self.recv_config_button.Disable()
self.send_button.Disable()
self.profile = []
for i in range(30):
self.profile_sizer.Remove(i)
self.profile_sizer.Layout()
def on_recv_config_button(self, event):
if not self.started:
self.datagen.recv_config()
def on_send_button(self, event):
if not self.started:
self.datagen.send_config()
def on_save_plot(self, event):
file_choices = "PNG (*.png)|*.png"
dlg = wx.FileDialog(
self,
message="Save plot as...",
defaultDir=os.getcwd(),
defaultFile="plot.png",
wildcard=file_choices,
style=wx.SAVE)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
self.canvas.print_figure(path, dpi=self.dpi)
self.flash_status_message("Saved to %s" % path)
def on_redraw_timer(self, event):
if self.started:
self.data.append(self.datagen.next())
self.update_state()
self.draw_plot()
def on_exit(self, event):
self.Destroy()
def flash_status_message(self, msg, flash_len_ms=1500):
self.statusbar.SetStatusText(msg)
self.timeroff = wx.Timer(self)
self.Bind(
wx.EVT_TIMER,
self.on_flash_status_off,
self.timeroff)
self.timeroff.Start(flash_len_ms, oneShot=True)
def on_flash_status_off(self, event):
self.statusbar.SetStatusText('')
if __name__ == '__main__':
app = wx.PySimpleApp()
app.frame = GraphFrame()
app.frame.Show()
app.MainLoop()

View File

@ -65,12 +65,16 @@ class Plotter(FigureCanvas):
QtGui.QSizePolicy.Expanding) QtGui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self) FigureCanvas.updateGeometry(self)
timer = QtCore.QTimer(self)
QtCore.QObject.connect(timer, QtCore.SIGNAL("timeout()"), self.update_figure)
timer.start(1000)
self.updated = False self.updated = False
self.fig.canvas.mpl_connect('pick_event', self.onpick) 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): def onpick(self, event):
if isinstance(event.artist, Line2D): if isinstance(event.artist, Line2D):
@ -145,6 +149,8 @@ class Plotter(FigureCanvas):
def solder_changed(self): def solder_changed(self):
self.solder.setChanged() self.solder.setChanged()
self.updated = True self.updated = True
if not self.started:
self.update_figure()
def setData(self, solder): def setData(self, solder):
self.solder = solder self.solder = solder
@ -252,7 +258,7 @@ class ApplicationWindow(QtGui.QMainWindow):
QtCore.Qt.CTRL + QtCore.Qt.Key_D) QtCore.Qt.CTRL + QtCore.Qt.Key_D)
self.file_menu.addAction('S&ave plot', self.save_plot, self.file_menu.addAction('S&ave plot', self.save_plot,
QtCore.Qt.CTRL + QtCore.Qt.Key_A) QtCore.Qt.CTRL + QtCore.Qt.Key_A)
self.file_menu.addAction('&Quit', self.show_report, self.file_menu.addAction('&Show Report', self.show_report,
QtCore.Qt.CTRL + QtCore.Qt.Key_T) QtCore.Qt.CTRL + QtCore.Qt.Key_T)
self.file_menu.addAction('&Quit', self.fileQuit, self.file_menu.addAction('&Quit', self.fileQuit,
QtCore.Qt.CTRL + QtCore.Qt.Key_Q) QtCore.Qt.CTRL + QtCore.Qt.Key_Q)
@ -313,6 +319,8 @@ class ApplicationWindow(QtGui.QMainWindow):
QtCore.SIGNAL("solder_changed()"), QtCore.SIGNAL("solder_changed()"),
self.plotter.solder_changed) self.plotter.solder_changed)
self.temp_level_widget.temp_level_added.connect(self.constraint_widget.temp_level_added)
self.connect( self.connect(
self.constraint_widget, self.constraint_widget,
QtCore.SIGNAL("edge_changed()"), QtCore.SIGNAL("edge_changed()"),
@ -366,11 +374,11 @@ class ApplicationWindow(QtGui.QMainWindow):
self.setCentralWidget(self.splitter) self.setCentralWidget(self.splitter)
self.statusBar().showMessage("Reflow GORE!1!", 10000) self.statusBar().showMessage("Reflow GORE!1!", 10000)
self.plotter.solder_changed()
def getPlotter(self): def getPlotter(self):
return self.plotter return self.plotter
def solder_selected(self, index): def solder_selected(self, index):
if index.isValid(): if index.isValid():
solder = self.solder_widget.solder_model.solder_list[index.row()] solder = self.solder_widget.solder_model.solder_list[index.row()]
@ -382,6 +390,8 @@ class ApplicationWindow(QtGui.QMainWindow):
def edge_picked(self, ix): def edge_picked(self, ix):
if self.tab_widget.currentIndex() != 1: if self.tab_widget.currentIndex() != 1:
self.tab_widget.setCurrentIndex(1) self.tab_widget.setCurrentIndex(1)
if not self.plotter.started:
self.plotter.update_figure()
def remove_solder(self): def remove_solder(self):
self.solder_selected(self.solder_widget.remove_solder()) self.solder_selected(self.solder_widget.remove_solder())
@ -433,12 +443,6 @@ class ApplicationWindow(QtGui.QMainWindow):
def main(): def main():
# numpy bug workaround
try:
numpy.log10(0.0)
except:
pass
qApp = QtGui.QApplication(sys.argv) qApp = QtGui.QApplication(sys.argv)
aw = ApplicationWindow() aw = ApplicationWindow()

View File

@ -1,81 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import serial, struct, time
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=2)
buf = ""
alles = []
#def parse():
#buffer = list()
#while 1:
#try:
#i = ser.read(1)
#if ord(i) == 255:
#except Exception, e:
#print e
#else:
def recv_config():
ser.write(chr(255))
ser.flush()
read(30)
ser.flushInput()
data = struct.unpack("hhhhhhhhhhhhhhh", buf)
print
print "Profile:"
print "ts_min:", data[0]
print "ts_max:", data[1]
print "tl:", data[2]
print "tp:", data[3]
print "time_max:", data[4]
print "ramp_up_min:", data[5]
print "ramp_up_max:", data[6]
print "ramp_down_min:", data[7]
print "ramp_down_max:", data[8]
print "ts_duration_min:", data[9]
print "ts_duration_max:", data[10]
print "tl_duration_min:", data[11]
print "tl_duration_max:", data[12]
print "tp_duration_min:", data[13]
print "tp_duration_max:", data[14]
print
def recv_state():
ser.write(chr(254))
ser.flush()
read(11)
data = struct.unpack("hhhhhb", buf)
print "time: %ds, temperature: %d°C, last temperature: %d°C, state: %d, error condition: %d, heating: %d" % data
def send_config():
ser.write(chr(253))
ser.write(buf)
ser.flushInput()
def read(l):
global buf
global alles
buf = ""
while len(buf) < l:
try:
buf += ser.read(l)
alles.append(buf)
except Exception, e:
print e
ser.flushInput()
time.sleep(2)
recv_config()
while 1:
recv_state()
time.sleep(1)

259
reflowctl/solder.py Normal file
View File

@ -0,0 +1,259 @@
import os
import os.path
import xml.etree.ElementTree as etree
from PyQt4 import QtGui, QtCore
from numpy import arange, sin, pi, array, linspace, arange
from temp_level import TempLevel, set_colors
from edge_widget import Edge
from control_widgets import AddRemoveWidget
def getTemperature():
return 20.
class Solder(QtCore.QObject):
log_message = QtCore.pyqtSignal(str)
def __init__(self, filename, name=str(), description=str(), parent=None):
super(Solder, self).__init__(parent)
self.changed = False
self.filename = filename
self.name = name
self.description = description
self.temp_levels = list()
self.edges = 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 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):
print self.calc_profile
self.log = list()
x = list()
y = list()
duration_points = dict()
rate_points = dict()
time = 0
def calc(edge):
if edge.duration:
return time + edge.duration
elif edge.rate:
return time + (edge.to_tl.temp - edge.from_tl.temp) / edge.rate
for _edge in self.edges:
x.append(time)
y.append(_edge.from_tl.temp)
time = calc(_edge)
x.append(time)
y.append(_edge.to_tl.temp)
print "end", self.calc_profile
return array(map(float, x)), array(map(float, y)), min(x), max(x), min(y), max(y), set(), set()
@staticmethod
def unpack(filename, parent):
xmltree = etree.parse(filename)
root = xmltree.getroot()
solder_node = root[0]
s = Solder(filename, solder_node.attrib["name"], solder_node.attrib["description"], parent)
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)
set_colors(s.temp_levels)
for edge in solder_node.findall("edge"):
from_tl = s.get_temp_level_by_name(edge.attrib["from"])
to_tl = s.get_temp_level_by_name(edge.attrib["to"])
duration = None
rate = None
try:
duration = float(edge.attrib["duration"])
except ValueError:
pass
try:
rate = float(edge.attrib["rate"])
except ValueError:
pass
e = Edge(from_tl, to_tl, duration, rate)
s.edges.append(e)
return s
def save(self):
if self.changed:
solder_node = etree.Element("solder_type", {"name" : self.name, "description" : self.description})
for temp_level in self.temp_levels:
temp = temp_level.is_env and "$ENV" or str(temp_level.temp)
solder_node.append(etree.Element("state", {"name" : temp_level.name, "temperature" : temp}))
for edge in self.edges:
element = etree.Element("edge", {
"from" : edge.from_tl.name,
"to" : edge.to_tl.name,
"duration" : str(edge.duration),
"rate" : str(edge.rate)})
solder_node.append(element)
dirname = SolderListModel.dirname()
root = etree.Element("xml")
root.append(solder_node)
if self.filename is None:
self.filename = os.path.join(dirname, self.name + ".xml")
etree.ElementTree(root).write(self.filename, "UTF-8", True)
self.changed = False
def setChanged(self):
self.changed = True
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._load_solder_list()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.solder_list)
def _load_solder_list(self):
dirname = self.dirname()
dirlisting = filter(lambda x: os.path.splitext(x)[1] == ".xml", os.listdir(dirname))
self.solder_list = []
for p in dirlisting:
self.solder_list.append(Solder.unpack(os.path.join(dirname, p), self))
self.solder_list.sort(key=lambda x: x.name)
self.reset()
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 not index.isValid():
return QtCore.QVariant()
solder = self.solder_list[index.row()]
if role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(solder.name)
elif role == QtCore.Qt.DecorationRole and solder.changed:
return QtGui.QIcon.fromTheme("document-save")
def setData(self, index, variant, role):
if index.isValid() and role == QtCore.Qt.EditRole:
new_name = str(variant.toString())
if new_name and new_name != "":
self.solder_list[index.row()].name = new_name
self.solder_list[index.row()].changed = True
return True
return False
def flags(self, index):
if not index.isValid():
return 0
return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
def create_solder(self):
solder = Solder(None, datetime.now().strftime("%Y-%M-%D %H:%m:%s"), "")
tl = solder.add_temp_level("environment temp", getTemperature(), True)
tl.color = QtGui.QColor(0, 0, 0)
self.solder_list.append(solder)
self.reset()
@staticmethod
def dirname():
return os.path.join(os.path.dirname(__file__), "solder_types")
def check_name(self, name):
for solder in self.solder_list:
if name == solder.name:
return False
return True
class SolderWidget(QtGui.QWidget):
def __init__(self, parent, readonly=False):
super(SolderWidget, self).__init__(parent)
self.solder_model = SolderListModel(self)
self.solder_view = QtGui.QListView()
self.solder_view.setModel(self.solder_model)
self.solder_controls = AddRemoveWidget(self)
layout = QtGui.QHBoxLayout(self)
layout.addWidget(self.solder_view, 3)
layout.addWidget(self.solder_controls, 1)
self.connect(
self.solder_controls.add_button,
QtCore.SIGNAL("clicked()"),
self.solder_model.create_solder)
self.connect(
self.solder_controls.remove_button,
QtCore.SIGNAL("clicked()"),
self.remove_solder)
self.solder_view.setCurrentIndex(self.solder_model.index(0,0))
def remove_solder(self):
index = self.solder_view.currentIndex()
solder = self.solder_model.solder_list[index.row()]
try:
os.remove(solder.filename)
except OSError:
pass
del self.solder_model.solder_list[index.row()]
self.solder_model.reset()
new_index = self.solder_model.index(0)
self.solder_view.setCurrentIndex(new_index)
return new_index
def save_solder(self, index):
self.solder_model.solder_list[index.row()].save()
self.solder_model.reset()
new_index = self.solder_model.index(self.solder_model.solder_list.index(self.plotter.solder))
self.solder_widget.solder_view.setCurrentIndex(new_index)

278
reflowctl/temp_level.py Normal file
View File

@ -0,0 +1,278 @@
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
from control_widgets import AddRemoveWidget
def calc_colors(count):
if count == 1:
return [QtGui.QColor(0, 255, 0),]
r = 0
g = 255
step = int(512. / (count-1))
colors = list()
for i in range(count):
colors.append(QtGui.QColor(r, g, 0))
if r < 255:
r += step
if r > 255:
g -= (r - 256)
r = 255
g = max(0, g)
else:
g -= step
g = max(0, g)
return colors
def set_colors(temp_levels):
colors = calc_colors(len(temp_levels) - 1)
ix = 0
for temp_level in temp_levels:
if not temp_level.is_env:
temp_level.color = colors[ix]
ix += 1
else:
temp_level.color = QtGui.QColor("black")
class TempLevel(QtCore.QObject):
def __init__(self, name, temp, is_env=False):
super(TempLevel, self).__init__()
self.name = name
self.temp = temp
self.is_env = is_env
self.color = None
def __str__(self):
return "<TempLevel: name=%r, temp=%r>" % (self.name, self.temp)
def __lt__(self, other):
return self.temp < other.temp
def __le__(self, other):
return self.temp <= other.temp
def __eq__(self, other):
return self.temp == other.temp
def is_between(self, tl1, tl2):
tmp = [tl1, tl2, self]
tmp.sort()
return self == tmp[1]
def set_next(self, temp_level):
if temp_level is self:
raise ValueError("same temp_level")
self.next = temp_level
def __repr__(self):
return "TempLevel(%r, %r, %r)" % (self.name, self.temp, self.is_env)
class TempLevelModel(QtCore.QAbstractTableModel):
solder_changed = QtCore.pyqtSignal()
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
if index.row() != 0:
return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
else:
return QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
def neigbors(self, temp_level):
ix = self.temp_levels.index(temp_level)
return self.temp_levels[ix-1], self.temp_levels[ix+1]
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:
temp, res = variant.toInt()
tl = self.temp_levels[index.row()]
try:
tl0 = self.temp_levels[index.row() - 1]
if tl0.temp >= temp:
return False
except Exception, e:
pass
try:
tl1 = self.temp_levels[index.row() + 1]
if tl1.temp <= temp:
return False
except Exception, e:
pass
tl.temp = temp
self.solder_changed.emit()
return True
return False
def remove_temp_level(self, index):
tmp = self.temp_levels[index]
del self.temp_levels[index]
self.reset()
self.solder_changed.emit()
return tmp
def add_temp_level(self, index, temp_level):
self.temp_levels.insert(index.row() + 1, temp_level)
set_colors(self.temp_levels)
self.reset()
self.solder_changed.emit()
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()
def get_temp_level(self, ix):
return self.temp_levels[ix]
class TempLevelWidget(QtGui.QWidget):
temp_level_removed = QtCore.pyqtSignal(TempLevel)
temp_level_added = QtCore.pyqtSignal(TempLevel, TempLevel)
temp_levels_changed = QtCore.pyqtSignal()
solder_changed = QtCore.pyqtSignal()
def __init__(self, parent, readonly=False):
super(TempLevelWidget, self).__init__(parent)
self.readonly = readonly
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)
h = QtGui.QHBoxLayout()
h.addWidget(self.temp_level_view)
self.setLayout(h)
self.connect(
self.temp_level_view,
QtCore.SIGNAL("clicked(QModelIndex)"),
self.temp_level_selected)
if not readonly:
self.controls = AddRemoveWidget(self)
h.addWidget(self.controls)
self.connect(
self.controls.add_button,
QtCore.SIGNAL("clicked()"),
self.add_temp_level)
self.connect(
self.controls.remove_button,
QtCore.SIGNAL("clicked()"),
self.remove_temp_level)
self.connect(
self.temp_level_model,
QtCore.SIGNAL("solder_changed()"),
self._solder_changed)
def _solder_changed(self):
self.solder_changed.emit()
def setData(self, solder):
self.temp_level_model.setTempLevels(solder.temp_levels)
self.temp_level_view.resizeColumnsToContents()
self.temp_level_view.setCurrentIndex(self.temp_level_model.index(0, 0))
def add_temp_level(self):
index = self.temp_level_view.currentIndex()
old_tl = self.temp_level_model.temp_levels[index.row()]
print "next temp", self.temp_level_model.temp_levels[index.row() + 1].temp
new_temp = old_tl.temp + (self.temp_level_model.temp_levels[index.row() + 1].temp - old_tl.temp) / 2
print "new_temp", new_temp
new_tl = TempLevel("new " + str(self.temp_level_model.rowCount(None)), new_temp)
self.temp_level_model.add_temp_level(index, new_tl)
self.temp_level_view.setCurrentIndex(self.temp_level_model.index(index.row() + 1, 0))
self.temp_levels_changed.emit()
print "TempLevelWidget.add_temp_level 1", old_tl, new_tl
self.temp_level_added.emit(old_tl, new_tl)
print "TempLevelWidget.add_temp_level 2"
def remove_temp_level(self):
self.temp_level_removed.emit(
self.temp_level_model.remove_temp_level(
self.temp_level_view.currentIndex().row()))
self.solder_changed.emit()
def temp_level_selected(self, index):
if index.isValid():
row = index.row()
if not self.readonly:
self.controls.remove_button.setEnabled(not self.temp_level_model.temp_levels[row].is_env)