]> sigrok.org Git - sigrok-meter.git/blob - mainwindow.py
Remove the thread used for sampling.
[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 multiplotwidget
25 import os.path
26 import qtcompat
27 import textwrap
28 import time
29 import util
30
31 QtCore = qtcompat.QtCore
32 QtGui = qtcompat.QtGui
33 pyqtgraph = qtcompat.pyqtgraph
34
35 class 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
52 class 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)