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