]> sigrok.org Git - sigrok-meter.git/blame - sigrok-meter
Better error handling.
[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
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
73f2129a
JS
109class 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.'''
1f199679 116 measured = qt_signal(object)
73f2129a 117
284a2e34
JS
118 '''Signal emmited in case of an error.'''
119 error = qt_signal(str)
120
73f2129a
JS
121 def __init__(self, drivers, loglevel):
122 super(self.__class__, self).__init__()
123
284a2e34
JS
124 self.sampling = False
125 self.drivers = drivers
126
73f2129a
JS
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
284a2e34
JS
133 def start_sampling(self):
134 devices = []
135 for name, options in self.drivers:
73f2129a
JS
136 try:
137 dr = self.context.drivers[name]
284a2e34 138 devices.append(dr.scan(**options)[0])
73f2129a 139 except:
284a2e34
JS
140 self.error.emit(
141 'Unable to get device for driver "{}".'.format(name))
142 return
73f2129a 143
73f2129a 144 self.session = self.context.create_session()
284a2e34 145 for dev in devices:
73f2129a
JS
146 self.session.add_device(dev)
147 dev.open()
148 self.session.add_datafeed_callback(self.callback)
149 self.session.start()
284a2e34 150 self.sampling = True
73f2129a
JS
151 self.session.run()
152
284a2e34
JS
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
73f2129a 159 def stop_sampling(self):
284a2e34
JS
160 if self.sampling:
161 self.sampling = False
162 self.session.stop()
73f2129a
JS
163
164 def callback(self, device, packet):
165 if packet.type == sr.PacketType.ANALOG:
50523e84 166 dev = '{} {}'.format(device.vendor, device.model)
73f2129a 167
50523e84
JS
168 # only send the most recent value
169 mag = packet.payload.data[0][-1]
73f2129a 170
50523e84
JS
171 self.measured.emit((dev, mag, packet.payload.unit,
172 packet.payload.mq_flags))
73f2129a
JS
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
1f199679 179 _start_signal = qt_signal()
73f2129a
JS
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
284a2e34 190 # expose the signals of the worker
73f2129a 191 self.measured = self.worker.measured
284a2e34 192 self.error = self.worker.error
73f2129a
JS
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
216class 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
50523e84
JS
223 self.inf = float('inf')
224
73f2129a
JS
225 self.thread = thread
226 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
284a2e34 227 self.thread.error.connect(self.error)
73f2129a
JS
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
50523e84
JS
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):
73f2129a
JS
340 '''Updates the labels with new measurement values.'''
341
50523e84
JS
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
73f2129a
JS
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)
2f5ef701 356
284a2e34
JS
357 def error(self, msg):
358 '''Error handler for the sampling thread.'''
359 QtGui.QMessageBox.critical(self, 'Error', msg)
360 self.close()
361
f94bb73f 362if __name__ == '__main__':
73f2129a
JS
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)