Capture the display of a Rigol DS1000Z series oscilloscope by LAN using LXI SCPI commands
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OscScreenGrabLAN.py 11KB

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