]> sigrok.org Git - sigrok-meter.git/blame_incremental - sigrok-meter
Convert GUI to Qt.
[sigrok-meter.git] / sigrok-meter
... / ...
CommitLineData
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
24import argparse
25import datetime
26import os.path
27import PyQt4.QtCore as QtCore
28import PyQt4.QtGui as QtGui
29import re
30import sigrok.core as sr
31import sys
32import textwrap
33
34default_drivers = [('demo', {'analog_channels': 1})]
35default_loglevel = sr.LogLevel.WARN
36
37def format_unit(u):
38 units = {
39 sr.Unit.VOLT: 'V',
40 sr.Unit.AMPERE: 'A',
41 sr.Unit.OHM: u'\u03A9',
42 sr.Unit.FARAD: 'F',
43 sr.Unit.KELVIN: 'K',
44 sr.Unit.CELSIUS: u'\u00B0C',
45 sr.Unit.FAHRENHEIT: u'\u00B0F',
46 sr.Unit.HERTZ: 'Hz',
47 sr.Unit.PERCENTAGE: '%',
48 # sr.Unit.BOOLEAN
49 sr.Unit.SECOND: 's',
50 sr.Unit.SIEMENS: 'S',
51 sr.Unit.DECIBEL_MW: 'dBu',
52 sr.Unit.DECIBEL_VOLT: 'dBV',
53 # sr.Unit.UNITLESS
54 sr.Unit.DECIBEL_SPL: 'dB',
55 # sr.Unit.CONCENTRATION
56 sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
57 sr.Unit.VOLT_AMPERE: 'VA',
58 sr.Unit.WATT: 'W',
59 sr.Unit.WATT_HOUR: 'Wh',
60 sr.Unit.METER_SECOND: 'm/s',
61 sr.Unit.HECTOPASCAL: 'hPa',
62 sr.Unit.HUMIDITY_293K: '%rF',
63 sr.Unit.DEGREE: u'\u00B0',
64 sr.Unit.HENRY: 'H'
65 }
66
67 return units.get(u, '')
68
69class SamplingThread(QtCore.QObject):
70 '''A class that handles the reception of sigrok packets in the background.'''
71
72 class Worker(QtCore.QObject):
73 '''Helper class that does the actual work in another thread.'''
74
75 '''Signal emitted when new data arrived.'''
76 measured = QtCore.pyqtSignal(object, object)
77
78 def __init__(self, drivers, loglevel):
79 super(self.__class__, self).__init__()
80
81 self.context = sr.Context_create()
82 self.context.log_level = loglevel
83
84 self.sr_pkg_version = self.context.package_version
85 self.sr_lib_version = self.context.lib_version
86
87 self.devices = []
88 for name, options in drivers:
89 try:
90 dr = self.context.drivers[name]
91 self.devices.append(dr.scan(**options)[0])
92 except:
93 print('error getting device for driver "{}", skipping'.format(name))
94
95 if not self.devices:
96 print('no devices found')
97
98 def start_sampling(self):
99 self.session = self.context.create_session()
100 for dev in self.devices:
101 self.session.add_device(dev)
102 dev.open()
103 self.session.add_datafeed_callback(self.callback)
104 self.session.start()
105 self.session.run()
106
107 def stop_sampling(self):
108 self.session.stop()
109
110 def callback(self, device, packet):
111 if packet.type == sr.PacketType.ANALOG:
112 data = packet.payload.data
113 unit_str = format_unit(packet.payload.unit)
114 mqflags, mqflags_str = packet.payload.mq_flags, ""
115
116 if sr.QuantityFlag.AC in mqflags:
117 mqflags_str = "AC"
118 elif sr.QuantityFlag.DC in mqflags:
119 mqflags_str = "DC"
120
121 for i in range(packet.payload.num_samples):
122 dev = "%s %s" % (device.vendor, device.model)
123 mag_str = "%f" % data[0][i]
124 val = ' '.join([mag_str, unit_str, mqflags_str])
125
126 self.measured.emit(dev, val)
127
128 # wait a short time so that in any case we don't flood the GUI
129 # with new data (for example if the demo device is used)
130 self.thread().msleep(100)
131
132 # signal used to start the worker across threads
133 _start_signal = QtCore.pyqtSignal()
134
135 def __init__(self, drivers, loglevel):
136 super(self.__class__, self).__init__()
137
138 self.worker = self.Worker(drivers, loglevel)
139 self.thread = QtCore.QThread()
140 self.worker.moveToThread(self.thread)
141
142 self._start_signal.connect(self.worker.start_sampling)
143
144 self.measured = self.worker.measured
145
146 self.thread.start()
147
148 def start(self):
149 '''Starts sampling'''
150 self._start_signal.emit()
151
152 def stop(self):
153 '''Stops sampling and the background thread.'''
154 self.worker.stop_sampling()
155 self.thread.quit()
156 # the timeout is needed when the demo device is used, because it
157 # produces so much outstanding data that quitting takes a long time
158 self.thread.wait(500)
159
160 def sr_pkg_version(self):
161 '''Returns the version number of the libsigrok package.'''
162 return self.worker.sr_pkg_version
163
164 def sr_lib_version(self):
165 '''Returns the version number fo the libsigrok library.'''
166 return self.worker.sr_lib_version
167
168class SigrokMeter(QtGui.QMainWindow):
169 '''The main window of the application.'''
170
171 def __init__(self, thread):
172 super(SigrokMeter, self).__init__()
173 self.setup_ui()
174
175 self.thread = thread
176 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
177 self.thread.start()
178
179 def setup_ui(self):
180 self.setWindowTitle('sigrok-meter')
181 self.setMinimumHeight(130)
182 self.setMinimumWidth(260)
183
184 p = os.path.abspath(os.path.dirname(__file__))
185 p = os.path.join(p, 'sigrok-logo-notext.png')
186 self.setWindowIcon(QtGui.QIcon(p))
187
188 actionQuit = QtGui.QAction(self)
189 actionQuit.setText('&Quit')
190 actionQuit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
191 actionQuit.setShortcut('Ctrl+Q')
192 actionQuit.triggered.connect(self.close)
193
194 actionAbout = QtGui.QAction(self)
195 actionAbout.setText('&About')
196 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
197 actionAbout.triggered.connect(self.show_about)
198
199 menubar = self.menuBar()
200 menuFile = menubar.addMenu('&File')
201 menuFile.addAction(actionQuit)
202 menuHelp = menubar.addMenu('&Help')
203 menuHelp.addAction(actionAbout)
204
205 self.lblValue = QtGui.QLabel('waiting for data...')
206 self.lblValue.setAlignment(QtCore.Qt.AlignCenter)
207 font = self.lblValue.font()
208 font.setPointSize(font.pointSize() * 1.7)
209 font.setBold(True)
210 self.lblValue.setFont(font)
211 self.setCentralWidget(self.lblValue)
212 self.centralWidget().setContentsMargins(0, 0, 0, 0)
213
214 self.lblDevName = QtGui.QLabel()
215 self.lblDevName.setToolTip('Name of used measurement device.')
216 self.statusBar().insertWidget(0, self.lblDevName, 10)
217 self.lblTime = QtGui.QLabel()
218 self.lblTime.setToolTip('Time of the last measurement.')
219 self.statusBar().insertWidget(1, self.lblTime)
220
221 self.statusBar().setSizeGripEnabled(False)
222
223 def show_about(self):
224 text = textwrap.dedent('''\
225 <div align="center">
226 <b>sigrok-meter</b><br/>
227 0.1.0<br/>
228 Using libsigrok {} (lib version {}).<br/>
229 <a href='http://www.sigrok.org'>
230 http://www.sigrok.org</a><br/>
231 <br/>
232 This program comes with ABSOLUTELY NO WARRANTY;<br/>
233 for details visit
234 <a href='http://www.gnu.org/licenses/gpl.html'>
235 http://www.gnu.org/licenses/gpl.html</a>
236 </div>
237 '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
238
239 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
240
241 def update(self, device, value):
242 '''Updates the labels with new measurement values.'''
243
244 n = datetime.datetime.now().time()
245 now = '{:02}:{:02}:{:02}.{:03}'.format(
246 n.hour, n.minute, n.second, n.microsecond / 1000)
247
248 self.lblValue.setText(value)
249 self.lblDevName.setText(device)
250 self.lblTime.setText(now)
251
252def parse_cli():
253 parser = argparse.ArgumentParser(
254 description='Simple sigrok GUI for multimeters and dataloggers.',
255 epilog=textwrap.dedent('''\
256 The DRIVER string is the same as for sigrok-cli(1).
257
258 examples:
259
260 %(prog)s --driver tecpel-dmm-8061-ser:conn=/dev/ttyUSB0
261
262 %(prog)s --driver uni-t-ut61e:conn=1a86.e008
263 '''),
264 formatter_class=argparse.RawDescriptionHelpFormatter)
265
266 parser.add_argument('-d', '--driver',
267 action='append',
268 help='The driver to use')
269 parser.add_argument('-l', '--loglevel',
270 type=int,
271 help='Set loglevel (5 is most verbose)')
272 args = parser.parse_args()
273
274 result = {
275 'drivers': default_drivers,
276 'loglevel': default_loglevel
277 }
278
279 if args.driver:
280 result['drivers'] = []
281 for d in args.driver:
282 m = re.match('(?P<name>[^:]+)(?P<opts>(:[^:=]+=[^:=]+)*)', d)
283 if not m:
284 sys.exit('error parsing option "{}"'.format(d))
285
286 opts = m.group('opts').split(':')[1:]
287 opts = [tuple(kv.split('=')) for kv in opts]
288 opts = dict(opts)
289
290 result['drivers'].append((m.group('name'), opts))
291
292 if args.loglevel != None:
293 try:
294 result['loglevel'] = sr.LogLevel.get(args.loglevel)
295 except:
296 sys.exit('error: invalid log level')
297
298 return result
299
300if __name__ == '__main__':
301 args = parse_cli()
302
303 thread = SamplingThread(args['drivers'], args['loglevel'])
304
305 app = QtGui.QApplication([])
306 s = SigrokMeter(thread)
307 s.show()
308
309 r = app.exec_()
310 thread.stop()
311 sys.exit(r)