]> sigrok.org Git - sigrok-meter.git/blob - sigrok-meter
0c1260077d99af5cea6dcaddc681e483198a5c83
[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     global qt_signal
96
97     if args['pyside']:
98         from PySide import QtCore, QtGui
99         qt_signal = QtCore.Signal
100     else:
101         try:
102             from PyQt4 import QtCore, QtGui
103             qt_signal = QtCore.pyqtSignal
104         except:
105             sys.stderr.write('import of PyQt4 failed, using PySide\n')
106             from PySide import QtCore, QtGui
107             qt_signal = QtCore.Signal
108
109 class SamplingThread(QtCore.QObject):
110     '''A class that handles the reception of sigrok packets in the background.'''
111
112     class Worker(QtCore.QObject):
113         '''Helper class that does the actual work in another thread.'''
114
115         '''Signal emitted when new data arrived.'''
116         measured = qt_signal(object)
117
118         '''Signal emmited in case of an error.'''
119         error = qt_signal(str)
120
121         def __init__(self, drivers, loglevel):
122             super(self.__class__, self).__init__()
123
124             self.sampling = False
125             self.drivers = drivers
126
127             self.context = sr.Context_create()
128             self.context.log_level = loglevel
129
130             self.sr_pkg_version = self.context.package_version
131             self.sr_lib_version = self.context.lib_version
132
133         def start_sampling(self):
134             devices = []
135             for name, options in self.drivers:
136                 try:
137                     dr = self.context.drivers[name]
138                     devices.append(dr.scan(**options)[0])
139                 except:
140                     self.error.emit(
141                         'Unable to get device for driver "{}".'.format(name))
142                     return
143
144             self.session = self.context.create_session()
145             for dev in devices:
146                 self.session.add_device(dev)
147                 dev.open()
148             self.session.add_datafeed_callback(self.callback)
149             self.session.start()
150             self.sampling = True
151             self.session.run()
152
153             # If sampling is 'True' here, it means that 'stop_sampling()' was
154             # not called, therefore 'session.run()' ended too early, indicating
155             # an error.
156             if self.sampling:
157                 self.error.emit('An error occured during the acquisition.')
158
159         def stop_sampling(self):
160             if self.sampling:
161                 self.sampling = False
162                 self.session.stop()
163
164         def callback(self, device, packet):
165             if packet.type == sr.PacketType.ANALOG:
166                 dev = '{} {}'.format(device.vendor, device.model)
167
168                 # only send the most recent value
169                 mag = packet.payload.data[0][-1]
170
171                 self.measured.emit((dev, mag, packet.payload.unit,
172                         packet.payload.mq_flags))
173
174             # wait a short time so that in any case we don't flood the GUI
175             # with new data (for example if the demo device is used)
176             self.thread().msleep(100)
177
178     # signal used to start the worker across threads
179     _start_signal = qt_signal()
180
181     def __init__(self, drivers, loglevel):
182         super(self.__class__, self).__init__()
183
184         self.worker = self.Worker(drivers, loglevel)
185         self.thread = QtCore.QThread()
186         self.worker.moveToThread(self.thread)
187
188         self._start_signal.connect(self.worker.start_sampling)
189
190         # expose the signals of the worker
191         self.measured = self.worker.measured
192         self.error = self.worker.error
193
194         self.thread.start()
195
196     def start(self):
197         '''Starts sampling'''
198         self._start_signal.emit()
199
200     def stop(self):
201         '''Stops sampling and the background thread.'''
202         self.worker.stop_sampling()
203         self.thread.quit()
204         # the timeout is needed when the demo device is used, because it
205         # produces so much outstanding data that quitting takes a long time
206         self.thread.wait(500)
207
208     def sr_pkg_version(self):
209         '''Returns the version number of the libsigrok package.'''
210         return self.worker.sr_pkg_version
211
212     def sr_lib_version(self):
213         '''Returns the version number fo the libsigrok library.'''
214         return self.worker.sr_lib_version
215
216 class SigrokMeter(QtGui.QMainWindow):
217     '''The main window of the application.'''
218
219     def __init__(self, thread):
220         super(SigrokMeter, self).__init__()
221         self.setup_ui()
222
223         self.inf = float('inf')
224
225         self.thread = thread
226         self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
227         self.thread.error.connect(self.error)
228         self.thread.start()
229
230     def setup_ui(self):
231         self.setWindowTitle('sigrok-meter')
232         self.setMinimumHeight(130)
233         self.setMinimumWidth(260)
234
235         p = os.path.abspath(os.path.dirname(__file__))
236         p = os.path.join(p, 'sigrok-logo-notext.png')
237         self.setWindowIcon(QtGui.QIcon(p))
238
239         actionQuit = QtGui.QAction(self)
240         actionQuit.setText('&Quit')
241         actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
242         actionQuit.setShortcut('Ctrl+Q')
243         actionQuit.triggered.connect(self.close)
244
245         actionAbout = QtGui.QAction(self)
246         actionAbout.setText('&About')
247         actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
248         actionAbout.triggered.connect(self.show_about)
249
250         menubar = self.menuBar()
251         menuFile = menubar.addMenu('&File')
252         menuFile.addAction(actionQuit)
253         menuHelp = menubar.addMenu('&Help')
254         menuHelp.addAction(actionAbout)
255
256         self.lblValue = QtGui.QLabel('waiting for data...')
257         self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
258         font = self.lblValue.font()
259         font.setPointSize(font.pointSize() * 1.7)
260         font.setBold(True)
261         self.lblValue.setFont(font)
262         self.setCentralWidget(self.lblValue)
263         self.centralWidget().setContentsMargins(0, 0, 0, 0)
264
265         self.lblDevName = QtGui.QLabel()
266         self.lblDevName.setToolTip('Name of used measurement device.')
267         self.statusBar().insertWidget(0, self.lblDevName, 10)
268         self.lblTime = QtGui.QLabel()
269         self.lblTime.setToolTip('Time of the last measurement.')
270         self.statusBar().insertWidget(1, self.lblTime)
271
272         self.statusBar().setSizeGripEnabled(False)
273
274     def show_about(self):
275         text = textwrap.dedent('''\
276             <div align="center">
277                 <b>sigrok-meter</b><br/>
278                 0.1.0<br/>
279                 Using libsigrok {} (lib version {}).<br/>
280                 <a href='http://www.sigrok.org'>
281                          http://www.sigrok.org</a><br/>
282                 <br/>
283                 This program comes with ABSOLUTELY NO WARRANTY;<br/>
284                 for details visit
285                 <a href='http://www.gnu.org/licenses/gpl.html'>
286                          http://www.gnu.org/licenses/gpl.html</a>
287             </div>
288         '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
289
290         QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
291
292     def format_unit(self, u):
293         units = {
294             sr.Unit.VOLT:                   'V',
295             sr.Unit.AMPERE:                 'A',
296             sr.Unit.OHM:                   u'\u03A9',
297             sr.Unit.FARAD:                  'F',
298             sr.Unit.KELVIN:                 'K',
299             sr.Unit.CELSIUS:               u'\u00B0C',
300             sr.Unit.FAHRENHEIT:            u'\u00B0F',
301             sr.Unit.HERTZ:                  'Hz',
302             sr.Unit.PERCENTAGE:             '%',
303           # sr.Unit.BOOLEAN
304             sr.Unit.SECOND:                 's',
305             sr.Unit.SIEMENS:                'S',
306             sr.Unit.DECIBEL_MW:             'dBu',
307             sr.Unit.DECIBEL_VOLT:           'dBV',
308           # sr.Unit.UNITLESS
309             sr.Unit.DECIBEL_SPL:            'dB',
310           # sr.Unit.CONCENTRATION
311             sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
312             sr.Unit.VOLT_AMPERE:            'VA',
313             sr.Unit.WATT:                   'W',
314             sr.Unit.WATT_HOUR:              'Wh',
315             sr.Unit.METER_SECOND:           'm/s',
316             sr.Unit.HECTOPASCAL:            'hPa',
317             sr.Unit.HUMIDITY_293K:          '%rF',
318             sr.Unit.DEGREE:                u'\u00B0',
319             sr.Unit.HENRY:                  'H'
320         }
321
322         return units.get(u, '')
323
324     def format_mqflags(self, mqflags):
325         if sr.QuantityFlag.AC in mqflags:
326             s = 'AC'
327         elif sr.QuantityFlag.DC in mqflags:
328             s = 'DC'
329         else:
330             s = ''
331
332         return s
333
334     def format_mag(self, mag):
335         if mag == self.inf:
336             return u'\u221E'
337         return '{:f}'.format(mag)
338
339     def update(self, data):
340         '''Updates the labels with new measurement values.'''
341
342         device, mag, unit, mqflags = data
343
344         unit_str = self.format_unit(unit)
345         mqflags_str = self.format_mqflags(mqflags)
346         mag_str = self.format_mag(mag)
347         value = ' '.join([mag_str, unit_str, mqflags_str])
348
349         n = datetime.datetime.now().time()
350         now = '{:02}:{:02}:{:02}.{:03}'.format(
351                 n.hour, n.minute, n.second, n.microsecond / 1000)
352
353         self.lblValue.setText(value)
354         self.lblDevName.setText(device)
355         self.lblTime.setText(now)
356
357     def error(self, msg):
358         '''Error handler for the sampling thread.'''
359         QtGui.QMessageBox.critical(self, 'Error', msg)
360         self.close()
361
362 if __name__ == '__main__':
363     thread = SamplingThread(args['drivers'], args['loglevel'])
364
365     app = QtGui.QApplication([])
366     s = SigrokMeter(thread)
367     s.show()
368
369     r = app.exec_()
370     thread.stop()
371     sys.exit(r)