Timestamp measurements as early as possible.
[sigrok-meter/gsi.git] / mainwindow.py
blob8ac3b3de142f7fa828a0c7fdbf18a874b28c0097
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.
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.
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
22 import datamodel
23 import multiplotwidget
24 import os.path
25 import qtcompat
26 import samplingthread
27 import textwrap
28 import time
29 import util
31 QtCore = qtcompat.QtCore
32 QtGui = qtcompat.QtGui
33 pyqtgraph = qtcompat.pyqtgraph
35 class EmptyMessageListView(QtGui.QListView):
36 '''List view that shows a message if the model im empty.'''
38 def __init__(self, message, parent=None):
39 super(self.__class__, self).__init__(parent)
41 self._message = message
43 def paintEvent(self, event):
44 m = self.model()
45 if m and m.rowCount():
46 super(self.__class__, self).paintEvent(event)
47 return
49 painter = QtGui.QPainter(self.viewport())
50 painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self._message)
52 class MainWindow(QtGui.QMainWindow):
53 '''The main window of the application.'''
55 # Number of seconds that the plots display.
56 BACKLOG = 30
58 # Update interval of the plots in milliseconds.
59 UPDATEINTERVAL = 100
61 def __init__(self, context, drivers):
62 super(self.__class__, self).__init__()
64 self.context = context
66 self.delegate = datamodel.MultimeterDelegate(self, self.font())
67 self.model = datamodel.MeasurementDataModel(self)
68 self.model.rowsInserted.connect(self.modelRowsInserted)
70 self.setup_ui()
72 self.thread = samplingthread.SamplingThread(self.context, drivers)
73 self.thread.measured.connect(self.model.update)
74 self.thread.error.connect(self.error)
75 self.thread.start()
77 def setup_ui(self):
78 self.setWindowTitle('sigrok-meter')
79 # Resizing the listView below will increase this again.
80 self.resize(350, 10)
82 p = os.path.abspath(os.path.dirname(__file__))
83 p = os.path.join(p, 'sigrok-logo-notext.png')
84 self.setWindowIcon(QtGui.QIcon(p))
86 actionQuit = QtGui.QAction(self)
87 actionQuit.setText('&Quit')
88 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
89 actionQuit.setShortcut('Ctrl+Q')
90 actionQuit.triggered.connect(self.close)
92 actionAbout = QtGui.QAction(self)
93 actionAbout.setText('&About')
94 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
95 actionAbout.triggered.connect(self.show_about)
97 menubar = self.menuBar()
98 menuFile = menubar.addMenu('&File')
99 menuFile.addAction(actionQuit)
100 menuHelp = menubar.addMenu('&Help')
101 menuHelp.addAction(actionAbout)
103 self.listView = EmptyMessageListView('waiting for data...')
104 self.listView.setFrameShape(QtGui.QFrame.NoFrame)
105 self.listView.viewport().setBackgroundRole(QtGui.QPalette.Window)
106 self.listView.viewport().setAutoFillBackground(True)
107 self.listView.setMinimumWidth(260)
108 self.listView.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
109 self.listView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
110 self.listView.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
111 self.listView.setItemDelegate(self.delegate)
112 self.listView.setModel(self.model)
113 self.listView.setUniformItemSizes(True)
114 self.listView.setMinimumSize(self.delegate.sizeHint())
116 self.plotwidget = multiplotwidget.MultiPlotWidget(self)
117 self.plotwidget.plotHidden.connect(self._on_plotHidden)
119 # Maps from 'unit' to the corresponding plot.
120 self._plots = {}
121 # Maps from '(plot, device)' to the corresponding curve.
122 self._curves = {}
124 self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal);
125 self.splitter.addWidget(self.listView)
126 self.splitter.addWidget(self.plotwidget)
127 self.splitter.setStretchFactor(0, 0)
128 self.splitter.setStretchFactor(1, 1)
130 self.setCentralWidget(self.splitter)
131 self.centralWidget().setContentsMargins(0, 0, 0, 0)
132 self.resize(800, 500)
134 self.startTimer(MainWindow.UPDATEINTERVAL)
136 def _getPlot(self, unit):
137 '''Looks up or creates a new plot for 'unit'.'''
139 if unit in self._plots:
140 return self._plots[unit]
142 # create a new plot for the unit
143 plot = self.plotwidget.addPlot()
144 plot.yaxis.setLabel(util.quantity_from_unit(unit), units=util.format_unit(unit))
145 plot.view.setXRange(-MainWindow.BACKLOG, 0, update=False)
146 plot.view.setYRange(-1, 1)
147 plot.view.enableAutoRange(axis=pyqtgraph.ViewBox.YAxis)
148 # lock to the range calculated by the view using additional padding,
149 # looks nicer this way
150 r = plot.view.viewRange()
151 plot.view.setLimits(xMin=r[0][0], xMax=r[0][1])
153 self._plots[unit] = plot
154 return plot
156 def _getCurve(self, plot, deviceID):
157 '''Looks up or creates a new curve for '(plot, deviceID)'.'''
159 key = (id(plot), deviceID)
160 if key in self._curves:
161 return self._curves[key]
163 # create a new curve
164 curve = pyqtgraph.PlotDataItem(
165 antialias=True,
166 symbolPen=pyqtgraph.mkPen(QtGui.QColor(QtCore.Qt.black)),
167 symbolBrush=pyqtgraph.mkBrush(QtGui.QColor(QtCore.Qt.black)),
168 symbolSize=1
170 plot.view.addItem(curve)
172 self._curves[key] = curve
173 return curve
175 def timerEvent(self, event):
176 '''Periodically updates all graphs.'''
178 self._updatePlots()
180 def _updatePlots(self):
181 '''Updates all plots.'''
183 # loop over all devices and channels
184 for row in range(self.model.rowCount()):
185 idx = self.model.index(row, 0)
186 deviceID = self.model.data(idx,
187 datamodel.MeasurementDataModel.idRole)
188 traces = self.model.data(idx,
189 datamodel.MeasurementDataModel.tracesRole)
191 for unit, trace in traces.items():
192 now = time.time()
194 # remove old samples
195 l = now - MainWindow.BACKLOG
196 while trace.samples and trace.samples[0][0] < l:
197 trace.samples.pop(0)
199 plot = self._getPlot(unit)
200 if not plot.visible:
201 if trace.new:
202 self.plotwidget.showPlot(plot)
204 if plot.visible:
205 xdata = [s[0] - now for s in trace.samples]
206 ydata = [s[1] for s in trace.samples]
208 color = self.model.data(idx,
209 datamodel.MeasurementDataModel.colorRole)
211 curve = self._getCurve(plot, deviceID)
212 curve.setPen(pyqtgraph.mkPen(color=color))
213 curve.setData(xdata, ydata)
215 @QtCore.Slot(multiplotwidget.Plot)
216 def _on_plotHidden(self, plot):
217 plotunit = [u for u, p in self._plots.items() if p == plot][0]
219 # Mark all traces of all devices/channels with the same unit as the
220 # plot as "old" ('trace.new = False'). As soon as a new sample arrives
221 # on one trace, the plot will be shown again
222 for row in range(self.model.rowCount()):
223 idx = self.model.index(row, 0)
224 traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
226 for traceunit, trace in traces.items():
227 if traceunit == plotunit:
228 trace.new = False
230 def closeEvent(self, event):
231 self.thread.stop()
232 event.accept()
234 @QtCore.Slot()
235 def show_about(self):
236 text = textwrap.dedent('''\
237 <div align="center">
238 <b>sigrok-meter 0.1.0</b><br/><br/>
239 Using libsigrok {} (lib version {}).<br/><br/>
240 <a href='http://www.sigrok.org'>
241 http://www.sigrok.org</a><br/>
242 <br/>
243 License: GNU GPL, version 3 or later<br/>
244 <br/>
245 This program comes with ABSOLUTELY NO WARRANTY;<br/>
246 for details visit
247 <a href='http://www.gnu.org/licenses/gpl.html'>
248 http://www.gnu.org/licenses/gpl.html</a>
249 </div>
250 '''.format(self.context.package_version, self.context.lib_version))
252 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
254 @QtCore.Slot(str)
255 def error(self, msg):
256 '''Error handler for the sampling thread.'''
257 QtGui.QMessageBox.critical(self, 'Error', msg)
258 self.close()
260 @QtCore.Slot(object, int, int)
261 def modelRowsInserted(self, parent, start, end):
262 '''Resize the list view to the size of the content.'''
263 rows = self.model.rowCount()
264 dh = self.delegate.sizeHint().height()
265 self.listView.setMinimumHeight(dh * rows)