]> sigrok.org Git - sigrok-meter.git/blame_incremental - mainwindow.py
Remove the thread used for sampling.
[sigrok-meter.git] / mainwindow.py
... / ...
CommitLineData
1##
2## This file is part of the sigrok-meter project.
3##
4## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
5## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
6##
7## This program is free software; you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation; either version 2 of the License, or
10## (at your option) any later version.
11##
12## This program is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with this program; if not, write to the Free Software
19## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20##
21
22import acquisition
23import datamodel
24import multiplotwidget
25import os.path
26import qtcompat
27import textwrap
28import time
29import util
30
31QtCore = qtcompat.QtCore
32QtGui = qtcompat.QtGui
33pyqtgraph = qtcompat.pyqtgraph
34
35class EmptyMessageListView(QtGui.QListView):
36 '''List view that shows a message if the model im empty.'''
37
38 def __init__(self, message, parent=None):
39 super(self.__class__, self).__init__(parent)
40
41 self._message = message
42
43 def paintEvent(self, event):
44 m = self.model()
45 if m and m.rowCount():
46 super(self.__class__, self).paintEvent(event)
47 return
48
49 painter = QtGui.QPainter(self.viewport())
50 painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
51
52class MainWindow(QtGui.QMainWindow):
53 '''The main window of the application.'''
54
55 # Number of seconds that the plots display.
56 BACKLOG = 30
57
58 # Update interval of the plots in milliseconds.
59 UPDATEINTERVAL = 100
60
61 def __init__(self, context, drivers):
62 super(self.__class__, self).__init__()
63
64 # Used to coordinate the stopping of the acquisition and
65 # the closing of the window.
66 self._closing = False
67
68 self.context = context
69 self.drivers = drivers
70
71 self.delegate = datamodel.MultimeterDelegate(self, self.font())
72 self.model = datamodel.MeasurementDataModel(self)
73 self.model.rowsInserted.connect(self.modelRowsInserted)
74
75 self.setup_ui()
76
77 QtCore.QTimer.singleShot(0, self._start_acquisition)
78
79 def _start_acquisition(self):
80 self.acquisition = acquisition.Acquisition(self.context)
81 self.acquisition.measured.connect(self.model.update)
82 self.acquisition.stopped.connect(self._stopped)
83
84 try:
85 for (ds, cs) in self.drivers:
86 self.acquisition.add_device(ds, cs)
87 except Exception as e:
88 QtGui.QMessageBox.critical(self, 'Error', str(e))
89 self.close()
90 return
91
92 self.acquisition.start()
93
94 def setup_ui(self):
95 self.setWindowTitle('sigrok-meter')
96 # Resizing the listView below will increase this again.
97 self.resize(350, 10)
98
99 p = os.path.abspath(os.path.dirname(__file__))
100 p = os.path.join(p, 'sigrok-logo-notext.png')
101 self.setWindowIcon(QtGui.QIcon(p))
102
103 actionQuit = QtGui.QAction(self)
104 actionQuit.setText('&Quit')
105 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
106 actionQuit.setShortcut('Ctrl+Q')
107 actionQuit.triggered.connect(self.close)
108
109 actionAbout = QtGui.QAction(self)
110 actionAbout.setText('&About')
111 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
112 actionAbout.triggered.connect(self.show_about)
113
114 menubar = self.menuBar()
115 menuFile = menubar.addMenu('&File')
116 menuFile.addAction(actionQuit)
117 menuHelp = menubar.addMenu('&Help')
118 menuHelp.addAction(actionAbout)
119
120 self.listView = EmptyMessageListView('waiting for data...')
121 self.listView.setFrameShape(QtGui.QFrame.NoFrame)
122 self.listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
123 self.listView.viewport().setAutoFillBackground(True)
124 self.listView.setMinimumWidth(260)
125 self.listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
126 self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
127 self.listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
128 self.listView.setItemDelegate(self.delegate)
129 self.listView.setModel(self.model)
130 self.listView.setUniformItemSizes(True)
131 self.listView.setMinimumSize(self.delegate.sizeHint())
132
133 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
134 self.plotwidget.plotHidden.connect(self._on_plotHidden)
135
136 # Maps from 'unit' to the corresponding plot.
137 self._plots = {}
138 # Maps from '(plot, device)' to the corresponding curve.
139 self._curves = {}
140
141 self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
142 self.splitter.addWidget(self.listView)
143 self.splitter.addWidget(self.plotwidget)
144 self.splitter.setStretchFactor(0, 0)
145 self.splitter.setStretchFactor(1, 1)
146
147 self.setCentralWidget(self.splitter)
148 self.centralWidget().setContentsMargins(0, 0, 0, 0)
149 self.resize(800, 500)
150
151 self.startTimer(MainWindow.UPDATEINTERVAL)
152
153 def stop(self):
154 self.acquisition.stop()
155 print(self.acquisition.is_running())
156
157 def _getPlot(self, unit):
158 '''Looks up or creates a new plot for 'unit'.'''
159
160 if unit in self._plots:
161 return self._plots[unit]
162
163 # create a new plot for the unit
164 plot = self.plotwidget.addPlot()
165 plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
166 plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
167 plot.view.setYRange(-1, 1)
168 plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
169 # lock to the range calculated by the view using additional padding,
170 # looks nicer this way
171 r = plot.view.viewRange()
172 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
173
174 self._plots[unit] = plot
175 return plot
176
177 def _getCurve(self, plot, deviceID):
178 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
179
180 key = (id(plot), deviceID)
181 if key in self._curves:
182 return self._curves[key]
183
184 # create a new curve
185 curve = pyqtgraph.PlotDataItem(
186 antialias=True,
187 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
188 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
189 symbolSize=1
190 )
191 plot.view.addItem(curve)
192
193 self._curves[key] = curve
194 return curve
195
196 def timerEvent(self, event):
197 '''Periodically updates all graphs.'''
198
199 self._updatePlots()
200
201 def _updatePlots(self):
202 '''Updates all plots.'''
203
204 # loop over all devices and channels
205 for row in range(self.model.rowCount()):
206 idx = self.model.index(row, 0)
207 deviceID = self.model.data(idx,
208 datamodel.MeasurementDataModel.idRole)
209 deviceID = tuple(deviceID) # PySide returns a list.
210 traces = self.model.data(idx,
211 datamodel.MeasurementDataModel.tracesRole)
212
213 for unit, trace in traces.items():
214 now = time.time()
215
216 # remove old samples
217 l = now - MainWindow.BACKLOG
218 while trace.samples and trace.samples[0][0] < l:
219 trace.samples.pop(0)
220
221 plot = self._getPlot(unit)
222 if not plot.visible:
223 if trace.new:
224 self.plotwidget.showPlot(plot)
225
226 if plot.visible:
227 xdata = [s[0] - now for s in trace.samples]
228 ydata = [s[1] for s in trace.samples]
229
230 color = self.model.data(idx,
231 datamodel.MeasurementDataModel.colorRole)
232
233 curve = self._getCurve(plot, deviceID)
234 curve.setPen(pyqtgraph.mkPen(color=color))
235 curve.setData(xdata, ydata)
236
237 @QtCore.Slot(multiplotwidget.Plot)
238 def _on_plotHidden(self, plot):
239 plotunit = [u for u, p in self._plots.items() if p == plot][0]
240
241 # Mark all traces of all devices/channels with the same unit as the
242 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
243 # on one trace, the plot will be shown again
244 for row in range(self.model.rowCount()):
245 idx = self.model.index(row, 0)
246 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
247
248 for traceunit, trace in traces.items():
249 if traceunit == plotunit:
250 trace.new = False
251
252 @QtCore.Slot()
253 def _stopped(self):
254 if self._closing:
255 self.close()
256
257 def closeEvent(self, event):
258 if self.acquisition.is_running():
259 self._closing = True
260 self.acquisition.stop()
261 event.ignore()
262 else:
263 event.accept()
264
265 @QtCore.Slot()
266 def show_about(self):
267 text = textwrap.dedent('''\
268 <div align="center">
269 <b>sigrok-meter 0.1.0</b><br/><br/>
270 Using libsigrok {} (lib version {}).<br/><br/>
271 <a href='http://www.sigrok.org'>
272 http://www.sigrok.org</a><br/>
273 <br/>
274 License: GNU GPL, version 3 or later<br/>
275 <br/>
276 This program comes with ABSOLUTELY NO WARRANTY;<br/>
277 for details visit
278 <a href='http://www.gnu.org/licenses/gpl.html'>
279 http://www.gnu.org/licenses/gpl.html</a>
280 </div>
281 '''.format(self.context.package_version, self.context.lib_version))
282
283 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
284
285 @QtCore.Slot(object, int, int)
286 def modelRowsInserted(self, parent, start, end):
287 '''Resize the list view to the size of the content.'''
288 rows = self.model.rowCount()
289 dh = self.delegate.sizeHint().height()
290 self.listView.setMinimumHeight(dh * rows)