4 ## This file is part of the sigrok-meter project.
6 ## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
7 ## Copyright (C) 2014 Jens Steinhauser <jens.steinhauser@gmail.com>
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.
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.
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
28 import sigrok.core as sr
32 default_drivers = [('demo', {'analog_channels': 1})]
33 default_loglevel = sr.LogLevel.WARN
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).
43 %(prog)s --driver tecpel-dmm-8061-ser:conn=/dev/ttyUSB0
45 %(prog)s --driver uni-t-ut61e:conn=1a86.e008
47 formatter_class=argparse.RawDescriptionHelpFormatter)
49 parser.add_argument('-d', '--driver',
51 help='The driver to use')
52 parser.add_argument('-l', '--loglevel',
54 help='Set loglevel (5 is most verbose)')
55 parser.add_argument('--pyside',
58 help='Force use of PySide (default is to use PyQt4)')
59 args = parser.parse_args()
62 'drivers': default_drivers,
63 'loglevel': default_loglevel,
68 result['drivers'] = []
70 m = re.match('(?P<name>[^:]+)(?P<opts>(:[^:=]+=[^:=]+)*)', d)
72 sys.exit('error parsing option "{}"'.format(d))
74 opts = m.group('opts').split(':')[1:]
75 opts = [tuple(kv.split('=')) for kv in opts]
78 result['drivers'].append((m.group('name'), opts))
80 if args.loglevel != None:
82 result['loglevel'] = sr.LogLevel.get(args.loglevel)
84 sys.exit('error: invalid log level')
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.
98 from PySide import QtCore, QtGui
99 qt_signal = QtCore.Signal
102 from PyQt4 import QtCore, QtGui
103 qt_signal = QtCore.pyqtSignal
105 sys.stderr.write('import of PyQt4 failed, using PySide\n')
106 from PySide import QtCore, QtGui
107 qt_signal = QtCore.Signal
109 class SamplingThread(QtCore.QObject):
110 '''A class that handles the reception of sigrok packets in the background.'''
112 class Worker(QtCore.QObject):
113 '''Helper class that does the actual work in another thread.'''
115 '''Signal emitted when new data arrived.'''
116 measured = qt_signal(object)
118 def __init__(self, drivers, loglevel):
119 super(self.__class__, self).__init__()
121 self.context = sr.Context_create()
122 self.context.log_level = loglevel
124 self.sr_pkg_version = self.context.package_version
125 self.sr_lib_version = self.context.lib_version
128 for name, options in drivers:
130 dr = self.context.drivers[name]
131 self.devices.append(dr.scan(**options)[0])
133 print('error getting device for driver "{}", skipping'.format(name))
136 print('no devices found')
138 def start_sampling(self):
139 self.session = self.context.create_session()
140 for dev in self.devices:
141 self.session.add_device(dev)
143 self.session.add_datafeed_callback(self.callback)
147 def stop_sampling(self):
150 def callback(self, device, packet):
151 if packet.type == sr.PacketType.ANALOG:
152 dev = '{} {}'.format(device.vendor, device.model)
154 # only send the most recent value
155 mag = packet.payload.data[0][-1]
157 self.measured.emit((dev, mag, packet.payload.unit,
158 packet.payload.mq_flags))
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)
164 # signal used to start the worker across threads
165 _start_signal = qt_signal()
167 def __init__(self, drivers, loglevel):
168 super(self.__class__, self).__init__()
170 self.worker = self.Worker(drivers, loglevel)
171 self.thread = QtCore.QThread()
172 self.worker.moveToThread(self.thread)
174 self._start_signal.connect(self.worker.start_sampling)
176 self.measured = self.worker.measured
181 '''Starts sampling'''
182 self._start_signal.emit()
185 '''Stops sampling and the background thread.'''
186 self.worker.stop_sampling()
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)
192 def sr_pkg_version(self):
193 '''Returns the version number of the libsigrok package.'''
194 return self.worker.sr_pkg_version
196 def sr_lib_version(self):
197 '''Returns the version number fo the libsigrok library.'''
198 return self.worker.sr_lib_version
200 class SigrokMeter(QtGui.QMainWindow):
201 '''The main window of the application.'''
203 def __init__(self, thread):
204 super(SigrokMeter, self).__init__()
207 self.inf = float('inf')
210 self.thread.measured.connect(self.update, QtCore.Qt.QueuedConnection)
214 self.setWindowTitle('sigrok-meter')
215 self.setMinimumHeight(130)
216 self.setMinimumWidth(260)
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))
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)
228 actionAbout = QtGui.QAction(self)
229 actionAbout.setText('&About')
230 actionAbout.setIcon(QtGui.QIcon.fromTheme('help-about'))
231 actionAbout.triggered.connect(self.show_about)
233 menubar = self.menuBar()
234 menuFile = menubar.addMenu('&File')
235 menuFile.addAction(actionQuit)
236 menuHelp = menubar.addMenu('&Help')
237 menuHelp.addAction(actionAbout)
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)
244 self.lblValue.setFont(font)
245 self.setCentralWidget(self.lblValue)
246 self.centralWidget().setContentsMargins(0, 0, 0, 0)
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)
255 self.statusBar().setSizeGripEnabled(False)
257 def show_about(self):
258 text = textwrap.dedent('''\
260 <b>sigrok-meter</b><br/>
262 Using libsigrok {} (lib version {}).<br/>
263 <a href='http://www.sigrok.org'>
264 http://www.sigrok.org</a><br/>
266 This program comes with ABSOLUTELY NO WARRANTY;<br/>
268 <a href='http://www.gnu.org/licenses/gpl.html'>
269 http://www.gnu.org/licenses/gpl.html</a>
271 '''.format(self.thread.sr_pkg_version(), self.thread.sr_lib_version()))
273 QtGui.QMessageBox.about(self, 'About sigrok-meter', text)
275 def format_unit(self, u):
279 sr.Unit.OHM: u'\u03A9',
282 sr.Unit.CELSIUS: u'\u00B0C',
283 sr.Unit.FAHRENHEIT: u'\u00B0F',
285 sr.Unit.PERCENTAGE: '%',
288 sr.Unit.SIEMENS: 'S',
289 sr.Unit.DECIBEL_MW: 'dBu',
290 sr.Unit.DECIBEL_VOLT: 'dBV',
292 sr.Unit.DECIBEL_SPL: 'dB',
293 # sr.Unit.CONCENTRATION
294 sr.Unit.REVOLUTIONS_PER_MINUTE: 'rpm',
295 sr.Unit.VOLT_AMPERE: 'VA',
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',
305 return units.get(u, '')
307 def format_mqflags(self, mqflags):
308 if sr.QuantityFlag.AC in mqflags:
310 elif sr.QuantityFlag.DC in mqflags:
317 def format_mag(self, mag):
320 return '{:f}'.format(mag)
322 def update(self, data):
323 '''Updates the labels with new measurement values.'''
325 device, mag, unit, mqflags = data
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])
332 n = datetime.datetime.now().time()
333 now = '{:02}:{:02}:{:02}.{:03}'.format(
334 n.hour, n.minute, n.second, n.microsecond / 1000)
336 self.lblValue.setText(value)
337 self.lblDevName.setText(device)
338 self.lblTime.setText(now)
340 if __name__ == '__main__':
341 thread = SamplingThread(args['drivers'], args['loglevel'])
343 app = QtGui.QApplication([])
344 s = SigrokMeter(thread)