]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
058fe5fb54e7edb589999d11ef058e499ad3874b
[sigrok-meter.git] / mainwindow.py
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
22 import acquisition
23 import datamodel
24 import icons
25 import multiplotwidget
26 import os.path
27 import qtcompat
28 import textwrap
29 import time
30 import util
31
32 QtCore = qtcompat.QtCore
33 QtGui = qtcompat.QtGui
34 pyqtgraph = qtcompat.pyqtgraph
35
36 class EmptyMessageListView(QtGui.QListView):
37     '''List view that shows a message if the model im empty.'''
38
39     def __init__(self, message, parent=None):
40         super(self.__class__, self).__init__(parent)
41
42         self._message = message
43
44     def paintEvent(self, event):
45         m = self.model()
46         if m and m.rowCount():
47             super(self.__class__, self).paintEvent(event)
48             return
49
50         painter = QtGui.QPainter(self.viewport())
51         painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
52
53 class MainWindow(QtGui.QMainWindow):
54     '''The main window of the application.'''
55
56     # Number of seconds that the plots display.
57     BACKLOG = 30
58
59     # Update interval of the plots in milliseconds.
60     UPDATEINTERVAL = 100
61
62     def __init__(self, context, drivers):
63         super(self.__class__, self).__init__()
64
65         # Used to coordinate the stopping of the acquisition and
66         # the closing of the window.
67         self._closing = False
68
69         self.context = context
70         self.drivers = drivers
71
72         self.delegate = datamodel.MultimeterDelegate(self, self.font())
73         self.model = datamodel.MeasurementDataModel(self)
74
75         # Maps from 'unit' to the corresponding plot.
76         self._plots = {}
77         # Maps from '(plot, device)' to the corresponding curve.
78         self._curves = {}
79
80         self._setup_ui()
81
82         self._plot_update_timer = QtCore.QTimer()
83         self._plot_update_timer.setInterval(MainWindow.UPDATEINTERVAL)
84         self._plot_update_timer.timeout.connect(self._updatePlots)
85
86         QtCore.QTimer.singleShot(0, self._start_acquisition)
87
88     def _start_acquisition(self):
89         self.acquisition = acquisition.Acquisition(self.context)
90         self.acquisition.measured.connect(self.model.update)
91         self.acquisition.stopped.connect(self._stopped)
92
93         try:
94             for (ds, cs) in self.drivers:
95                 self.acquisition.add_device(ds, cs)
96         except Exception as e:
97             QtGui.QMessageBox.critical(self, 'Error', str(e))
98             self.close()
99             return
100
101         self.start_stop_acquisition()
102
103     def _setup_ui(self):
104         self.setWindowTitle('sigrok-meter')
105         # Resizing the listView below will increase this again.
106         self.resize(350, 10)
107
108         self.setWindowIcon(QtGui.QIcon(':/logo.png'))
109
110         self._setup_graphPage()
111         self._setup_addDevicePage()
112         self._setup_logPage()
113         self._setup_preferencesPage()
114
115         self._pages = [
116             self.graphPage,
117             self.addDevicePage,
118             self.logPage,
119             self.preferencesPage
120         ]
121
122         self.stackedWidget = QtGui.QStackedWidget(self)
123         for page in self._pages:
124             self.stackedWidget.addWidget(page)
125
126         self._setup_sidebar()
127
128         self.setCentralWidget(QtGui.QWidget())
129         self.centralWidget().setContentsMargins(0, 0, 0, 0)
130
131         layout = QtGui.QHBoxLayout(self.centralWidget())
132         layout.addWidget(self.sideBar)
133         layout.addWidget(self.stackedWidget)
134         layout.setSpacing(0)
135         layout.setContentsMargins(0, 0, 0, 0)
136
137         self.resize(900, 550)
138
139     def _setup_sidebar(self):
140         self.sideBar = QtGui.QToolBar(self)
141         self.sideBar.setOrientation(QtCore.Qt.Vertical)
142
143         actionGraph = self.sideBar.addAction('Instantaneous Values and Graphs')
144         actionGraph.setCheckable(True)
145         actionGraph.setIcon(icons.graph)
146         actionGraph.triggered.connect(self.showGraphPage)
147
148         #actionAdd = self.sideBar.addAction('Add Device')
149         #actionAdd.setCheckable(True)
150         #actionAdd.setIcon(icons.add)
151         #actionAdd.triggered.connect(self.showAddDevicePage)
152
153         #actionLog = self.sideBar.addAction('Logs')
154         #actionLog.setCheckable(True)
155         #actionLog.setIcon(icons.log)
156         #actionLog.triggered.connect(self.showLogPage)
157
158         #actionPreferences = self.sideBar.addAction('Preferences')
159         #actionPreferences.setCheckable(True)
160         #actionPreferences.setIcon(icons.preferences)
161         #actionPreferences.triggered.connect(self.showPreferencesPage)
162
163         # make the buttons at the top exclusive
164         self.actionGroup = QtGui.QActionGroup(self)
165         self.actionGroup.addAction(actionGraph)
166         #self.actionGroup.addAction(actionAdd)
167         #self.actionGroup.addAction(actionLog)
168         #self.actionGroup.addAction(actionPreferences)
169
170         # show graph at startup
171         actionGraph.setChecked(True)
172
173         # fill space between buttons on the top and on the bottom
174         fill = QtGui.QWidget(self)
175         fill.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
176         self.sideBar.addWidget(fill)
177
178         self.actionStartStop = self.sideBar.addAction('Start Acquisition')
179         self.actionStartStop.setIcon(icons.start)
180         self.actionStartStop.triggered.connect(self.start_stop_acquisition)
181
182         actionAbout = self.sideBar.addAction('About')
183         actionAbout.setIcon(icons.about)
184         actionAbout.triggered.connect(self.show_about)
185
186         actionQuit = self.sideBar.addAction('Quit')
187         actionQuit.setIcon(icons.exit)
188         actionQuit.triggered.connect(self.close)
189
190         s = self.style().pixelMetric(QtGui.QStyle.PM_LargeIconSize)
191         self.sideBar.setIconSize(QtCore.QSize(s, s))
192
193         self.sideBar.setStyleSheet('''
194             QToolBar {
195                 background-color: white;
196                 margin: 0px;
197                 border: 0px;
198                 border-right: 1px solid black;
199             }
200
201             QToolButton {
202                 padding: 10px;
203                 border: 0px;
204                 border-right: 1px solid black;
205             }
206
207             QToolButton:checked,
208             QToolButton[checkable="false"]:hover {
209                 background-color: #c0d0e8;
210             }
211         ''')
212
213     def _setup_graphPage(self):
214         listView = EmptyMessageListView('waiting for data...')
215         listView.setFrameShape(QtGui.QFrame.NoFrame)
216         listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
217         listView.viewport().setAutoFillBackground(True)
218         listView.setMinimumWidth(260)
219         listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
220         listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
221         listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
222         listView.setItemDelegate(self.delegate)
223         listView.setModel(self.model)
224         listView.setUniformItemSizes(True)
225         listView.setMinimumSize(self.delegate.sizeHint())
226
227         self.plotwidget = multiplotwidget.MultiPlotWidget(self)
228         self.plotwidget.plotHidden.connect(self._on_plotHidden)
229
230         self.graphPage = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
231         self.graphPage.addWidget(listView)
232         self.graphPage.addWidget(self.plotwidget)
233         self.graphPage.setStretchFactor(0, 0)
234         self.graphPage.setStretchFactor(1, 1)
235
236     def _setup_addDevicePage(self):
237         self.addDevicePage = QtGui.QWidget(self)
238         layout = QtGui.QVBoxLayout(self.addDevicePage)
239         label = QtGui.QLabel('add device page')
240         layout.addWidget(label)
241
242     def _setup_logPage(self):
243         self.logPage = QtGui.QWidget(self)
244         layout = QtGui.QVBoxLayout(self.logPage)
245         label = QtGui.QLabel('log page')
246         layout.addWidget(label)
247
248     def _setup_preferencesPage(self):
249         self.preferencesPage = QtGui.QWidget(self)
250         layout = QtGui.QVBoxLayout(self.preferencesPage)
251         label = QtGui.QLabel('preferences page')
252         layout.addWidget(label)
253
254     def showPage(self, page):
255         self.stackedWidget.setCurrentIndex(self._pages.index(page))
256
257     @QtCore.Slot(bool)
258     def showGraphPage(self):
259         self.showPage(self.graphPage)
260
261     @QtCore.Slot(bool)
262     def showAddDevicePage(self):
263         self.showPage(self.addDevicePage)
264
265     @QtCore.Slot(bool)
266     def showLogPage(self):
267         self.showPage(self.logPage)
268
269     @QtCore.Slot(bool)
270     def showPreferencesPage(self):
271         self.showPage(self.preferencesPage)
272
273     def _getPlot(self, unit):
274         '''Looks up or creates a new plot for 'unit'.'''
275
276         if unit in self._plots:
277             return self._plots[unit]
278
279         # create a new plot for the unit
280         plot = self.plotwidget.addPlot()
281         plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
282         plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
283         plot.view.setYRange(-1, 1)
284         plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
285         # lock to the range calculated by the view using additional padding,
286         # looks nicer this way
287         r = plot.view.viewRange()
288         plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
289
290         self._plots[unit] = plot
291         return plot
292
293     def _getCurve(self, plot, deviceID):
294         '''Looks up or creates a new curve for '(plot, deviceID)'.'''
295
296         key = (plot, deviceID)
297         if key in self._curves:
298             return self._curves[key]
299
300         # create a new curve
301         curve = pyqtgraph.PlotDataItem(
302             antialias=True,
303             symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
304             symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
305             symbolSize=1
306         )
307         plot.view.addItem(curve)
308
309         self._curves[key] = curve
310         return curve
311
312     def _updatePlots(self):
313         '''Updates all plots.'''
314
315         # loop over all devices and channels
316         for row in range(self.model.rowCount()):
317             idx = self.model.index(row, 0)
318             deviceID = self.model.data(idx,
319                             datamodel.MeasurementDataModel.idRole)
320             deviceID = tuple(deviceID) # PySide returns a list.
321             traces = self.model.data(idx,
322                             datamodel.MeasurementDataModel.tracesRole)
323
324             for unit, trace in traces.items():
325                 now = time.time()
326
327                 # remove old samples
328                 l = now - MainWindow.BACKLOG
329                 while trace.samples and trace.samples[0][0] < l:
330                     trace.samples.pop(0)
331
332                 plot = self._getPlot(unit)
333                 if not plot.visible:
334                     if trace.new:
335                         self.plotwidget.showPlot(plot)
336
337                 if plot.visible:
338                     xdata = [s[0] - now for s in trace.samples]
339                     ydata = [s[1]       for s in trace.samples]
340
341                     color = self.model.data(idx,
342                                 datamodel.MeasurementDataModel.colorRole)
343
344                     curve = self._getCurve(plot, deviceID)
345                     curve.setPen(pyqtgraph.mkPen(color=color))
346                     curve.setData(xdata, ydata)
347
348     @QtCore.Slot(multiplotwidget.Plot)
349     def _on_plotHidden(self, plot):
350         plotunit = [u for u, p in self._plots.items() if p == plot][0]
351
352         # Mark all traces of all devices/channels with the same unit as the
353         # plot as "old" ('trace.new = False'). As soon as a new sample arrives
354         # on one trace, the plot will be shown again
355         for row in range(self.model.rowCount()):
356             idx = self.model.index(row, 0)
357             traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
358
359             for traceunit, trace in traces.items():
360                 if traceunit == plotunit:
361                     trace.new = False
362
363     @QtCore.Slot()
364     def _stopped(self):
365         if self._closing:
366             # The acquisition was stopped by the 'closeEvent()', close the
367             # window again now that the acquisition has stopped.
368             self.close()
369
370     def closeEvent(self, event):
371         if self.acquisition.is_running():
372             # Stop the acquisition before closing the window.
373             self._closing = True
374             self.start_stop_acquisition()
375             event.ignore()
376         else:
377             event.accept()
378
379     @QtCore.Slot()
380     def start_stop_acquisition(self):
381         if self.acquisition.is_running():
382             self.acquisition.stop()
383             self._plot_update_timer.stop()
384             self.actionStartStop.setText('Start Acquisition')
385             self.actionStartStop.setIcon(icons.start)
386         else:
387             # before starting (again), remove all old samples and old curves
388             self.model.clear_samples()
389
390             for key in self._curves:
391                 plot, _ = key
392                 curve = self._curves[key]
393                 plot.view.removeItem(curve)
394             self._curves = {}
395
396             self.acquisition.start()
397             self._plot_update_timer.start()
398             self.actionStartStop.setText('Stop Acquisition')
399             self.actionStartStop.setIcon(icons.stop)
400
401     @QtCore.Slot()
402     def show_about(self):
403         text = textwrap.dedent('''\
404             <div align="center">
405                 <b>sigrok-meter 0.1.0</b><br/><br/>
406                 Using libsigrok {} (lib version {}).<br/><br/>
407                 <a href='http://www.sigrok.org'>
408                          http://www.sigrok.org</a><br/>
409                 <br/>
410                 License: GNU GPL, version 3 or later<br/>
411                 <br/>
412                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
413                 for details visit
414                 <a href='http://www.gnu.org/licenses/gpl.html'>
415                          http://www.gnu.org/licenses/gpl.html</a><br/>
416                 <br/>
417                 Some icons by <a href='https://www.gnome.org'>
418                               the GNOME project</a>
419             </div>
420         '''.format(self.context.package_version, self.context.lib_version))
421
422         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)