]> sigrok.org Git - sigrok-meter.git/blame - sigrok-meter
Use PyQts APIv2 to match the PySide API.
[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
284a2e34
JS
143 def start_sampling(self):
144 devices = []
145 for name, options in self.drivers:
73f2129a
JS
146 try:
147 dr = self.context.drivers[name]
284a2e34 148 devices.append(dr.scan(**options)[0])
73f2129a 149 except:
284a2e34
JS
150 self.error.emit(
151 'Unable to get device for driver "{}".'.format(name))
152 return
73f2129a 153
73f2129a 154 self.session = self.context.create_session()
284a2e34 155 for dev in devices:
73f2129a
JS
156 self.session.add_device(dev)
157 dev.open()
158 self.session.add_datafeed_callback(self.callback)
159 self.session.start()
284a2e34 160 self.sampling = True
73f2129a
JS
161 self.session.run()
162
284a2e34
JS
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
73f2129a 169 def stop_sampling(self):
284a2e34
JS
170 if self.sampling:
171 self.sampling = False
172 self.session.stop()
73f2129a
JS
173
174 def callback(self, device, packet):
175 if packet.type == sr.PacketType.ANALOG:
50523e84 176 dev = '{} {}'.format(device.vendor, device.model)
73f2129a 177
50523e84
JS
178 # only send the most recent value
179 mag = packet.payload.data[0][-1]
73f2129a 180
50523e84
JS
181 self.measured.emit((dev, mag, packet.payload.unit,
182 packet.payload.mq_flags))
73f2129a
JS
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
5fda58ad 189 _start_signal = QtCore.Signal()
73f2129a
JS
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
284a2e34 200 # expose the signals of the worker
73f2129a 201 self.measured = self.worker.measured
284a2e34 202 self.error = self.worker.error
73f2129a
JS
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
226class 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
50523e84
JS
233 self.inf = float('inf')
234
73f2129a
JS
235 self.thread = thread
236 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
284a2e34 237 self.thread.error.connect(self.error)
73f2129a
JS
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
50523e84
JS
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):
73f2129a
JS
350 '''Updates the labels with new measurement values.'''
351
50523e84
JS
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
73f2129a
JS
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)
2f5ef701 366
284a2e34
JS
367 def error(self, msg):
368 '''Error handler for the sampling thread.'''
369 QtGui.QMessageBox.critical(self, 'Error', msg)
370 self.close()
371
f94bb73f 372if __name__ == '__main__':
73f2129a
JS
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)