]> sigrok.org Git - sigrok-meter.git/blame - sigrok-meter
Decorate all functions used as slots.
[sigrok-meter.git] / sigrok-meter
CommitLineData
5add80f6
JS
1#!/usr/bin/env python
2
c09ca11b
UH
3##
4## This file is part of the sigrok-meter project.
5##
6## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
73f2129a 7## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
c09ca11b
UH
8##
9## This program is free software; you can redistribute it and/or modify
10## it under the terms of the GNU General Public License as published by
11## the Free Software Foundation; either version 2 of the License, or
12## (at your option) any later version.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17## GNU General Public License for more details.
18##
19## You should have received a copy of the GNU General Public License
20## along with this program; if not, write to the Free Software
21## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22##
23
782f5926 24import argparse
73f2129a
JS
25import datetime
26import os.path
f94bb73f 27import re
efdef4fa 28import sigrok.core as sr
782f5926 29import sys
f94bb73f 30import textwrap
13e332b7 31
f94bb73f 32default_drivers = [('demo', {'analog_channels': 1})]
782f5926
JS
33default_loglevel = sr.LogLevel.WARN
34
1f199679
JS
35def parse_cli():
36 parser = argparse.ArgumentParser(
37 description='Simple sigrok GUI for multimeters and dataloggers.',
38 epilog=textwrap.dedent('''\
39 The DRIVER string is the same as for sigrok-cli(1).
40
41 examples:
42
43 %(prog)s --driver tecpel-dmm-8061-ser:conn=/dev/ttyUSB0
44
45 %(prog)s --driver uni-t-ut61e:conn=1a86.e008
46 '''),
47 formatter_class=argparse.RawDescriptionHelpFormatter)
48
49 parser.add_argument('-d', '--driver',
50 action='append',
51 help='The driver to use')
52 parser.add_argument('-l', '--loglevel',
53 type=int,
54 help='Set loglevel (5 is most verbose)')
55 parser.add_argument('--pyside',
56 action='store_true',
57 default=False,
58 help='Force use of PySide (default is to use PyQt4)')
59 args = parser.parse_args()
60
61 result = {
62 'drivers': default_drivers,
63 'loglevel': default_loglevel,
64 'pyside': args.pyside
65 }
66
67 if args.driver:
68 result['drivers'] = []
69 for d in args.driver:
70 m = re.match('(?P<name>[^:]+)(?P<opts>(:[^:=]+=[^:=]+)*)', d)
71 if not m:
72 sys.exit('error parsing option "{}"'.format(d))
73
74 opts = m.group('opts').split(':')[1:]
75 opts = [tuple(kv.split('=')) for kv in opts]
76 opts = dict(opts)
77
78 result['drivers'].append((m.group('name'), opts))
79
80 if args.loglevel != None:
81 try:
82 result['loglevel'] = sr.LogLevel.get(args.loglevel)
83 except:
84 sys.exit('error: invalid log level')
85
86 return result
87
88if __name__ == '__main__':
89 # The command line parsing and import of the Qt modules is done here,
90 # so that the modules are imported before the classes below derive
91 # from classes therein. The rest of the main function is at the bottom.
92
93 args = parse_cli()
94
1f199679
JS
95 if args['pyside']:
96 from PySide import QtCore, QtGui
1f199679
JS
97 else:
98 try:
5fda58ad
JS
99 # Use version 2 API in all cases, because that's what PySide uses.
100 import sip
101 sip.setapi('QVariant', 2)
102 sip.setapi('QDate', 2)
103 sip.setapi('QDateTime', 2)
104 sip.setapi('QString', 2)
105 sip.setapi('QTextStream', 2)
106 sip.setapi('QTime', 2)
107 sip.setapi('QUrl', 2)
108 sip.setapi('QVariant', 2)
109
1f199679 110 from PyQt4 import QtCore, QtGui
5fda58ad
JS
111
112 # Add PySide compatible names.
113 QtCore.Signal = QtCore.pyqtSignal
114 QtCore.Slot = QtCore.pyqtSlot
1f199679
JS
115 except:
116 sys.stderr.write('import of PyQt4 failed, using PySide\n')
117 from PySide import QtCore, QtGui
1f199679 118
73f2129a
JS
119class SamplingThread(QtCore.QObject):
120 '''A class that handles the reception of sigrok packets in the background.'''
121
122 class Worker(QtCore.QObject):
123 '''Helper class that does the actual work in another thread.'''
124
125 '''Signal emitted when new data arrived.'''
5fda58ad 126 measured = QtCore.Signal(object, object)
73f2129a 127
284a2e34 128 '''Signal emmited in case of an error.'''
5fda58ad 129 error = QtCore.Signal(str)
284a2e34 130
73f2129a
JS
131 def __init__(self, drivers, loglevel):
132 super(self.__class__, self).__init__()
133
284a2e34
JS
134 self.sampling = False
135 self.drivers = drivers
136
73f2129a
JS
137 self.context = sr.Context_create()
138 self.context.log_level = loglevel
139
140 self.sr_pkg_version = self.context.package_version
141 self.sr_lib_version = self.context.lib_version
142
f517686f 143 @QtCore.Slot()
284a2e34
JS
144 def start_sampling(self):
145 devices = []
146 for name, options in self.drivers:
73f2129a
JS
147 try:
148 dr = self.context.drivers[name]
284a2e34 149 devices.append(dr.scan(**options)[0])
73f2129a 150 except:
284a2e34
JS
151 self.error.emit(
152 'Unable to get device for driver "{}".'.format(name))
153 return
73f2129a 154
73f2129a 155 self.session = self.context.create_session()
284a2e34 156 for dev in devices:
73f2129a
JS
157 self.session.add_device(dev)
158 dev.open()
159 self.session.add_datafeed_callback(self.callback)
160 self.session.start()
284a2e34 161 self.sampling = True
73f2129a
JS
162 self.session.run()
163
284a2e34
JS
164 # If sampling is 'True' here, it means that 'stop_sampling()' was
165 # not called, therefore 'session.run()' ended too early, indicating
166 # an error.
167 if self.sampling:
168 self.error.emit('An error occured during the acquisition.')
169
73f2129a 170 def stop_sampling(self):
284a2e34
JS
171 if self.sampling:
172 self.sampling = False
173 self.session.stop()
73f2129a
JS
174
175 def callback(self, device, packet):
176 if packet.type == sr.PacketType.ANALOG:
50523e84 177 dev = '{} {}'.format(device.vendor, device.model)
73f2129a 178
50523e84
JS
179 # only send the most recent value
180 mag = packet.payload.data[0][-1]
73f2129a 181
50523e84
JS
182 self.measured.emit((dev, mag, packet.payload.unit,
183 packet.payload.mq_flags))
73f2129a
JS
184
185 # wait a short time so that in any case we don't flood the GUI
186 # with new data (for example if the demo device is used)
187 self.thread().msleep(100)
188
189 # signal used to start the worker across threads
5fda58ad 190 _start_signal = QtCore.Signal()
73f2129a
JS
191
192 def __init__(self, drivers, loglevel):
193 super(self.__class__, self).__init__()
194
195 self.worker = self.Worker(drivers, loglevel)
196 self.thread = QtCore.QThread()
197 self.worker.moveToThread(self.thread)
198
199 self._start_signal.connect(self.worker.start_sampling)
200
284a2e34 201 # expose the signals of the worker
73f2129a 202 self.measured = self.worker.measured
284a2e34 203 self.error = self.worker.error
73f2129a
JS
204
205 self.thread.start()
206
207 def start(self):
208 '''Starts sampling'''
209 self._start_signal.emit()
210
211 def stop(self):
212 '''Stops sampling and the background thread.'''
213 self.worker.stop_sampling()
214 self.thread.quit()
215 # the timeout is needed when the demo device is used, because it
216 # produces so much outstanding data that quitting takes a long time
217 self.thread.wait(500)
218
219 def sr_pkg_version(self):
220 '''Returns the version number of the libsigrok package.'''
221 return self.worker.sr_pkg_version
222
223 def sr_lib_version(self):
224 '''Returns the version number fo the libsigrok library.'''
225 return self.worker.sr_lib_version
226
227class SigrokMeter(QtGui.QMainWindow):
228 '''The main window of the application.'''
229
230 def __init__(self, thread):
231 super(SigrokMeter, self).__init__()
232 self.setup_ui()
233
50523e84
JS
234 self.inf = float('inf')
235
73f2129a
JS
236 self.thread = thread
237 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
284a2e34 238 self.thread.error.connect(self.error)
73f2129a
JS
239 self.thread.start()
240
241 def setup_ui(self):
242 self.setWindowTitle('sigrok-meter')
243 self.setMinimumHeight(130)
244 self.setMinimumWidth(260)
245
246 p = os.path.abspath(os.path.dirname(__file__))
247 p = os.path.join(p, 'sigrok-logo-notext.png')
248 self.setWindowIcon(QtGui.QIcon(p))
249
250 actionQuit = QtGui.QAction(self)
251 actionQuit.setText('&Quit')
252 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
253 actionQuit.setShortcut('Ctrl+Q')
254 actionQuit.triggered.connect(self.close)
255
256 actionAbout = QtGui.QAction(self)
257 actionAbout.setText('&About')
258 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
259 actionAbout.triggered.connect(self.show_about)
260
261 menubar = self.menuBar()
262 menuFile = menubar.addMenu('&File')
263 menuFile.addAction(actionQuit)
264 menuHelp = menubar.addMenu('&Help')
265 menuHelp.addAction(actionAbout)
266
267 self.lblValue = QtGui.QLabel('waiting for data...')
268 self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
269 font = self.lblValue.font()
270 font.setPointSize(font.pointSize() * 1.7)
271 font.setBold(True)
272 self.lblValue.setFont(font)
273 self.setCentralWidget(self.lblValue)
274 self.centralWidget().setContentsMargins(0, 0, 0, 0)
275
276 self.lblDevName = QtGui.QLabel()
277 self.lblDevName.setToolTip('Name of used measurement device.')
278 self.statusBar().insertWidget(0, self.lblDevName, 10)
279 self.lblTime = QtGui.QLabel()
280 self.lblTime.setToolTip('Time of the last measurement.')
281 self.statusBar().insertWidget(1, self.lblTime)
282
283 self.statusBar().setSizeGripEnabled(False)
284
f517686f 285 @QtCore.Slot()
73f2129a
JS
286 def show_about(self):
287 text = textwrap.dedent('''\
288 <div align="center">
289 <b>sigrok-meter</b><br/>
290 0.1.0<br/>
291 Using libsigrok {} (lib version {}).<br/>
292 <a href='http://www.sigrok.org'>
293 http://www.sigrok.org</a><br/>
294 <br/>
295 This program comes with ABSOLUTELY NO WARRANTY;<br/>
296 for details visit
297 <a href='http://www.gnu.org/licenses/gpl.html'>
298 http://www.gnu.org/licenses/gpl.html</a>
299 </div>
300 '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
301
302 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
303
50523e84
JS
304 def format_unit(self, u):
305 units = {
306 sr.Unit.VOLT: 'V',
307 sr.Unit.AMPERE: 'A',
308 sr.Unit.OHM: u'\u03A9',
309 sr.Unit.FARAD: 'F',
310 sr.Unit.KELVIN: 'K',
311 sr.Unit.CELSIUS: u'\u00B0C',
312 sr.Unit.FAHRENHEIT: u'\u00B0F',
313 sr.Unit.HERTZ: 'Hz',
314 sr.Unit.PERCENTAGE: '%',
315 # sr.Unit.BOOLEAN
316 sr.Unit.SECOND: 's',
317 sr.Unit.SIEMENS: 'S',
318 sr.Unit.DECIBEL_MW: 'dBu',
319 sr.Unit.DECIBEL_VOLT: 'dBV',
320 # sr.Unit.UNITLESS
321 sr.Unit.DECIBEL_SPL: 'dB',
322 # sr.Unit.CONCENTRATION
323 sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
324 sr.Unit.VOLT_AMPERE: 'VA',
325 sr.Unit.WATT: 'W',
326 sr.Unit.WATT_HOUR: 'Wh',
327 sr.Unit.METER_SECOND: 'm/s',
328 sr.Unit.HECTOPASCAL: 'hPa',
329 sr.Unit.HUMIDITY_293K: '%rF',
330 sr.Unit.DEGREE: u'\u00B0',
331 sr.Unit.HENRY: 'H'
332 }
333
334 return units.get(u, '')
335
336 def format_mqflags(self, mqflags):
337 if sr.QuantityFlag.AC in mqflags:
338 s = 'AC'
339 elif sr.QuantityFlag.DC in mqflags:
340 s = 'DC'
341 else:
342 s = ''
343
344 return s
345
346 def format_mag(self, mag):
347 if mag == self.inf:
348 return u'\u221E'
349 return '{:f}'.format(mag)
350
f517686f 351 @QtCore.Slot(object)
50523e84 352 def update(self, data):
73f2129a
JS
353 '''Updates the labels with new measurement values.'''
354
50523e84
JS
355 device, mag, unit, mqflags = data
356
357 unit_str = self.format_unit(unit)
358 mqflags_str = self.format_mqflags(mqflags)
359 mag_str = self.format_mag(mag)
360 value = ' '.join([mag_str, unit_str, mqflags_str])
361
73f2129a
JS
362 n = datetime.datetime.now().time()
363 now = '{:02}:{:02}:{:02}.{:03}'.format(
364 n.hour, n.minute, n.second, n.microsecond / 1000)
365
366 self.lblValue.setText(value)
367 self.lblDevName.setText(device)
368 self.lblTime.setText(now)
2f5ef701 369
f517686f 370 @QtCore.Slot(str)
284a2e34
JS
371 def error(self, msg):
372 '''Error handler for the sampling thread.'''
373 QtGui.QMessageBox.critical(self, 'Error', msg)
374 self.close()
375
f94bb73f 376if __name__ == '__main__':
73f2129a
JS
377 thread = SamplingThread(args['drivers'], args['loglevel'])
378
379 app = QtGui.QApplication([])
380 s = SigrokMeter(thread)
381 s.show()
382
383 r = app.exec_()
384 thread.stop()
385 sys.exit(r)