Capture the display of a Rigol DS1000Z series oscilloscope by LAN using LXI SCPI commands
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

OscScreenGrabLAN.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python3
  2. from telnetlib_receive_all import Telnet
  3. from Rigol_functions import *
  4. import time
  5. import sys
  6. import os
  7. import platform
  8. import logging
  9. __version__ = 'v1.1.0'
  10. # Added TMC Blockheader decoding
  11. # Added possibility to manually allow run for scopes other then DS1000Z
  12. __author__ = 'RoGeorge'
  13. #
  14. # TODO: Write all SCPI commands in their short name, with capitals
  15. # TODO: Add ignore instrument model switch instead of asking
  16. #
  17. # TODO: Detect if the scope is in RUN or in STOP mode (looking at the length of data extracted)
  18. # TODO: Add logic for 1200/mdep points to avoid displaying the 'Invalid Input!' message
  19. # TODO: Add message for csv data points: mdep (all) or 1200 (screen), depending on RUN/STOP state, MATH and WAV:MODE
  20. # TODO: Add STOP scope switch
  21. #
  22. # TODO: Add debug switch
  23. # TODO: Clarify info, warning, error, debug and print messages
  24. #
  25. # TODO: Add automated version increase
  26. #
  27. # TODO: Extract all memory datapoints. For the moment, CSV is limited to the displayed 1200 datapoints.
  28. # TODO: Use arrays instead of strings and lists for csv mode.
  29. #
  30. # TODO: variables/functions name refactoring
  31. # TODO: Fine tune maximum chunk size request
  32. # TODO: Investigate scaling. Sometimes 3.0e-008 instead of expected 3.0e-000
  33. # TODO: Add timestamp and mark the trigger point as t0
  34. # TODO: Use channels label instead of chan1, chan2, chan3, chan4, math
  35. # TODO: Add command line parameters file path
  36. # TODO: Speed-up the transfer, try to replace Telnet with direct TCP
  37. # TODO: Add GUI
  38. # TODO: Add browse and custom filename selection
  39. # TODO: Create executable distributions
  40. #
  41. # Set the desired logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  42. logging.basicConfig(level=logging.INFO,
  43. format='%(asctime)s - %(levelname)s - %(message)s',
  44. filename=os.path.basename(sys.argv[0]) + '.log',
  45. filemode='w')
  46. logging.info("***** New run started...")
  47. logging.info("OS Platform: " + str(platform.uname()))
  48. log_running_python_versions()
  49. # Update the next lines for your own default settings:
  50. path_to_save = "captures/"
  51. save_format = "PNG"
  52. IP_DS1104Z = "192.168.1.3"
  53. # Rigol/LXI specific constants
  54. port = 5555
  55. big_wait = 10
  56. smallWait = 1
  57. company = 0
  58. model = 1
  59. serial = 2
  60. # Check command line parameters
  61. script_name = os.path.basename(sys.argv[0])
  62. def print_help():
  63. print ()
  64. print ("Usage:")
  65. print (" " + "python " + script_name + " png|bmp|csv [oscilloscope_IP [save_path]]")
  66. print ()
  67. print ("Usage examples:")
  68. print (" " + "python " + script_name + " png")
  69. print (" " + "python " + script_name + " csv 192.168.1.3")
  70. print ()
  71. print ("The following usage cases are not yet implemented:")
  72. print (" " + "python " + script_name + " bmp 192.168.1.3 my_place_for_captures")
  73. print ()
  74. print ("This program captures either the waveform or the whole screen")
  75. print (" of a Rigol DS1000Z series oscilloscope, then save it on the computer")
  76. print (" as a CSV, PNG or BMP file with a timestamp in the file name.")
  77. print ()
  78. print (" The program is using LXI protocol, so the computer")
  79. print (" must have LAN connection with the oscilloscope.")
  80. print (" USB and/or GPIB connections are not used by this software.")
  81. print ()
  82. print (" No VISA, IVI or Rigol drivers are needed.")
  83. print ()
  84. # Read/verify file type
  85. if len(sys.argv) <= 1:
  86. print_help()
  87. sys.exit("Warning - wrong command line parameters.")
  88. elif sys.argv[1].lower() not in ["png", "bmp", "csv"]:
  89. print_help()
  90. print ("This file type is not supported: ", sys.argv[1])
  91. sys.exit("ERROR")
  92. file_format = sys.argv[1].lower()
  93. # Read IP
  94. if len(sys.argv) > 1:
  95. IP_DS1104Z = sys.argv[2]
  96. # Check network response (ping)
  97. if platform.system() == "Windows":
  98. response = os.system("ping -n 1 " + IP_DS1104Z + " > nul")
  99. else:
  100. response = os.system("ping -c 1 " + IP_DS1104Z + " > /dev/null")
  101. if response != 0:
  102. print ()
  103. print ("WARNING! No response pinging " + IP_DS1104Z)
  104. print ("Check network cables and settings.")
  105. print ("You should be able to ping the oscilloscope.")
  106. # Open a modified telnet session
  107. # The default telnetlib drops 0x00 characters,
  108. # so a modified library 'telnetlib_receive_all' is used instead
  109. tn = Telnet(IP_DS1104Z, port)
  110. instrument_id = command(tn, "*IDN?").decode() # ask for instrument ID
  111. # Check if instrument is set to accept LAN commands
  112. if instrument_id == "command error":
  113. print ("Instrument reply:", instrument_id)
  114. print ("Check the oscilloscope settings.")
  115. print ("Utility -> IO Setting -> RemoteIO -> LAN must be ON")
  116. sys.exit("ERROR")
  117. # Check if instrument is indeed a Rigol DS1000Z series
  118. id_fields = instrument_id.split(",")
  119. if (id_fields[company] != "RIGOL TECHNOLOGIES") or \
  120. (id_fields[model][:3] != "DS1") or (id_fields[model][-1] != "Z"):
  121. print ("Found instrument model '{}' from '{}'".format(id_fields[model], id_fields[company]))
  122. print ("WARNING: No Rigol from series DS1000Z found at", IP_DS1104Z)
  123. print ()
  124. typed = raw_input("ARE YOU SURE YOU WANT TO CONTINUE? (No/Yes):")
  125. if typed != 'Yes':
  126. sys.exit('Nothing done. Bye!')
  127. print ("Instrument ID:", instrument_id)
  128. # Prepare filename as C:\MODEL_SERIAL_YYYY-MM-DD_HH.MM.SS
  129. timestamp = time.strftime("%Y-%m-%d_%H.%M.%S", time.localtime())
  130. if len(sys.argv) > 3:
  131. filename = sys.argv[3]
  132. else:
  133. filename = path_to_save + id_fields[model] + "_" + id_fields[serial] + "_" + timestamp + "." + file_format
  134. if file_format in ["png", "bmp"]:
  135. # Ask for an oscilloscope display print screen
  136. print ("Receiving screen capture...")
  137. if file_format == "png":
  138. buff = command(tn, ":DISP:DATA? ON,OFF,PNG")
  139. else:
  140. buff = command(tn, ":DISP:DATA? ON,OFF,BMP8")
  141. expectedBuffLen = expected_buff_bytes(buff)
  142. # Just in case the transfer did not complete in the expected time, read the remaining 'buff' chunks
  143. while len(buff) < expectedBuffLen:
  144. logging.warning("Received LESS data then expected! (" +
  145. str(len(buff)) + " out of " + str(expectedBuffLen) + " expected 'buff' bytes.)")
  146. tmp = tn.read_until(b"\n", smallWait)
  147. if len(tmp) == 0:
  148. break
  149. buff += tmp
  150. logging.warning(str(len(tmp)) + " leftover bytes added to 'buff'.")
  151. if len(buff) < expectedBuffLen:
  152. logging.error("After reading all data chunks, 'buff' is still shorter then expected! (" +
  153. str(len(buff)) + " out of " + str(expectedBuffLen) + " expected 'buff' bytes.)")
  154. sys.exit("ERROR")
  155. # Strip TMC Blockheader and keep only the data
  156. tmcHeaderLen = tmc_header_bytes(buff)
  157. expectedDataLen = expected_data_bytes(buff)
  158. buff = buff[tmcHeaderLen: tmcHeaderLen+expectedDataLen]
  159. # Write raw data to file
  160. with open(filename, 'wb') as f:
  161. f.write(buff)
  162. print('Saved raw data to {}'.format(filename))
  163. # TODO: Change WAV:FORM from ASC to BYTE
  164. elif file_format == "csv":
  165. # Put the scope in STOP mode - for the moment, deal with it by manually stopping the scope
  166. # TODO: Add command line switch and code logic for 1200 vs ALL memory data points
  167. # tn.write("stop")
  168. # response = tn.read_until("\n", 1)
  169. # Scan for displayed channels
  170. chanList = []
  171. for channel in ["CHAN1", "CHAN2", "CHAN3", "CHAN4", "MATH"]:
  172. response = command(tn, ":" + channel + ":DISP?")
  173. # If channel is active
  174. if response == '1\n':
  175. chanList += [channel]
  176. # the meaning of 'max' is - will read only the displayed data when the scope is in RUN mode,
  177. # or when the MATH channel is selected
  178. # - will read all the acquired data points when the scope is in STOP mode
  179. # TODO: Change mode to MAX
  180. # TODO: Add command line switch for MAX/NORM
  181. command(tn, ":WAV:MODE NORM")
  182. command(tn, ":WAV:STAR 0")
  183. command(tn, ":WAV:MODE NORM")
  184. csv_buff = ""
  185. # for each active channel
  186. for channel in chanList:
  187. print ()
  188. # Set WAVE parameters
  189. command(tn, ":WAV:SOUR " + channel)
  190. command(tn, ":WAV:FORM ASC")
  191. # MATH channel does not allow START and STOP to be set. They are always 0 and 1200
  192. if channel != "MATH":
  193. command(tn, ":WAV:STAR 1")
  194. command(tn, ":WAV:STOP 1200")
  195. buff = ""
  196. print ("Data from channel '" + str(channel) + "', points " + str(1) + "-" + str(1200) + ": Receiving...")
  197. buffChunk = command(tn, ":WAV:DATA?")
  198. # Just in case the transfer did not complete in the expected time
  199. while buffChunk[-1] != "\n":
  200. logging.warning("The data transfer did not complete in the expected time of " +
  201. str(smallWait) + " second(s).")
  202. tmp = tn.read_until(b"\n", smallWait)
  203. if len(tmp) == 0:
  204. break
  205. buffChunk += tmp
  206. logging.warning(str(len(tmp)) + " leftover bytes added to 'buff_chunks'.")
  207. # Append data chunks
  208. # Strip TMC Blockheader and terminator bytes
  209. buff += buffChunk[tmc_header_bytes(buffChunk):-1] + ","
  210. # Strip the last \n char
  211. buff = buff[:-1]
  212. # Process data
  213. buff_list = buff.split(",")
  214. buff_rows = len(buff_list)
  215. # Put read data into csv_buff
  216. csv_buff_list = csv_buff.split(os.linesep)
  217. csv_rows = len(csv_buff_list)
  218. current_row = 0
  219. if csv_buff == "":
  220. csv_first_column = True
  221. csv_buff = str(channel) + os.linesep
  222. else:
  223. csv_first_column = False
  224. csv_buff = str(csv_buff_list[current_row]) + "," + str(channel) + os.linesep
  225. for point in buff_list:
  226. current_row += 1
  227. if csv_first_column:
  228. csv_buff += str(point) + os.linesep
  229. else:
  230. if current_row < csv_rows:
  231. csv_buff += str(csv_buff_list[current_row]) + "," + str(point) + os.linesep
  232. else:
  233. csv_buff += "," + str(point) + os.linesep
  234. # Save data as CSV
  235. scr_file = open(filename, "wb")
  236. scr_file.write(csv_buff)
  237. scr_file.close()
  238. print ("Saved file:", "'" + filename + "'")
  239. tn.close()