├── .gitignore ├── README.md ├── setup.py └── ut61e ├── __init__.py ├── es51922.py ├── he2325u_hidapi.py └── he2325u_pyusb.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Measurement files of the es51922.py utility 2 | measurement_*.csv 3 | 4 | ### General Python stuff 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | bin/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .coverage 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | 42 | # Translations 43 | *.mo 44 | 45 | # Mr Developer 46 | .mr.developer.cfg 47 | .project 48 | .pydevproject 49 | 50 | # Rope 51 | .ropeproject 52 | 53 | # Django stuff: 54 | *.log 55 | *.pot 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### ut61e (Python) 3 | 4 | This is a Python package helping you to capture and interpret data from 5 | the digital multimeter Uni-T UT61E. You can easily install it via `pip`. 6 | 7 | #### Tools which this package provides: 8 | 9 | ##### `es51922` – Interprets the output of the ES51922 chip 10 | 11 | This utility interprets data sent by the Cyrustek ES51922 chip 12 | used in the Uni-Trend digital multimeter UT61E. 13 | It reads lines from stdin, tries to interpret them as messages 14 | from the chip and prints basic information on the stdout. 15 | In addition it writes a CSV file with a lot more information 16 | to the working directory. 17 | 18 | ##### `he2325u_hidapi` – Reads from the USB/HID adapter cable using HIDAPI 19 | 20 | This tool tries to read from the adapter cable using the HID API 21 | provided by the operating system. It relies on [cython-hidapi][]. 22 | 23 | The tool is called after the original chip from the USB/HID cables 24 | which was the *Hoitek HE2325U*. Nowadays those cables come with a 25 | newer chip called *WCH CH9325* but the way to get data out of them 26 | didn't change. 27 | 28 | This tool prints its output to stdout so that you can directly 29 | pipe it into `es51922`. 30 | Works on Linux and Mac OS X (Windows not tested) without root access. 31 | On Linux you may have to [create a udev rule][] in order to get access 32 | to the `/dev/hidrawX` device as a regular user. 33 | 34 | 35 | ##### `he2325u_pyusb` – Reads from the USB/HID adapter cable using PyUSB 36 | 37 | This tool is very much similar to `he2325u_hidapi` as it also 38 | allows to read from the USB/HID adapter cable. It also prints its 39 | output to stdout. It uses PyUSB instead of HIDAPI which in turn uses 40 | direct libusb calls to talk to the adapter. **Needs to be run as root.** 41 | Works on Linux only. 42 | 43 | #### Installation 44 | 45 | This Python package is registered on PyPI with the name 46 | [ut61e](https://pypi.python.org/pypi/ut61e). 47 | To install it, simply run 48 | 49 | pip install ut61e 50 | 51 | #### Usage 52 | 53 | To read data from the USB/HID adapter cable and interpret 54 | it as Cyrustek ES51922 information, you can do: 55 | 56 | he2325u_hidapi | es51922 57 | 58 | #### Requirements 59 | 60 | You need either Python2 or Python3 to run this software. 61 | 62 | If you want to run `he2325u_hidapi`, you need [cython-hidapi][]. 63 | 64 | If you want to run `he2325u_pyusb`, you need [PyUSB][]. 65 | 66 | To analyze output using `es51922` you don't need any external modules. 67 | 68 | #### Software using this Package 69 | 70 | I also wrote a web interface for the display of the UT61E. 71 | I put it in the repository [ut61e-web][] 72 | on Github. It relies on the tools from this package. 73 | 74 | #### Alternatives 75 | 76 | There is also a C++ based software out there which can read and interpret 77 | the data from the digital multimeter. The older version is called 78 | *dmm_ut61e* and the newer version *ut61e-linux-sw*, both of which 79 | you can find in my repository [ut61e_cpp][] on Github. 80 | 81 | If you run Windows, you may be better off with 82 | [DMM.exe](http://www-user.tu-chemnitz.de/~heha/hs/UNI-T/), 83 | an open source tool provided by Henrik Haftmann. 84 | 85 | #### Acknowledgement 86 | 87 | The file es51922.py was originally written by Domas Jokubauskis ([1][]) 88 | and was reused in this project. I'm very grateful to his work and 89 | the work of many others spent on analyzing the USB/HID interface and the 90 | protocol, including Steffen Vogel ([2][]) and Henrik Haftmann ([3][]). 91 | 92 | #### Licence and Authors 93 | 94 | This software is licenced under the LGPL2+ 95 | 96 | Authors: 97 | 98 | * Philipp Klaus () 99 | * Domas Jokubauskis () 100 | 101 | [cython-hidapi]: https://github.com/trezor/cython-hidapi 102 | [PyUSB]: https://github.com/walac/pyusb 103 | [create a udev rule]: https://github.com/signal11/hidapi/blob/master/udev/99-hid.rules 104 | [ut61e-web]: https://github.com/pklaus/ut61e-web/ 105 | [ut61e_cpp]: https://github.com/pklaus/ut61e_cpp 106 | [1]: https://bitbucket.org/kuzavas/dmm_es51922/ 107 | [2]: http://www.noteblok.net/2009/11/29/uni-trend-ut61e-digital-multimeter/ 108 | [3]: http://www-user.tu-chemnitz.de/~heha/bastelecke/Rund%20um%20den%20PC/hid-ser 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | try: 7 | import pypandoc 8 | LDESC = pypandoc.convert('README.md', 'rst') 9 | except (IOError, ImportError, RuntimeError): 10 | LDESC = '' 11 | 12 | setup( 13 | name = 'ut61e', 14 | version = '1.0.2', 15 | description = 'Captures and Interprets Data from your Digital Multimeter Uni-T UT61E.', 16 | long_description = LDESC, 17 | author = 'Philipp Klaus', 18 | author_email = 'philipp.l.klaus@web.de', 19 | packages = ['ut61e'], 20 | entry_points = { 21 | 'console_scripts': [ 22 | 'es51922 = ut61e.es51922:main', 23 | 'he2325u_hidapi = ut61e.he2325u_hidapi:main', 24 | 'he2325u_pyusb = ut61e.he2325u_pyusb:main', 25 | ], 26 | }, 27 | url = 'https://github.com/pklaus/ut61e_python', 28 | license = 'GPL', 29 | install_requires = [], 30 | extras_require = { 31 | 'access to he2325u_hidapi.py': ["hidapi >= 0.7.0-1"], 32 | 'access to he2325u_pyusb.py': ["pyusb >= 1.0.0"], 33 | }, 34 | keywords = 'UNI-T UT61E DMM digital multimeter', 35 | classifiers = [ 36 | 'Development Status :: 4 - Beta', 37 | 'Operating System :: OS Independent', 38 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 2', 41 | 'Programming Language :: Python :: 2.7', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.3', 44 | 'Topic :: System :: Hardware :: Hardware Drivers', 45 | 'Topic :: Scientific/Engineering', 46 | ] 47 | ) 48 | 49 | 50 | -------------------------------------------------------------------------------- /ut61e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pklaus/ut61e_python/18cfa802dace60822a340c48aa478c7df158eb7f/ut61e/__init__.py -------------------------------------------------------------------------------- /ut61e/es51922.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utility for parsing data from multimeters based on Cyrustek ES51922 chipset. 5 | 6 | Written using as much information from the datasheet as possible 7 | (some functionality is not documented). 8 | The utility should output only sensible measurements and checks if 9 | the data packet is valid (there is no check sum in the data packet). 10 | 11 | Tested with UNI-T UT61E multimeter. 12 | All the functionality of UNI-T UT61E seems to work fine. 13 | Not tested: temperature and ADP modes. 14 | 15 | Licenced LGPL2+ 16 | Copyright 17 | (C) 2013 Domas Jokubauskis (domas@jokubauskis.lt) 18 | (C) 2014 Philipp Klaus (philipp.l.klaus@web.de) 19 | 20 | Some information was used from dmmut61e utility by Steffen Vogel 21 | """ 22 | 23 | from __future__ import print_function 24 | import sys 25 | from decimal import Decimal 26 | import struct 27 | import logging 28 | import datetime 29 | 30 | def test_bit(int_type, offset): 31 | """ 32 | testBit() returns True if the bit at 'offset' is one. 33 | From http://wiki.python.org/moin/BitManipulation 34 | """ 35 | mask = 1 << offset 36 | return bool(int_type & mask) 37 | 38 | def get_bits(int_type, template): 39 | """ 40 | Extracts 'named bits' from int_type. 41 | Naming the bits works by supplying a list of 42 | bit names (or fixed bits as 0/1) via template. 43 | """ 44 | bits = {} 45 | for i in range(7): 46 | bit = test_bit(int_type, i) 47 | bit_name = template[6-i] 48 | #print(bit, bit_name, i) 49 | if bit_name in (0,1) and bit==bit_name: 50 | continue 51 | elif bit_name in (0,1): 52 | raise ValueError 53 | else: 54 | bits[bit_name] = bit 55 | return bits 56 | 57 | """ 58 | The entries in the following RANGE dictionaries have the following structure: 59 | 60 | (value_multiplier, dp_digit_position, display_unit) 61 | 62 | value_multiplier: Multiply the displayed value by this factor to get the value in base units. 63 | dp_digit_position: The digit position of the decimal point in the displayed meter reading value. 64 | display_unit: The unit the displayed value is shown in. 65 | """ 66 | 67 | RANGE_VOLTAGE = { 68 | 0b0110000: (1e0, 4, "V"), #2.2000V 69 | 0b0110001: (1e0, 3, "V"), #22.000V 70 | 0b0110010: (1e0, 2, "V"), #220.00V 71 | 0b0110011: (1e0, 1, "V"), #2200.0V 72 | 0b0110100: (1e-3, 2,"mV"), #220.00mV 73 | } 74 | 75 | # undocumented in datasheet 76 | RANGE_CURRENT_AUTO_UA = { 77 | 0b0110000: (1e-6, 2, "µA"), # 78 | 0b0110001: (1e-6, 1, "µA"), #2 79 | } 80 | # undocumented in datasheet 81 | RANGE_CURRENT_AUTO_MA = { 82 | 0b0110000: (1e-3, 3, "mA"), # 83 | 0b0110001: (1e-3, 2, "mA"), #2 84 | } 85 | 86 | RANGE_CURRENT_AUTO = { #2-range auto A *It includes auto μA, mA, 22.000A/220.00A, 220.00A/2200.0A. 87 | 0b0110000: "Lower Range (IVSL)", #Current measurement input for 220μA, 22mA. 88 | 0b0110001: "Higher Range (IVSH)" #Current measurement input for 2200μA, 220mA and 22A modes. 89 | } 90 | RANGE_CURRENT_22A = { 0b0110000: (1e0, 3, "A") } #22.000 A 91 | 92 | RANGE_CURRENT_MANUAL = { 93 | 0b0110000: (1e0, 4, "A"), #2.2000A 94 | 0b0110001: (1e0, 3, "A"), #22.000A 95 | 0b0110010: (1e0, 2, "A"), #220.00A 96 | 0b0110011: (1e0, 1, "A"), #2200.0A 97 | 0b0110100: (1e0, 0, "A"), #22000A 98 | } 99 | 100 | RANGE_ADP = { 101 | 0b0110000: "ADP4", 102 | 0b0110001: "ADP3", 103 | 0b0110010: "ADP2", 104 | 0b0110011: "ADP1", 105 | 0b0110100: "ADP0", 106 | } 107 | 108 | RANGE_RESISTANCE = { 109 | 0b0110000: (1e0, 2, "Ω"), #220.00Ω 110 | 0b0110001: (1e3, 4, "kΩ"), #2.2000KΩ 111 | 0b0110010: (1e3, 3, "kΩ"), #22.000KΩ 112 | 0b0110011: (1e3, 2, "kΩ"), #220.00KΩ 113 | 0b0110100: (1e6, 4, "MΩ"), #2.2000MΩ 114 | 0b0110101: (1e6, 3, "MΩ"), #22.000MΩ 115 | 0b0110110: (1e6, 2, "MΩ"), #220.00MΩ 116 | } 117 | 118 | RANGE_FREQUENCY = { 119 | 0b0110000: (1e0, 2, "Hz"), #22.00Hz 120 | 0b0110001: (1e0, 1, "Hz"), #220.0Hz 121 | #0b0110010 122 | 0b0110011: (1e3, 3, "kHz"), #22.000KHz 123 | 0b0110100: (1e3, 2, "kHz"), #220.00KHz 124 | 0b0110101: (1e6, 4, "MHz"), #2.2000MHz 125 | 0b0110110: (1e6, 3, "MHz"), #22.000MHz 126 | 0b0110111: (1e6, 2, "MHz"), #220.00MHz 127 | } 128 | 129 | RANGE_CAPACITANCE = { 130 | 0b0110000: (1e-9, 3, "nF"), #22.000nF 131 | 0b0110001: (1e-9, 2, "nF"), #220.00nF 132 | 0b0110010: (1e-6, 4, "µF"), #2.2000μF 133 | 0b0110011: (1e-6, 3, "µF"), #22.000μF 134 | 0b0110100: (1e-6, 2, "µF"), #220.00μF 135 | 0b0110101: (1e-3, 4, "mF"), #2.2000mF 136 | 0b0110110: (1e-3, 3, "mF"), #22.000mF 137 | 0b0110111: (1e-3, 2, "mF"), #220.00mF 138 | } 139 | 140 | # When the meter operates in continuity mode or diode mode, this packet is always 141 | # 0110000 since the full-scale ranges in these modes are fixed. 142 | RANGE_DIODE = { 143 | 0b0110000: (1e0, 4, "V"), #2.2000V 144 | } 145 | RANGE_CONTINUITY = { 146 | 0b0110000: (1e0, 2, "Ω"), #220.00Ω 147 | } 148 | 149 | FUNCTION = { 150 | # (function, subfunction, unit) 151 | 0b0111011: ("voltage", RANGE_VOLTAGE, "V"), 152 | 0b0111101: ("current", RANGE_CURRENT_AUTO_UA, "A"), #Auto μA Current / Auto μA Current / Auto 220.00A/2200.0A 153 | 0b0111111: ("current", RANGE_CURRENT_AUTO_MA, "A"), #Auto mA Current Auto mA Current Auto 22.000A/220.00A 154 | 0b0110000: ("current", RANGE_CURRENT_22A, "A"), #22 A current 155 | 0b0111001: ("current", RANGE_CURRENT_MANUAL, "A"), #Manual A Current 156 | 0b0110011: ("resistance", RANGE_RESISTANCE, "Ω"), 157 | 0b0110101: ("continuity", RANGE_CONTINUITY, "Ω"), 158 | 0b0110001: ("diode", RANGE_DIODE, "V"), 159 | 0b0110010: ("frequency", RANGE_FREQUENCY, "Hz"), 160 | 0b0110110: ("capacitance", RANGE_CAPACITANCE, "F"), 161 | 0b0110100: ("temperature", None, "deg"), 162 | 0b0111110: ("ADP", RANGE_ADP, ""), 163 | } 164 | 165 | DIGITS = { 166 | 0b0110000: 0, 167 | 0b0110001: 1, 168 | 0b0110010: 2, 169 | 0b0110011: 3, 170 | 0b0110100: 4, 171 | 0b0110101: 5, 172 | 0b0110110: 6, 173 | 0b0110111: 7, 174 | 0b0111000: 8, 175 | 0b0111001: 9, 176 | } 177 | 178 | STATUS = [ 179 | 0, 1, 1, 180 | "JUDGE", # 1-°C, 0-°F. 181 | "SIGN", # 1-minus sign, 0-no sign 182 | "BATT", # 1-battery low 183 | "OL", # input overflow 184 | ] 185 | 186 | OPTION1 = [ 187 | 0, 1, 1, 188 | "MAX", # maximum 189 | "MIN", # minimum 190 | "REL", # relative/zero mode 191 | "RMR", # current value 192 | ] 193 | 194 | OPTION2 = [ 195 | 0, 1, 1, 196 | "UL", # 1 -at 22.00Hz <2.00Hz., at 220.0Hz <20.0Hz,duty cycle <10.0%. 197 | "PMAX", # maximum peak value 198 | "PMIN", # minimum peak value 199 | 0, 200 | ] 201 | 202 | OPTION3 = [ 203 | 0, 1, 1, 204 | "DC", # DC measurement mode, either voltage or current. 205 | "AC", # AC measurement mode, either voltage or current. 206 | "AUTO", # 1-automatic mode, 0-manual 207 | "VAHZ", 208 | ] 209 | 210 | OPTION4 = [ 211 | 0, 1, 1, 0, 212 | "VBAR", # 1-VBAR pin is connected to V-. 213 | "HOLD", # hold mode 214 | "LPF", #low-pass-filter feature is activated. 215 | ] 216 | 217 | def parse(packet, extended_format = False): 218 | """ 219 | The most important function of this module: 220 | Parses 12-byte-long packets from the UT61E DMM and returns 221 | a dictionary with all information extracted from the packet. 222 | """ 223 | d_range, \ 224 | d_digit4, d_digit3, d_digit2, d_digit1, d_digit0, \ 225 | d_function, d_status, \ 226 | d_option1, d_option2, d_option3, d_option4 = struct.unpack("B"*12, packet) 227 | 228 | options = {} 229 | d_options = (d_status, d_option1, d_option2, d_option3, d_option4) 230 | OPTIONS = (STATUS, OPTION1, OPTION2, OPTION3, OPTION4) 231 | for d_option, OPTION in zip(d_options, OPTIONS): 232 | bits = get_bits(d_option, OPTION) 233 | options.update(bits) 234 | 235 | function = FUNCTION[d_function] 236 | # When the rotary switch is set to 'voltage' or 'ampere' mode and then you 237 | # press the frequency button, the meter shows 'Hz' (or '%') but the 238 | # function byte is still the same as before so we have to correct for that: 239 | if options["VAHZ"]: 240 | function = FUNCTION[0b0110010] 241 | mode = function[0] 242 | m_range = function[1][d_range] 243 | unit = function[2] 244 | if mode == "frequency" and options["JUDGE"]: 245 | mode = "duty_cycle" 246 | unit = "%" 247 | m_range = (1e0, 1, "%") #2200.0°C 248 | 249 | current = None 250 | if options["AC"] and options["DC"]: 251 | raise ValueError 252 | elif options["DC"]: 253 | current = "DC" 254 | elif options["AC"]: 255 | current = "AC" 256 | 257 | operation = "normal" 258 | # sometimes there a glitch where both UL and OL are enabled in normal operation 259 | # so no error is raised when it occurs 260 | if options["UL"]: 261 | operation = "underload" 262 | elif options["OL"]: 263 | operation = "overload" 264 | 265 | if options["AUTO"]: 266 | mrange = "auto" 267 | else: 268 | mrange = "manual" 269 | 270 | if options["BATT"]: 271 | battery_low = True 272 | else: 273 | battery_low = False 274 | 275 | # relative measurement mode, received value is actual! 276 | if options["REL"]: 277 | relative = True 278 | else: 279 | relative = False 280 | 281 | # data hold mode, received value is actual! 282 | if options["HOLD"]: 283 | hold = True 284 | else: 285 | hold = False 286 | 287 | peak = None 288 | if options["MAX"]: 289 | peak = "max" 290 | elif options["MIN"]: 291 | peak = "min" 292 | 293 | if mode == "current" and options["VBAR"]: 294 | pass 295 | """Auto μA Current 296 | Auto mA Current""" 297 | elif mode == "current" and not options["VBAR"]: 298 | pass 299 | """Auto 220.00A/2200.0A 300 | Auto 22.000A/220.00A""" 301 | 302 | if mode == "temperature" and options["VBAR"]: 303 | m_range = (1e0, 1, "deg") #2200.0°C 304 | elif mode == "temperature" and not options["VBAR"]: 305 | m_range = (1e0, 2, "deg") #220.00°C and °F 306 | 307 | digits = [d_digit4, d_digit3, d_digit2, d_digit1, d_digit0] 308 | digits = [DIGITS[digit] for digit in digits] 309 | 310 | display_value = 0 311 | for i, digit in zip(range(5), digits): 312 | display_value += digit*(10**(4-i)) 313 | if options["SIGN"]: display_value = -display_value 314 | display_value = Decimal(display_value) / 10**m_range[1] 315 | display_unit = m_range[2] 316 | value = float(display_value) * m_range[0] 317 | 318 | if operation != "normal": 319 | display_value = "" 320 | value = "" 321 | results = { 322 | 'value' : value, 323 | 'unit' : unit, 324 | 'display_value' : display_value, 325 | 'display_unit' : display_unit, 326 | 'mode' : mode, 327 | 'current' : current, 328 | 'peak' : peak, 329 | 'relative' : relative, 330 | 'hold' : hold, 331 | 'range' : mrange, 332 | 'operation' : operation, 333 | 'battery_low' : battery_low 334 | } 335 | 336 | detailed_results = { 337 | 'packet_details' : { 338 | 'raw_data_binary' : packet, 339 | 'raw_data_hex' : ' '.join('0x{:02X}'.format(x) for x in packet), 340 | 'data_bytes' : { 341 | 'd_range' : d_range, 342 | 'd_digit4' : d_digit4, 343 | 'd_digit3' : d_digit3, 344 | 'd_digit2' : d_digit2, 345 | 'd_digit1' : d_digit1, 346 | 'd_digit0' : d_digit0, 347 | 'd_function' : d_function, 348 | 'd_status' : d_status, 349 | 'd_option1' : d_option1, 350 | 'd_option2' : d_option2, 351 | 'd_option3' : d_option3, 352 | 'd_option4' : d_option4 353 | }, 354 | 'options' : options, 355 | 'range' : { 356 | 'value_multiplier' : m_range[0], 357 | 'dp_digit_position' : m_range[1], 358 | 'display_unit' : m_range[2] 359 | } 360 | }, 361 | 'display_value' : str(display_value) 362 | } 363 | if extended_format: 364 | results.update(detailed_results) 365 | return results 366 | 367 | return results 368 | 369 | def output_readable(results): 370 | operation = results["operation"] 371 | battery_low = results["battery_low"] 372 | if operation == "normal": 373 | display_value = results["display_value"] 374 | display_unit = results["display_unit"] 375 | line = "{value} {unit}".format(value=display_value, unit=display_unit) 376 | else: 377 | line = "-, the measurement is {operation}ed!".format(operation=operation) 378 | if battery_low: 379 | line.append(" Battery low!") 380 | return line 381 | 382 | def format_field(results, field_name): 383 | """ 384 | Helper function for output formatting. 385 | """ 386 | value = results[field_name] 387 | if field_name == "value": 388 | if results["operation"]=="normal": 389 | return str(value) 390 | else: 391 | return "" 392 | if value==None: 393 | return "" 394 | elif value==True: 395 | return "1" 396 | elif value==False: 397 | return "0" 398 | else: 399 | return str(value) 400 | 401 | CSV_FIELDS = ["value", "unit", "mode", "current", "operation", "peak", 402 | "battery_low", "relative", "hold"] 403 | def output_csv(results): 404 | """ 405 | Helper function to write output lines to a CSV file. 406 | """ 407 | field_data = [format_field(results, field_name) for field_name in CSV_FIELDS] 408 | line = ";".join(field_data) 409 | return line 410 | 411 | def main(): 412 | """ 413 | Main function: Entry point if running this module from the command line. 414 | Reads lines from stdin and parses them as ES51922 messages. 415 | Prints to stdout and to a CSV file. 416 | """ 417 | import argparse 418 | parser = argparse.ArgumentParser(description='Utility for parsing data from multimeters based on Cyrustek ES51922 chipset.') 419 | parser.add_argument('-m', '--mode', choices=['csv', 'readable'], 420 | default="csv", 421 | help='output mode (default: csv)') 422 | parser.add_argument('-f', '--file', 423 | help='output file') 424 | parser.add_argument('--verbose', action='store_true', 425 | help='enable verbose output') 426 | args = parser.parse_args() 427 | 428 | if args.verbose: 429 | log_level = logging.DEBUG 430 | else: 431 | log_level = logging.INFO 432 | logging.basicConfig(format='%(levelname)s:%(message)s', level=log_level) 433 | 434 | output_file = None 435 | if args.mode == 'csv': 436 | timestamp = datetime.datetime.now() 437 | date_format = "%Y-%m-%d_%H:%S" 438 | timestamp = timestamp.strftime(date_format) 439 | if args.file: 440 | file_name = args.file 441 | else: 442 | file_name = "measurement_{}.csv".format(timestamp) 443 | output_file = open(file_name, "w") 444 | logging.info('Writing to file "{}"'.format(file_name)) 445 | header = "timestamp;{}\n".format(";".join(CSV_FIELDS)) 446 | output_file.write(header) 447 | while True: 448 | line = sys.stdin.readline() 449 | if not line: break 450 | line = line.strip() 451 | try: 452 | line = line.encode('ascii') 453 | except: 454 | logging.warning('Not an ASCII input line, ignoring: "{}"'.format(line)) 455 | continue 456 | timestamp = datetime.datetime.now() 457 | timestamp = timestamp.isoformat(sep=' ') 458 | if len(line)==12: 459 | try: 460 | results = parse(line) 461 | except Exception as e: 462 | logging.warning('Error "{}" packet from multimeter: "{}"'.format(e, line)) 463 | if args.mode == 'csv': 464 | line = output_csv(results) 465 | output_file.write("{};{}\n".format(timestamp, line)) 466 | elif args.mode == 'readable': 467 | pass 468 | else: 469 | raise NotImplementedError 470 | line = output_readable(results) 471 | print(timestamp.split(" ")[1], line) 472 | elif line: 473 | logging.warning('Unknown packet from multimeter: "{}"'.format(line)) 474 | else: 475 | logging.warning('Not a response from the multimeter: ""'.format(line)) 476 | 477 | if __name__ == "__main__": 478 | main() 479 | -------------------------------------------------------------------------------- /ut61e/he2325u_hidapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Read from a Hoitek HE2325U or WCH CH9325 USB/HID adapter cable 5 | using the HID-API provided by the operating system. 6 | It relies on cython-hidapi: https://github.com/trezor/cython-hidapi 7 | """ 8 | 9 | import sys 10 | 11 | BPS = 19200 12 | 13 | def main(): 14 | """ 15 | Main function: Entry point if running this module from the command line. 16 | Prints messages to stdout. 17 | """ 18 | import argparse 19 | from inspect import cleandoc 20 | parser = argparse.ArgumentParser(description=cleandoc(__doc__)) 21 | parser.add_argument('-v', '--verbose', action='store_true', help='Increase verbosity') 22 | parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') 23 | args = parser.parse_args() 24 | try: 25 | try: 26 | import hidraw as hid 27 | except ImportError: 28 | import hid 29 | except ImportError: 30 | parser.error("You need to install cython-hidapi first!") 31 | import logging 32 | loglevel = logging.WARNING 33 | if args.verbose: 34 | loglevel = logging.INFO 35 | if args.debug: 36 | loglevel = logging.DEBUG 37 | logging.basicConfig(format='%(message)s', level=loglevel) 38 | 39 | try: 40 | logging.info("Enumerating Devices") 41 | devices = hid.enumerate(0x1a86, 0xe008) 42 | if len(devices) == 0: 43 | raise NameError('No device found. Check your USB connection.') 44 | logging.info("Found {} devices: ".format(len(devices))) 45 | for dev in devices: 46 | name = dev['manufacturer_string'] + " " + dev['product_string'] 47 | path = dev['path'].decode('ascii') 48 | logging.info("* {} [{}]".format(name, path)) 49 | 50 | logging.info("Opening device") 51 | h = hid.device() 52 | try: 53 | h.open(0x1a86, 0xe008) 54 | except IOError as ex: 55 | raise NameError('Cannot open the device. Please check permissions.') 56 | 57 | buf = [0]*6 58 | buf[0] = 0x0 # report ID 59 | buf[1] = BPS & 0xFF 60 | buf[2] = ( BPS >> 8 ) & 0xFF 61 | buf[3] = ( BPS >> 16 ) & 0xFF 62 | buf[4] = ( BPS >> 24 ) & 0xFF 63 | buf[5] = 0x03 # 3 = enable? 64 | 65 | fr = h.send_feature_report(buf) 66 | if fr == -1: 67 | raise NameError("Sending Feature Report Failed") 68 | logging.debug("Feature Report Sent") 69 | 70 | try: 71 | logging.debug("Start Reading Messages") 72 | while True: 73 | #answer = h.read(256) 74 | answer = h.read(256, timeout_ms=1000) 75 | if len(answer) < 1: continue 76 | nbytes = answer[0] & 0x7 77 | if nbytes > 0: 78 | if len(answer) < nbytes+1: 79 | raise NameError("More bytes announced then sent") 80 | payload = answer[1:nbytes+1] 81 | data = [b & ( ~(1<<7) ) for b in payload] 82 | data = [chr(b) for b in data] 83 | data = ''.join(data) 84 | sys.stdout.write(data) 85 | sys.stdout.flush() 86 | except KeyboardInterrupt: 87 | logging.info("You pressed CTRL-C, stopping...") 88 | 89 | logging.debug("Closing device") 90 | h.close() 91 | 92 | except IOError as ex: 93 | logging.error(ex) 94 | except Exception as ex: 95 | logging.error(ex) 96 | 97 | if __name__ == "__main__": 98 | main() 99 | -------------------------------------------------------------------------------- /ut61e/he2325u_pyusb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Read from a Hoitek HE2325U or WCH CH9325 USB/HID adapter cable 5 | using PyUSB/libusb. You need root access to get this to work. 6 | """ 7 | 8 | import sys 9 | 10 | idVendor = 0x1a86 11 | idProduct = 0xe008 12 | interface = 0 13 | 14 | def main(): 15 | """ 16 | Main function: Entry point if running this module from the command line. 17 | Prints messages to stdout. 18 | """ 19 | import argparse 20 | from inspect import cleandoc 21 | parser = argparse.ArgumentParser(description=cleandoc(__doc__)) 22 | parser.add_argument('-v', '--verbose', action='store_true', help='Increase verbosity') 23 | parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') 24 | args = parser.parse_args() 25 | try: 26 | import usb.core 27 | import usb.util 28 | except ImportError: 29 | parser.error("You need to install PyUSB first!") 30 | import logging 31 | loglevel = logging.WARNING 32 | if args.verbose: 33 | loglevel = logging.INFO 34 | if args.debug: 35 | loglevel = logging.DEBUG 36 | logging.basicConfig(format='%(message)s', level=loglevel) 37 | 38 | try: 39 | logging.info("Looking for the USB/HID Adapter") 40 | dev = usb.core.find(idVendor=idVendor, idProduct=idProduct) 41 | 42 | if dev is None: 43 | raise NameError('Device not found') 44 | 45 | lnd = (dev.bLength, dev.bNumConfigurations, dev.bDeviceClass) 46 | logging.debug("Length: {}, # configurations: {}, device class: {}".format(*lnd)) 47 | 48 | if dev.is_kernel_driver_active(interface) is True: 49 | logging.info('Detaching kernel driver') 50 | dev.detach_kernel_driver(interface) 51 | dev.set_configuration() 52 | 53 | # get an endpoint instance 54 | cfg = dev.get_active_configuration() 55 | interface_number = cfg[(0,0)].bInterfaceNumber 56 | alternate_setting = usb.control.get_interface(dev,interface_number) 57 | intf = usb.util.find_descriptor( 58 | cfg, bInterfaceNumber = interface_number, 59 | bAlternateSetting = alternate_setting 60 | ) 61 | 62 | ep = usb.util.find_descriptor( 63 | intf, 64 | # match the first IN endpoint 65 | custom_match = \ 66 | lambda e: \ 67 | usb.util.endpoint_direction(e.bEndpointAddress) == \ 68 | usb.util.ENDPOINT_IN 69 | ) 70 | 71 | assert ep is not None 72 | em = (ep.bEndpointAddress, ep.wMaxPacketSize) 73 | logging.debug("Endpoint Address: {}, Max Packet Size: {}".format(*em)) 74 | 75 | message = [0x00, 0x4b, 0x00, 0x00, 0x03] 76 | #dev.ctrl_transfer(bmRequestType, bmRequest, wValue, wIndex, payload) 77 | assert dev.ctrl_transfer(0x21, 9, 0x0300, 0, message) 78 | logging.debug("Feature Report Sent") 79 | 80 | try: 81 | logging.info("Start Reading Messages") 82 | while True: 83 | answer = dev.read(ep.bEndpointAddress, ep.wMaxPacketSize, timeout=1000) 84 | if len(answer) < 1: continue 85 | nbytes = answer[0] & 0x7 86 | if nbytes > 0: 87 | if len(answer) < nbytes+1: 88 | raise NameError("More bytes announced then sent") 89 | payload = answer[1:nbytes+1] 90 | data = [b & ( ~(1<<7) ) for b in payload] 91 | data = [chr(b) for b in data] 92 | data = ''.join(data) 93 | sys.stdout.write(data) 94 | sys.stdout.flush() 95 | except KeyboardInterrupt: 96 | logging.info("You pressed CTRL-C, stopping...") 97 | 98 | logging.info("Closing device") 99 | h.close() 100 | 101 | except usb.core.USBError as ex: 102 | logging.error("USB Error occured: " + str(ex)) 103 | except Exception as ex: 104 | logging.error(ex) 105 | 106 | if __name__ == "__main__": 107 | main() 108 | --------------------------------------------------------------------------------