]> sigrok.org Git - sigrok-meter.git/blame - mainwindow.py
Remove the thread used for sampling.
[sigrok-meter.git] / mainwindow.py
CommitLineData
48723bbb
JS
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
2e8c2e6e 22import acquisition
48723bbb 23import datamodel
f76b9df8 24import multiplotwidget
48723bbb
JS
25import os.path
26import qtcompat
48723bbb 27import textwrap
f76b9df8
JS
28import time
29import util
48723bbb
JS
30
31QtCore = qtcompat.QtCore
32QtGui = qtcompat.QtGui
f76b9df8 33pyqtgraph = qtcompat.pyqtgraph
48723bbb
JS
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
f76b9df8 55 # Number of seconds that the plots display.
3010b5a0 56 BACKLOG = 30
f76b9df8
JS
57
58 # Update interval of the plots in milliseconds.
59 UPDATEINTERVAL = 100
60
48723bbb
JS
61 def __init__(self, context, drivers):
62 super(self.__class__, self).__init__()
63
2e8c2e6e
JS
64 # Used to coordinate the stopping of the acquisition and
65 # the closing of the window.
66 self._closing = False
67
48723bbb 68 self.context = context
2e8c2e6e 69 self.drivers = drivers
48723bbb
JS
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
2e8c2e6e
JS
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()
48723bbb
JS
93
94 def setup_ui(self):
95 self.setWindowTitle('sigrok-meter')
480cdb7b 96 # Resizing the listView below will increase this again.
0e810ddf 97 self.resize(350, 10)
48723bbb
JS
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
f76b9df8 133 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
d0aa45b4 134 self.plotwidget.plotHidden.connect(self._on_plotHidden)
f76b9df8
JS
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)
48723bbb 148 self.centralWidget().setContentsMargins(0, 0, 0, 0)
f76b9df8
JS
149 self.resize(800, 500)
150
151 self.startTimer(MainWindow.UPDATEINTERVAL)
152
2e8c2e6e
JS
153 def stop(self):
154 self.acquisition.stop()
155 print(self.acquisition.is_running())
156
f76b9df8
JS
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
3010b5a0
JS
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 )
f76b9df8
JS
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
d0aa45b4 199 self._updatePlots()
f76b9df8 200
d0aa45b4
JS
201 def _updatePlots(self):
202 '''Updates all plots.'''
f76b9df8 203
d0aa45b4
JS
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)
1879265a 209 deviceID = tuple(deviceID) # PySide returns a list.
d0aa45b4
JS
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)
f76b9df8 247
d0aa45b4
JS
248 for traceunit, trace in traces.items():
249 if traceunit == plotunit:
250 trace.new = False
48723bbb 251
2e8c2e6e
JS
252 @QtCore.Slot()
253 def _stopped(self):
254 if self._closing:
255 self.close()
256
48723bbb 257 def closeEvent(self, event):
2e8c2e6e
JS
258 if self.acquisition.is_running():
259 self._closing = True
260 self.acquisition.stop()
261 event.ignore()
262 else:
263 event.accept()
48723bbb
JS
264
265 @QtCore.Slot()
266 def show_about(self):
267 text = textwrap.dedent('''\
268 <div align="center">
480cdb7b
UH
269 <b>sigrok-meter 0.1.0</b><br/><br/>
270 Using libsigrok {} (lib version {}).<br/><br/>
48723bbb
JS
271 <a href='http://www.sigrok.org'>
272 http://www.sigrok.org</a><br/>
273 <br/>
480cdb7b
UH
274 License: GNU GPL, version 3 or later<br/>
275 <br/>
48723bbb
JS
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
48723bbb
JS
285 @QtCore.Slot(object, int, int)
286 def modelRowsInserted(self, parent, start, end):
480cdb7b 287 '''Resize the list view to the size of the content.'''
48723bbb
JS
288 rows = self.model.rowCount()
289 dh = self.delegate.sizeHint().height()
290 self.listView.setMinimumHeight(dh * rows)