]>
Commit | Line | Data |
---|---|---|
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 | 22 | import acquisition |
48723bbb | 23 | import datamodel |
f76b9df8 | 24 | import multiplotwidget |
48723bbb JS |
25 | import os.path |
26 | import qtcompat | |
48723bbb | 27 | import textwrap |
f76b9df8 JS |
28 | import time |
29 | import util | |
48723bbb JS |
30 | |
31 | QtCore = qtcompat.QtCore | |
32 | QtGui = qtcompat.QtGui | |
f76b9df8 | 33 | pyqtgraph = qtcompat.pyqtgraph |
48723bbb JS |
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 | ||
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) |