]> sigrok.org Git - sigrok-meter.git/blob - sigrok-meter
Decorate all functions used as slots.
[sigrok-meter.git] / sigrok-meter
1 #!/usr/bin/env python
2
3 ##
4 ## This file is part of the sigrok-meter project.
5 ##
6 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
7 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
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
24 import argparse
25 import datetime
26 import os.path
27 import re
28 import sigrok.core as sr
29 import sys
30 import textwrap
31
32 default_drivers = [('demo', {'analog_channels': 1})]
33 default_loglevel = sr.LogLevel.WARN
34
35 def 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
88 if __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
95     if args['pyside']:
96         from PySide import QtCore, QtGui
97     else:
98         try:
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
110             from PyQt4 import QtCore, QtGui
111
112             # Add PySide compatible names.
113             QtCore.Signal = QtCore.pyqtSignal
114             QtCore.Slot = QtCore.pyqtSlot
115         except:
116             sys.stderr.write('import of PyQt4 failed, using PySide\n')
117             from PySide import QtCore, QtGui
118
119 class 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.'''
126         measured = QtCore.Signal(object, object)
127
128         '''Signal emmited in case of an error.'''
129         error = QtCore.Signal(str)
130
131         def __init__(self, drivers, loglevel):
132             super(self.__class__, self).__init__()
133
134             self.sampling = False
135             self.drivers = drivers
136
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
143         @QtCore.Slot()
144         def start_sampling(self):
145             devices = []
146             for name, options in self.drivers:
147                 try:
148                     dr = self.context.drivers[name]
149                     devices.append(dr.scan(**options)[0])
150                 except:
151                     self.error.emit(
152                         'Unable to get device for driver "{}".'.format(name))
153                     return
154
155             self.session = self.context.create_session()
156             for dev in devices:
157                 self.session.add_device(dev)
158                 dev.open()
159             self.session.add_datafeed_callback(self.callback)
160             self.session.start()
161             self.sampling = True
162             self.session.run()
163
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
170         def stop_sampling(self):
171             if self.sampling:
172                 self.sampling = False
173                 self.session.stop()
174
175         def callback(self, device, packet):
176             if packet.type == sr.PacketType.ANALOG:
177                 dev = '{} {}'.format(device.vendor, device.model)
178
179                 # only send the most recent value
180                 mag = packet.payload.data[0][-1]
181
182                 self.measured.emit((dev, mag, packet.payload.unit,
183                         packet.payload.mq_flags))
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
190     _start_signal = QtCore.Signal()
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
201         # expose the signals of the worker
202         self.measured = self.worker.measured
203         self.error = self.worker.error
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
227 class 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
234         self.inf = float('inf')
235
236         self.thread = thread
237         self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
238         self.thread.error.connect(self.error)
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
285     @QtCore.Slot()
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
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
351     @QtCore.Slot(object)
352     def update(self, data):
353         '''Updates the labels with new measurement values.'''
354
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
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)
369
370     @QtCore.Slot(str)
371     def error(self, msg):
372         '''Error handler for the sampling thread.'''
373         QtGui.QMessageBox.critical(self, 'Error', msg)
374         self.close()
375
376 if __name__ == '__main__':
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)