mrSCPI
Instrument Control Software
Author: | Mitch Richling |
Updated: | 2023-04-07 21:13:10 |
Generated: | 2023-04-07 21:14:59 |
Copyright 2023 Mitch Richling. All rights reserved.
Table of Contents
1. Introduction
From the README:
mrSCPI
is a software tool for controlling programmable test equipment; however, I think ofmrSCPI
as more of a test bench productivity tool.From a software/hardware architectural perspective, programmable test equipment is designed in such a way so as to make very complex test automation possible. Systems designed from the ground up to support very complex use cases frequently overly complicate things for people with simple use cases. Larry Wall captured the phenomenon quite clearly when he said:
Perl makes easy things easy and hard things possible. Professional programming languages tend to make all things equally difficult.
mrSCPI
aims to make it possible to easily integrate test equipment automation into my day to day bench workflow. The goal is to make it so easy that I use it even for the hundreds of tiny, repetitive tasks I preform at the bench.If you are wondering if
mrSCPI
is for you, that emphasized "my" is a warning!mrSCPI
is very much designed around my personal workflow and tool preferences. I very much like command line tools with UNIX'ish interfaces. If this sounds like you, thenmrSCPI
might be for you. OTOH, if you were looking for a nice GUI to control your equipment, then you will most likely be quite disappointed withmrSCPI
.Lastly,
mrSCPI
is simple to set-up so I can run it anyplace. It has no dependencies beyond a standard Ruby install. No drivers. No modules. No packages. No PIPs. No GEMs. Nothing to compile. Just a single script – yes, the library and the executable are the same file!Links:
- Documentation: https://richmit.github.io/mrSCPI/
- Additional examples: https://github.com/richmit/TestEquipmentScripts
- Github repo:: https://github.com/richmit/mrSCPI
2. Interfaces
The mrSCPI
ecosystem provides three distinct ways to control SCPI
instruments:
- Ruby API
- For complex test automation requiring a real programming language.
This stateful API is unlike most test automation APIs and naturally compliments the way many
SCPI
tasks are preformed … Or at least the way I tend to useSCPI
.;)
require_relative 'mrSCPIlib' SCPIsession.new(:url => '@tek2k', :cmd => '*IDN?')
mrSCPI.rb
command- A CLI command providing sophisticated
SCPI
functionality from the command line – much likelxi-tool
.
mrSCPI.rb --url @tek2k --cmd '*IDN?'
mrSCPI
Scripts- An efficient scripting language for simple
SCPI
tasks. Full Emacsorg-mode
Babel support is included enabling instrument control right from your laboratory notebook!
:url @tek2k :cmd *IDN?
All of these methods use the same underlying infrastructure, and mirror each other in the way they work. So if you learn how to use one of them, you have learned how to use them all.
3. Emacs as a mrSCPI
Interface
mrSCPI
really has nothing to do with Emacs; however, as an enthusiastic Emacs user, I wanted to be able to use mrSCPI
from inside
Emacs! To that end I have developed a bit of Emacs software to seamlessly integrate mrSCPI
into Emacs. For me, because Emacs
plays a central role in my workflow, this is one of the most important aspects of mrSCPI
– 90% of my mrSCPI
use is from within Emacs. This
section provides some insight into my workflow. If you are not interested in Emacs, then you can safely skip this section.
3.1. The Laboratory Notebook
I always have my laboratory notebook open at the bench – just an org-mode
document loaded up in Emacs running on my bench computer. I have
implemented a major mode for mrSCPI
scripts as well as org-mode
Babel support. This allows me to embed SCPI
commands right inside my notes
making those notes EXECUTABLE. Here is an example excerpt from one of my notebooks:
To make this sort of thing effortless, I have a few scripts that capture instrument configuration details and insert them into my org-mode
notebook.
Then, when I come back to my experiment some time later, I can have org-mode
reconfigure all the equipment on my bench to where I left off.
3.2. Test Equipment Notes & Macro Buttons
I keep general test equipment notes in a couple org-mode
files. Of cource I include in my notes littel bits of mrSCPI
script code for common
things I do with my test equipment. I could simply put my cursor on those code bits in org-mode
and execute them, but I wanted a quicker way to access
these code snippits. So I added an Emacs function that finds all the mrSCPI
code bits in an org-mode
document, and produces a GUI window full
of buttons to run them. This has proven to be super handy, and saves me a ton of time at the bench. Here is an example of one of my button buffers:
3.3. Integrated Data Capture and Analysis
3.4. Setting Up Emacs
I put the mrSCPI
stuff in my Emacs dot file directory. Of course it doesn't need to be in the dot file directory. That's just where I put it.
If you have some other organizational structure for your auxiliary Emacs files, then feel free to use it!
3.4.1. Editing mrSCPI
Code: mrscpi-mode
A simple mode for mrSCPI
scripts is provided in emacs/mrscpi-mode.el. This should be before your org-mode
config.
(if (file-exists-p "~/.emacs.d/mrscpi-mode.el") (progn (autoload 'mrscpi-mode "~/.emacs.d/mrscpi-mode.el" "Mode for mrSCPI files") (add-to-list 'auto-mode-alist '("\\.mrscpi$" . mrscpi-mode))))
3.4.2. org-mode
Babel Support: ob-mrscpi.el
& mrscpi-buttons.el
A language module for org-mode
Babel may be found in emacs/ob-mrscpi.el. The magical
button maker may be found in emacs/mrscpi-buttons.el – you don't need this one for
just basic org-mode
support. I load these things up at the very end of the org-mode
setup block in my init.el
file:
(if (and (file-exists-p "~/.emacs.d/ob-mrscpi.el") (file-exists-p "~/bin/mrSCPI.rb")) (load "~/.emacs.d/ob-mrscpi.el")) (if (and (file-exists-p "~/.emacs.d/mrscpi-buttons.el") (file-exists-p "~/bin/mrSCPI.rb")) (load "~/.emacs.d/mrscpi-buttons.el"))
4. Ruby API
4.1. API Structure & Philosophy
In essence any SCPI
program is about managing a communication session state between computer and instrument. The heart of mrSCPI
is the
SCPIsession
class encapsulates and manages session state. Session state is represented by named parameters (key-value
pairs). For example the parameter :ip_address
stores the IP address for the connection. As another example, the :print_results
parameter stores a
boolean value that determines if the results from SCPI
commands will be printed. A few parameters, like :ip_address
, may only be set when the
SCPIsession
is created. Other parameters, like :print_results
may be freely changed at any time. Parameters are sticky
in that they stay in effect until changed. Using the API always follows the same basic outline:
- Create an
SCPIsession
object (The:url
,:ip_address
,:net_protocol
, and:net_port
parameters are used by new) - Set parameters regarding how you want to run
SCPI
commands - Set the
:cmd
parameter to execute anSCPI
command - Go back to 2) till everything is done
See the documentation for the SCPIsession
class for information about all the parameters.
4.2. Example
In the following example, we pull a PNG screenshot from a Rigol DHO2000/4000 series oscilloscope.
if ARGV[0] then puts("This script will pull a PNG screenshot from a Rigol DHO2000/4000 series oscilloscope.") puts("The screenshot file looks like DATESTAMP_DHO.png") exit end require ENV['PATH'].split(File::PATH_SEPARATOR).map {|x| File.join(x, 'mrSCPI.rb')}.find {|x| FileTest.exist?(x)} imgFileName = "#{Time.now.localtime.strftime('%Y%m%d%H%M%S')}_DHO.png" theSPCIsession = SCPIsession.new(:url => '@dho4k', :result_macro_block => true, :print_result => true, :out_file => imgFileName, :cmd => ':DISPlay:DATA? PNG' ) puts("screenshot_dho4k.rb: Screen image captured to: #{imgFileName}")
All the magic is in the SCPIsession.new
call:
5. mrSCPI
Scripts
Even if you only want to know about scripts, I encourage you to read the section on Ruby API Structure & Philosophy. To begin,
here is a mrSCPI
script version of the Ruby script given in that section:
#!/usr/bin/env mrSCPI.rb :url @dho4k # Connect to my DHO4k :eval imgFileName=Time.now.localtime.strftime('%Y%m%d%H%M%S') + "_DHO.png" # Filename for PNG :result_macro_block true # Expect/Extract a block :print_result true # Print the block :out_file ${imgFileName} # into a filename :cmd :DISPlay:DATA? PNG # Send the command
Notice how the code parallels the Ruby script. Essentially the mrSCPI
script is what was inside the SCPIsession.new
call in the Ruby script! Also
note the shebang line (#!/usr/bin/env mrSCPI.rb
). This is how we make a mrSCPI
script executable on the command line – just like a shell script.
Note the above script may be found in the example
directory named "dho4ksc.mrscpi
". Let's take a look at another simple script.
# This little script pulls the identifier string from an instrument, matches # matches it against a regular expression, and prints the results. :url @34401a # @34401a is an instrument connection "nickname" :name ids # We name the next :cmd that will run :cmd *IDN? # Creates ids variable and stores command result in it :skip_if ${ids}~34401[aA] # Checks the result against a regular expression, and skips the next command if it matches :stop Unknown Device: ${ids} # This line is skipped if the previous one was true. Note :stop ends the script. :print Known Device: 34401A # The normal script end -- when the regex above matches indicating a recognized device
If the above script may be found in the example
directory named "example.mrscpi
". From that directory, we can run it like so:
mrSCPI.rb -f example.mrscpi
Note that scripts can also be provided on STDIN
. If we continue with the previous example, then here is another way to run the script:
mrSCPI.rb -f STDIN <example.mrscpi
Scripts can be described on the command line with arguments instead of being put in a script file. You simply replace the mrSCPI
script commands with
arguments of the same name. For small scripts with just a couple commands, this can be very handy. What may be more useful is using command line script
arguments with a script file. When we do this it is as if we prepend the script file with the commands given as arguments. One thing to note is that the
first :url
option will override any subsequent instances – this is also true for :ip_address
, :net_port
, & :net_protocol
as well. For example we
could do something like this to connect to an alternate DMM:
mrSCPI.rb --url @dmm2 example.mrscpi
If we set the verbosity to level 2 (with -V 2), we will see a warning printed about the second :url
command being ignored.
A common technique is to parameterize things with variables, and then set those variables on the command line. For example, suppose we wish to set our power supply up with the first two channels providing analog voltage rails and the third channel providing a voltage for a microcontroller. We might use a script like the following:
:url @hmc8043 :result_type nil :delay_after_complete 100 :cmd :OUTPut:MASTer:STATe OFF' :cmd :INSTrument:NSELect 1; :SOURce:VOLTage:LEVel:IMMediate:AMPLitude ${analog_voltage:10}; :OUTPut:CHANnel:STATe ON :cmd :INSTrument:NSELect 2; :SOURce:VOLTage:LEVel:IMMediate:AMPLitude ${analog_voltage:10}; :OUTPut:CHANnel:STATe ON :cmd :INSTrument:NSELect 3; :SOURce:VOLTage:LEVel:IMMediate:AMPLitude ${digital_voltage:5}; :OUTPut:CHANnel:STATe ON
If the above script may be found in the example
directory named "power_setup_example.mrscpi
". From that directory, we could set the variables and call
the script like this:
mrSCPI.rb -D analog_voltage=15 -D digitial_voltage=3.3 power_setup_example.mrscpi
If we wanted the default values, then we could have just used this:
mrSCPI.rb -f power_setup_example.mrscpi
Inside of Emacs in an org-mode
code block, we could set these variables via the src
header arguments like so:
#+begin_src mrscpi :output verbatum :var analog_voltage="15" digitial_voltage="3.3"
Note that the "new" commands (:url
, :ip_address
, :net_port
, & :net_protocol
) can not be parameterized with variables, but they can be overridden.
6. SCPIrcFile
Class
mrSCPI
can use a configuration file to keep track of things like IP addresses, port numbers, protocols, and firewall details. The
SCPIrcFile
class is the primary interface to the information in the RC file. For the details, see the class
documentation. The basics can be illustrated with a couple examples:
6.1. mrNetwork.rb
require 'mrSCPI.rb' puts(SCPIrcFile.instance.lookupNetwork.inspect)
6.2. teAlias.rb
require 'mrSCPI.rb' if (ARGV.length < 1) || (ARGV.first.match(/^-+[hH]/)) then puts("USE: teAlias.rb [format] thingy") puts(" thingy is one of netqork@nickname, @nickname, or nickname") puts(" network is a network name in the mrSCPIrc file") puts(" nickname is a nickname name in the mrSCPIrc file") puts(" format is a format string for how to print the result. Default: '%u'") puts(" %s -- replaced by the scheme (http, https, soip, etc...)") puts(" %h -- replaced by the IP address or DNS name") puts(" %p -- replaced by the port number") puts(" %u -- replaced by the full URL") puts(" Example: telnet `teAlias '%h %p' @foobar`") puts(" Example: ssh `teAlias '-p %p %h' @foobar`") puts(" Example: chromium-browser `teAlias.rb @foobar`") puts(" Works because '%u' is the default format.") puts("") puts("See SCPIrcFile.lookupURLnickname for full details.") exit 0 elsif (ARGV.length == 1) then print(SCPIrcFile.instance.lookupURLnickname(ARGV[0])) elsif (ARGV.length == 2) then urlBits = SCPIrcFile.instance.urlParser(ARGV[1]) print(ARGV[0].gsub('%h', urlBits[:ip_address]).gsub('%s', urlBits[:net_protocol].to_s).gsub('%u', urlBits[:url]).gsub('%p', urlBits[:net_port].to_s)) end
6.3. Example RC file
Here is an example of what my RC file looks like. I have two network subnets on my bench. One for test equipment (192.168.42.X), and one for computers
(192.168.43.X). The only access from outside these networks is to an SSH bastion server on the 192.168.43.X network. So when I'm on my bench I can access
the test equipment directly, but otherwise I must access the equipment via SSH tunnels to the bastion server. This configuration file makes it so that I can
specify a connection URL like "@awg
", and connect to my 33210a AWG regardless of where I'm running the script – i.e. the library figures out what network
I'm on, and expands "@awg
" into the right thing.
########################################################################################################### # SCPI Nicknames ########################################################################################## #........CNAME......nicknames.................Bench Network....................ssh to bench-gate........... nickname 33210ae 33210a aawge aawg => bench@raw://172.16.42.40:5025 ssh@raw://127.0.0.1:9040 nickname 33210aw aawgw => bench@https://172.16.42.40:443 ssh@https://127.0.0.1:9041 nickname 34401as 34401a dmms dmm => bench@soip://172.16.42.32:10001 ssh@soip://127.0.0.1:9033 nickname 34401ap dmmp => bench@plgx://172.16.42.88:1234 ssh@plgx://127.0.0.1:9088 nickname dg2052e dg2052 dg2ke dg2k => bench@raw://172.16.42.96:5555 ssh@raw://127.0.0.1:9096 nickname dg2052w dg2kw => bench@http://172.16.42.96:80 ssh@http://127.0.0.1:9097 nickname dho4204e dho4204 dho4ke dho4k => bench@raw://172.16.42.104:5555 ssh@raw://127.0.0.1:9104 nickname dho4204w dho4kw => bench@http://172.16.42.104:80 ssh@http://127.0.0.1:9105 nickname dmm6500e dmm6500 6500e 6500 => bench@raw://172.16.42.64:5025 ssh@raw://127.0.0.1:9064 nickname dmm6500w 6500w => bench@http://172.16.42.64:80 ssh@http://127.0.0.1:9065 nickname hmc8043e hmc8043 pse ps => bench@raw://172.16.42.72:5025 ssh@raw://127.0.0.1:9072 nickname hmc8043w psw => bench@http://172.16.42.72:80 ssh@http://127.0.0.1:9073 nickname sds2504xpe sds2504xp sig2ke sig2k => bench@raw://172.16.42.56:5025 ssh@raw://127.0.0.1:9056 nickname sds2504xpw sig2kw => bench@http://172.16.42.56:80 ssh@http://127.0.0.1:9057 nickname tds2024s tek2ks tek2k => bench@soip://172.16.42.32:10002 ssh@soip://127.0.0.1:9034 nickname tds3052bh tds3052b tek3kh tek3k => bench@t3k://172.16.42.80:80 ssh@t3k://127.0.0.1:9081 nickname tds3052bs tek3ks => bench@soip://172.16.42.32:10003 ssh@soip://127.0.0.1:9035 nickname tds3052bw tek3kw => bench@http://172.16.42.80:80 ssh@http://127.0.0.1:9081 nickname tds3052bp tek3kp => bench@plgx://172.16.42.88:1234 ssh@plgx://127.0.0.1:9088 nickname 53131ap 53131a countp count => bench@plgx://172.16.42.88:1234 ssh@plgx://127.0.0.1:9088 nickname router42 router => bench@https://172.16.42.1:443 ssh@https://127.0.0.1:9001 #ickname router43 => bench@https://172.16.43.1:443 nickname serial serial => bench@http://172.16.42.32:80 ssh@http://127.0.0.1:9032 ########################################################################################################### # Networks ################################################################################################ #.......Name..Subnet.Mask......Comment..................................................................... network bench 172.16.43.0/23 # Bench network for the bench workstation/bastian network te 172.16.42.0/23 # Bench network for instruments (test equipment) network ssh 172.16.1.0/24 # Home office network ########################################################################################################### # DNS ##################################################################################################### #.......Device.......IP.............CNAME...........................Note................................... #host nomad 172.16.43.24 bench-nomad.bench.mitchr.me # My laptop on the bench #host pi 172.16.43.32 bench-pi.bench.mitchr.me # Bench Raspberry Pi #host awg-agilent 172.16.42.40 awg-agilent.bench.mitchr.me # Agilent 33210A AWG #host gpib-rover 172.16.42.88 gpib-rover.bench.mitchr.me # HP 53131A Counter #host awg-rigol 172.16.42.96 awg-rigol.bench.mitchr.me # Rigol DG2052 AWG #host oscope-rigol 172.16.42.104 oscope-rigol.bench.mitchr.me # Rigol DHO4204 HiRes Scope #host dmm-keithley 172.16.42.64 dmm-keithley.bench.mitchr.me # Keithley/Tektronix DMM6500 DMM #host ps-rns 172.16.42.72 ps-rns.bench.mitchr.me # R&S HMC8043 Power Supply #host oscope-sig 172.16.42.56 oscope-sig.bench.mitchr.me # Siglent SDS2504X+ MSO Scope #host oscope-tek 172.16.42.80 oscope-tek.bench.mitchr.me # Tektronix TDS3052B DPO Scope #host bench-router4 172.16.42.1 bench-router4.bench.mitchr.me # Ubiquiti EdgeRouter Lite #host bench-router4 172.16.43.1 bench-router4.bench.mitchr.me # Ubiquiti EdgeRouter Lite #host serial 172.16.42.32 serial.bench.mitchr.me # Lantronix EDS4100 Serial Console ########################################################################################################### # Serial Console Ports #################################################################################### #.....Device..Line..Interface..Protocol..Port..Baud..Parity..DBits..SBits..Flow..Xon..Xof...Device......... #scon serial 1 RS232 Tunnel 10001 9600 Even 7 2 SOFT ^Q ^S 34401A #scon serial 2 RS232 Tunnel 10002 19200 Even 2 2 HARD ^Q ^S TDS2024 #scon serial 3 RS232 Tunnel 10003 38400 None 8 1 HARD ^Q ^S TDS3052B #scon serial 4 DISABLED -- -- -- -- -- -- -- -- -- -- ########################################################################################################### # GPIB/ETH ################################################################################################ #......Device.......Instrument....Note..................................................................... #geth gpib-rover 53131a # HP 53131A Counter ########################################################################################################### # Layer 3 IP Router Ports ################################################################################# #..............Device.............Interface......Subnet.............Note................................... #L3router bench-router4 eth0 172.16.1.0/24 #L3router bench-router4 eth1 172.16.42.0/23 #L3router bench-router4 eth2 172.16.43.0/23 ########################################################################################################### # Layer 2 Ethernet Switch Ports ########################################################################### #..............Device.............Interface......Device.............Note................................... #L2switch bench-switch42 1 bench-router4 #L2switch bench-switch42 2 nomad # Long, sheilded, yellow cable #L2switch bench-switch42 3 NC #L2switch bench-switch42 4 NC #L2switch bench-switch42 5 NC #L2switch bench-switch42 6 NC #L2switch bench-switch42 7 NC #L2switch bench-switch42 8 NC #L2switch bench-switch42 9 NC #L2switch bench-switch42 10 oscope-rigol # Red Cable #L2switch bench-switch42 11 gpib-rover # Yellow Cable #L2switch bench-switch42 12 dmm-keithley # Bright blue #L2switch bench-switch42 13 oscope-sig # Grey cable #L2switch bench-switch42 14 ps-rns # Dark Blue #L2switch bench-switch42 15 NC # Grey #L2switch bench-switch42 16 serial
7. Features (current & future)
- Broad platform support
- Minimal runtime requirements (base ruby install)
- Broad OS compatibility (Windows, OSX, Linux, Raspberry Pi OS, etc…)
- Modern Ruby API for interacting with instruments via SCPI
- Stateful programming model that compliments the structure of
SCPI
itself - Good control over output (commands, debug, results, etc…)
- Stateful programming model that compliments the structure of
- Simple
mrSCPI
scripting language- All the goodness of the Ruby API, but for simple applications
- Usage that parallels how the Ruby API is used in practice
- Shebang support for "executable" scripts on UNIX/Linux/Windows MSYS2
- Support simple programming constructs
- Conditionals
- Goto/labels
- Variables (with variable interpolation)
- Evaluate ruby one liners and assign results to a variable
- Stop execution with optional message
- Print things
- Emacs
org-mode
Babel support (execution and header variables) - Emacs language mode (highlight
mrSCPI
script code & identify common syntax errors)
- Protocol/Communication Features
SCPI
over raw TCP/IP sockets (whichlxi-tool
can do very well)- Support for instruments that don't close the TCP connection after the client closes for write
- Serial (RS-232/RS-485)
SCPI
over Ethernet via a network attached serial console server- Correctly control DTE via TCP/IP socket close
- Console server port mapping is directly supported via instrument nicknames
- GPIB
SCPI
over Ethernet with a Prologix lan controller- Correctly configures Prologix controllers to provide :raw mode access
- Preserves Prologix EPROM values – and extends life of device memory
- Uses
:read_timeout_first_byte
to set Prologix++read_tmo_ms
- Uses
:eol
to set Prologix++eos
- Transparently avoids "Query Unterminated" and "-420" errors by using
++auto 0
and calling++read
- Support Prologix specific commands for when to do GPIB reads
SCPI
over HTTP for TDS3000B series oscilloscopes- Works around the lack of raw sockets support by scraping the instrument's web server
- Flexible timing control
- Delay after sending a command but before doing a read on the line
- Timeout to wait for a response to start
- Timeout to for more data after a successful read
- Delay before retrying an I/O operation
- Delay after a command is run and all I/O processed, but before returning a value (or running the next command)
- Features for Emacs
org-mode
support- Option to always return an exit code of 0 –
org
throws away output from commands with a non-zero exit code - Option to not send things to
STDERR
–org
only capturesSTDOUT
- Option for running
SCPI
scripts onSTDIN
- Support for prepending script options/commands via command line
- Option to always return an exit code of 0 –
- A set of standard post-processing steps for results
- Standard string processing operations:
- chop (end of line characters)
- trim white space
- Extract last word
- Extract a 488.2 binary block data payload
- split into an array
- Binary data can be unpack'ed into native types
- Binary return blocks can be split into header and payload
- Strings can be split on
- commas
- semicolons
- whitespace
- vertical whitespace
- regular expressions
- fixed strings or character sets
- scan (TODO: future feature)
- compress sequences of one or more white-space characters into single spaces
- regex to extract (TODO: future feature)
- Convert results into numeric Ruby types
- float or array of floats
- integer or array of integers
- boolean or array of booleans (actually tri-state: true, false, or nil)
- Standard string processing operations:
- Global Configuration File
- Keeping track of instrument IP addresses or DNS names
- Keeping track of port mappings so I can get through the firewall isolating my bench
- Avoiding the need to type full DNS names or IP addresses
- A library of regular expression strings for matching 488.2 results and program elements
8. FAQ
8.1. Why don't you use a simple require
to load mrSCPI
in Ruby scripts?
In my scripts, I normally load mrSCPI
by searching my PATH
environment variable like this:
require ENV['PATH'].split(File::PATH_SEPARATOR).map {|x| File.join(x, 'mrSCPI.rb')}.find {|x| FileTest.exist?(x)}
Why? I normally put mrSCPI
on my path, but I don't want to contaminate my Ruby LOAD_PATH
by including a generic bin
directory. This is just a
personal preference in the way I set up my operating environment. I expect most people will just do a require_relative
or require
to load mrSCPI
.
8.2. Where can I find more examples?
I have another repository for scripts related to test equipment that contains several mrSCPI
scripts.