PageRenderTime 70ms CodeModel.GetById 44ms RepoModel.GetById 0ms app.codeStats 0ms

/dmm_es51922.py

https://bitbucket.org/kuzavas/dmm_es51922
Python | 454 lines | 390 code | 14 blank | 50 comment | 5 complexity | 3565bc2f7f49ac94f0bee7be35fdb23b MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. """
  3. Utility for parsing data from multimeters based on Cyrustek ES51922 chipset.
  4. Version 0.9
  5. Written using as much information from the datasheet as possible (some
  6. functionality is not documented).
  7. The utility should output only sensible measurements and checks if the data
  8. packet is valid (there is no check sum in the data packet).
  9. Requires pySerial library from http://pyserial.sourceforge.net/
  10. NOTE: if RS-232 to USB adapter is used make sure the DTR signal is connected
  11. in the adapter. Otherwise, there will be no received data (this is the case with
  12. UNI-T UT61E).
  13. Tested with UNI-T UT61E multimeter.
  14. All the functionality of UNI-T UT61E seems to work fine.
  15. Not tested: temperature and ADP modes.
  16. Licenced LGPL2+
  17. Copyright (C) 2013 Domas Jokubauskis (domas@jokubauskis.lt)
  18. Some information was used from dmmut61e utility by Steffen Vogel
  19. """
  20. from __future__ import print_function
  21. import serial
  22. import sys
  23. from decimal import Decimal
  24. import struct
  25. import logging
  26. import datetime
  27. """
  28. baud rate 19230
  29. single byte:
  30. 0V --| |-----------| |-|
  31. |0| D0-D6 |P|1|
  32. -3V |_|___________|_| |__
  33. LSB MSB
  34. whole packet:
  35. range digit4 digit3
  36. digit2 digit1 digit0
  37. function status option1
  38. option2 option3 option4
  39. CR LF
  40. """
  41. # http://wiki.python.org/moin/BitManipulation
  42. # testBit() returns a nonzero result, 2**offset, if the bit at 'offset' is one.
  43. def test_bit(int_type, offset):
  44. mask = 1 << offset
  45. return bool(int_type & mask)
  46. def get_bits(int_type, template):
  47. bits = {}
  48. for i in range(7):
  49. bit = test_bit(int_type, i)
  50. bit_name = template[6-i]
  51. #print(bit, bit_name, i)
  52. if bit_name in (0,1) and bit==bit_name:
  53. continue
  54. elif bit_name in (0,1):
  55. raise ValueError
  56. else:
  57. bits[bit_name] = bit
  58. return bits
  59. RANGE_VOLTAGE = {
  60. 0b0110000: (1e0, 4, "V"), #2.2000V
  61. 0b0110001: (1e0, 3, "V"), #22.000V
  62. 0b0110010: (1e0, 2, "V"), #220.00V
  63. 0b0110011: (1e0, 1, "V"), #2200.0V
  64. 0b0110100: (1e-3, 2,"mV"), #220.00mV
  65. }
  66. # undocumented in datasheet
  67. RANGE_CURRENT_AUTO_UA = {
  68. 0b0110000: (1e-6, 2, "uA"), #
  69. 0b0110001: (1e-6, 1, "uA"), #2
  70. }
  71. # undocumented in datasheet
  72. RANGE_CURRENT_AUTO_MA = {
  73. 0b0110000: (1e-3, 3, "mA"), #
  74. 0b0110001: (1e-3, 2, "mA"), #2
  75. }
  76. RANGE_CURRENT_AUTO = { #2-range auto A *It includes auto μA, mA, 22.000A/220.00A, 220.00A/2200.0A.
  77. 0b0110000: "Lower Range (IVSL)", #Current measurement input for 220μA, 22mA.
  78. 0b0110001: "Higher Range (IVSH)" #Current measurement input for 2200μA, 220mA and 22A modes.
  79. }
  80. RANGE_CURRENT_22A = { 0b0110000: (1e0, 3, "A") } #22.000 A
  81. RANGE_CURRENT_MANUAL = {
  82. 0b0110000: (1e0, 4, "A"), #2.2000A
  83. 0b0110001: (1e0, 3, "A"), #22.000A
  84. 0b0110010: (1e0, 2, "A"), #220.00A
  85. 0b0110011: (1e0, 1, "A"), #2200.0A
  86. 0b0110100: (1e0, 0, "A"), #22000A
  87. }
  88. RANGE_ADP = {
  89. 0b0110000: "ADP4",
  90. 0b0110001: "ADP3",
  91. 0b0110010: "ADP2",
  92. 0b0110011: "ADP1",
  93. 0b0110100: "ADP0",
  94. }
  95. RANGE_RESISTANCE = {
  96. 0b0110000: (1e0, 2, "W"), #220.00Ω
  97. 0b0110001: (1e3, 4, "kW"), #2.2000
  98. 0b0110010: (1e3, 3, "kW"), #22.000
  99. 0b0110011: (1e3, 2, "kW"), #220.00
  100. 0b0110100: (1e6, 4, "MW"), #2.2000
  101. 0b0110101: (1e6, 3, "MW"), #22.000
  102. 0b0110110: (1e6, 2, "MW"), #220.00
  103. }
  104. RANGE_FREQUENCY = {
  105. 0b0110000: (1e0, 1, "Hz"), #22.00Hz
  106. 0b0110001: (1e0, 1, "Hz"), #220.0Hz
  107. #0b0110010
  108. 0b0110011: (1e3, 3, "kHz"), #22.000KHz
  109. 0b0110100: (1e3, 2, "kHz"), #220.00KHz
  110. 0b0110101: (1e6, 4, "MHz"), #2.2000MHz
  111. 0b0110110: (1e6, 3, "MHz"), #22.000MHz
  112. 0b0110111: (1e6, 2, "MHz"), #220.00MHz
  113. }
  114. RANGE_CAPACITANCE = {
  115. 0b0110000: (1e-9, 3, "nF"), #22.000nF
  116. 0b0110001: (1e-9, 2, "nF"), #220.00nF
  117. 0b0110010: (1e-6, 4, "uF"), #2.2000μF
  118. 0b0110011: (1e-6, 3, "uF"), #22.000μF
  119. 0b0110100: (1e-6, 2, "uF"), #220.00μF
  120. 0b0110101: (1e-3, 4, "mF"), #2.2000mF
  121. 0b0110110: (1e-3, 3, "mF"), #22.000mF
  122. 0b0110111: (1e-3, 2, "mF"), #220.00mF
  123. }
  124. # When the meter operates in continuity mode or diode mode, this packet is always
  125. # 0110000 since the full-scale ranges in these modes are fixed.
  126. RANGE_DIODE = {
  127. 0b0110000: (1e0, 4, "V"), #2.2000V
  128. }
  129. RANGE_CONTINUITY = {
  130. 0b0110000: (1e0, 2, "W"), #220.00Ω
  131. }
  132. FUNCTION = {
  133. # (function, subfunction, unit)
  134. 0b0111011: ("voltage", RANGE_VOLTAGE, "V"),
  135. 0b0111101: ("current", RANGE_CURRENT_AUTO_UA, "A"), #Auto μA Current / Auto μA Current / Auto 220.00A/2200.0A
  136. 0b0111111: ("current", RANGE_CURRENT_AUTO_MA, "A"), #Auto mA Current Auto mA Current Auto 22.000A/220.00A
  137. 0b0110000: ("current", RANGE_CURRENT_22A, "A"), #22 A current
  138. 0b0111001: ("current", RANGE_CURRENT_MANUAL, "A"), #Manual A Current
  139. 0b0110011: ("resistance", RANGE_RESISTANCE, "W"),
  140. 0b0110101: ("continuity", RANGE_CONTINUITY, "W"),
  141. 0b0110001: ("diode", RANGE_DIODE, "V"),
  142. 0b0110010: ("frequency", RANGE_FREQUENCY, "Hz"),
  143. 0b0110110: ("capacitance", RANGE_CAPACITANCE, "F"),
  144. 0b0110100: ("temperature", None, "deg"),
  145. 0b0111110: ("ADP", RANGE_ADP, ""),
  146. }
  147. DIGITS = {
  148. 0b0110000: 0,
  149. 0b0110001: 1,
  150. 0b0110010: 2,
  151. 0b0110011: 3,
  152. 0b0110100: 4,
  153. 0b0110101: 5,
  154. 0b0110110: 6,
  155. 0b0110111: 7,
  156. 0b0111000: 8,
  157. 0b0111001: 9,
  158. }
  159. STATUS = [
  160. 0, 1, 1,
  161. "Judge", # 1-°C, 0-°F.
  162. "Sign", # 1-minus sign, 0-no sign
  163. "BATT", # 1-battery low
  164. "OL", # input overflow
  165. ]
  166. OPTION1 = [
  167. 0, 1, 1,
  168. "MAX", # maximum
  169. "MIN", # minimum
  170. "REL", # relative/zero mode
  171. "RMR", # current value
  172. ]
  173. OPTION2 = [
  174. 0, 1, 1,
  175. "UL", # 1 -at 22.00Hz <2.00Hz., at 220.0Hz <20.0Hz,duty cycle <10.0%.
  176. "PMAX", # maximum peak value
  177. "PMIN", # minimum peak value
  178. 0,
  179. ]
  180. OPTION3 = [
  181. 0, 1, 1,
  182. "DC", # DC measurement mode, either voltage or current.
  183. "AC", # AC measurement mode, either voltage or current.
  184. "AUTO", # 1-automatic mode, 0-manual
  185. "VAHZ",
  186. ]
  187. OPTION4 = [
  188. 0, 1, 1, 0,
  189. "VBAR", # 1-VBAR pin is connected to V-.
  190. "Hold", # hold mode
  191. "LPF", #low-pass-filter feature is activated.
  192. ]
  193. def parse(packet):
  194. #packet = [ord(byte) for byte in packet]
  195. d_range, \
  196. d_digit4, d_digit3, d_digit2, d_digit1, d_digit0, \
  197. d_function, d_status, \
  198. d_option1, d_option2, d_option3, d_option4 = struct.unpack("B"*12, packet)
  199. mode = FUNCTION[d_function][0]
  200. m_range = FUNCTION[d_function][1][d_range]
  201. unit = FUNCTION[d_function][2]
  202. options = {}
  203. d_options = (d_status, d_option1, d_option2, d_option3, d_option4)
  204. OPTIONS = (STATUS, OPTION1, OPTION2, OPTION3, OPTION4)
  205. for d_option, OPTION in zip(d_options, OPTIONS):
  206. bits = get_bits(d_option, OPTION)
  207. options.update(bits)
  208. current = None
  209. if options["AC"] and options["DC"]:
  210. raise ValueError
  211. elif options["DC"]:
  212. current = "AC"
  213. elif options["AC"]:
  214. current = "DC"
  215. operation = "normal"
  216. # sometimes there a glitch where both UL and OL are enabled in normal operation
  217. # so no error is raised when it occurs
  218. if options["UL"]:
  219. operation = "underload"
  220. elif options["OL"]:
  221. operation = "overload"
  222. if options["AUTO"]:
  223. mrange = "auto"
  224. else:
  225. mrange = "manual"
  226. if options["BATT"]:
  227. battery_low = True
  228. else:
  229. battery_low = False
  230. # relative measurement mode, received value is actual!
  231. if options["REL"]:
  232. relative = True
  233. else:
  234. relative = False
  235. # data hold mode, received value is actual!
  236. if options["Hold"]:
  237. hold = True
  238. else:
  239. hold = False
  240. peak = None
  241. if options["MAX"]:
  242. peak = "max"
  243. elif options["MIN"]:
  244. peak = "min"
  245. if mode == "current" and options["VBAR"]:
  246. pass
  247. """Auto μA Current
  248. Auto mA Current"""
  249. elif mode == "current" and not options["VBAR"]:
  250. pass
  251. """Auto 220.00A/2200.0A
  252. Auto 22.000A/220.00A"""
  253. if options["VAHZ"] and not options["Judge"]:
  254. mode = "frequency"
  255. unit = "Hz"
  256. m_range = (1e0, 1, "Hz") #2200.0°C
  257. elif (options["VAHZ"] or mode == "frequency") and options["Judge"]:
  258. mode = "duty_cycle"
  259. unit = "%"
  260. m_range = (1e0, 1, "%") #2200.0°C
  261. if mode == "temperature" and options["VBAR"]:
  262. m_range = (1e0, 1, "deg") #2200.0°C
  263. elif mode == "temperature" and not options["VBAR"]:
  264. m_range = (1e0, 2, "deg") #220.00°C and °F
  265. digits = [d_digit4, d_digit3, d_digit2, d_digit1, d_digit0]
  266. digits = [DIGITS[digit] for digit in digits]
  267. display_value = 0
  268. for i, digit in zip(range(5), digits):
  269. display_value += digit*(10**(4-i))
  270. # negative value
  271. if options["Sign"]:
  272. display_value = display_value * -1
  273. display_value = Decimal(display_value) / 10**m_range[1]
  274. display_value = display_value.quantize(Decimal(1)/10**m_range[1])
  275. display_unit = m_range[2]
  276. value = float(display_value) * m_range[0]
  277. if operation != "normal":
  278. display_value = ""
  279. value = ""
  280. results = {
  281. "value": value,
  282. "unit": unit,
  283. "display_value": display_value,
  284. "display_unit": display_unit,
  285. "mode": mode,
  286. "current": current,
  287. "peak": peak,
  288. "relative": relative,
  289. "hold": hold,
  290. #"range": mrange,
  291. "operation": operation,
  292. "battery_low": battery_low
  293. }
  294. return results
  295. def output_readable(results):
  296. operation = results["operation"]
  297. battery_low = results["battery_low"]
  298. if operation == "normal":
  299. display_value = results["display_value"]
  300. display_unit = results["display_unit"]
  301. line = "{value} {unit}".format(value=display_value, unit=display_unit)
  302. else:
  303. line = "-, the measurement is {operation}ed!".format(operation=operation)
  304. if battery_low:
  305. line.append(" Battery low!")
  306. return line
  307. CSV_FIELDS = ["value", "unit", "mode", "current", "operation", "peak",
  308. "battery_low", "relative", "hold"]
  309. def format_field(results, field_name):
  310. value = results[field_name]
  311. if field_name == "value":
  312. if results["operation"]=="normal":
  313. return str(value)
  314. else:
  315. return ""
  316. if value==None:
  317. return ""
  318. elif value==True:
  319. return "1"
  320. elif value==False:
  321. return "0"
  322. else:
  323. return str(value)
  324. def output_csv(results):
  325. field_data = [format_field(results, field_name) for field_name in CSV_FIELDS]
  326. line = ";".join(field_data)
  327. return line
  328. def main():
  329. import argparse
  330. parser = argparse.ArgumentParser(description='Upload time data files.')
  331. default_port = '/dev/ttyUSB0'
  332. parser.add_argument('port',
  333. help='multimeter port (/dev/tty0, /dev/ttyUSB0, etc.)')
  334. parser.add_argument('-m', '--mode', choices=['csv', 'readable'],
  335. default="csv",
  336. help='output mode (default: csv)')
  337. parser.add_argument('-f', '--file',
  338. help='output file')
  339. parser.add_argument('--verbose', action='store_true',
  340. help='the program is verbose about its work')
  341. #parser.add_argument('--port', help='config file', default="time_data_upload.cfg")
  342. args = parser.parse_args()
  343. if args.verbose:
  344. log_level = logging.DEBUG
  345. else:
  346. log_level = logging.INFO
  347. logging.basicConfig(format='%(levelname)s:%(message)s', level=log_level)
  348. logging.info('Using port "{port}" in "{mode}" mode."'.format(port=args.port,
  349. mode=args.mode))
  350. try:
  351. ser = serial.Serial(port = args.port,
  352. baudrate = 19200,
  353. bytesize=serial.SEVENBITS,
  354. stopbits = serial.STOPBITS_ONE,
  355. parity = serial.PARITY_ODD,
  356. timeout=15) # default timeout for reading in seconds
  357. # exit if the port is not opened
  358. except serial.SerialException, e:
  359. sys.exit(e)
  360. ser.setDTR(level=True)
  361. ser.setRTS(level=False)
  362. output_file = None
  363. if args.mode == 'csv':
  364. timestamp = datetime.datetime.now()
  365. date_format = "%Y-%m-%d_%H:%S"
  366. timestamp = timestamp.strftime(date_format)
  367. if args.file:
  368. file_name = args.file
  369. else:
  370. file_name = "measurement_{}.csv".format(timestamp)
  371. output_file = open(file_name, "w")
  372. logging.info('Writing to file "{}"'.format(file_name))
  373. header = "timestamp;{}\n".format(";".join(CSV_FIELDS))
  374. output_file.write(header)
  375. while True:
  376. line = ser.readline()
  377. line = line.strip()
  378. timestamp = datetime.datetime.now()
  379. timestamp = timestamp.isoformat(sep=' ')
  380. if len(line)==12:
  381. try:
  382. results = parse(line)
  383. except Exception, e:
  384. logging.warning('Error "{}" packet from multimeter: "{}"'.format(e, line))
  385. if args.mode == 'csv':
  386. line = output_csv(results)
  387. output_file.write("{};{}\n".format(timestamp, line))
  388. elif args.mode == 'readable':
  389. pass
  390. else:
  391. raise NotImplementedError
  392. line = output_readable(results)
  393. print(timestamp.split(" ")[1], line)
  394. elif line:
  395. logging.warning('Unknown packet from multimeter: "{}"'.format(line))
  396. else:
  397. logging.warning("No response from multimeter")
  398. ser.close()
  399. if __name__ == "__main__":
  400. main()