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