ghettoIB: getting color screenshots and more out of an old logic analyzer

joukos edited this page Apr 2, 2016 · 39 revisions

tl;dr: self-contained Python module for controlling IEEE 488.2 compliant instruments over RS-232.

Download: git clone



A while back I happened to find an old (from 1992 or so) HP16500B logic analyzer mainframe for cheap. I had been trying to find information about these things prior to considering getting one since they seemed fairly common items on ebay and while noisy and bulky they still have decent specs. The unit I got included three modules, the 16532A 1GSa/s scope, 16550A 100MHz state/500MHz timing logic analyzer and the 16520A pattern generator. I was very lucky to also get all the logic analyzer pods. However, the only means of communication on the unit are the HP-IB and RS-232C ports since I don't have the necessary module (16500L, IIRC) to get a 10base-T port. A networked unit can open a client window to a running X server and is probably very convenient, but I haven't seen that functionality in operation.

The HP16500B rig

I have the instrument on a small table thingy that has wheels, it fits there perfectly and can be easily moved. I've hooked up a Raspberry Pi and various small things like a USB hub and tools with some velcro and velcro-like "KLETT-POWER" stuff so they tag along and can be easily rearranged.

Raspberry Pi behind the analyzer

But that wasn't the reason I'm writing this. While trying to look up the specs and information about these instruments I found it strange that there are no decent screenshots online. They would help a lot in giving insight to what the user interface is like and what to expect from its features. The best images I found were high-resolution camera shots from someone's lab, showing the instrument's CRT from a couple of meters away or black and white images in some obscure PDFs.

Getting shot, part I

So I tried to look for a way to get screenshots myself and put them online, now that I got the instrument. I figured that while at it, I could probably do some other neat things with it if I get the serial communication working. Well, that was somewhat of a hurdle also.

To connect the instrument to the Pi, I happened to have an RS-232 serial-to-USB dongle, Belkin F5U103V. Serial-to-USB seems to be a hit-and-miss affair and while serial communication seems very simple, it's everything but. The myriad adapters, varying pinouts, different handshaking protocols and terminal settings, simple or extended interfaces, let alone the quirks of the hardware that you just need to deal with can sometimes make even the basic spitting of bytes between two devices difficult. I was lucky to have an adapter that seems to work ok, or at least I haven't noticed a difference to a plain serial port, though at some point I suspected that too. More on that later.

Now that I had a /dev/ttyUSB0 on my Pi, I looked at the serial port of the instrument. Doh! Of course it's a female connector. That means I can't just plug my four-headed null-modem cable to it and instead need to find a combination of adapters or a proper cable with the right pinout and, well, good luck with that. I had a DB25(m)-DB9(f) adapter that I figured might help but it didn't because of its internal wiring and me lacking the necessary cables (if any even exist) to get it to work. And it would have ended up as a heavy cable-adapter mess and would have been cumbersome with the rig. I tried for some time to find a combination that would work but it was in vain. One of the cables I had was a DB25(m)-DB25(m) but out of 25 pins seemed that only two were connected or the cable was broken. I used some wires to connect the port pins so that it could do a loopback test and it succeeded, so I figured it should work if I sort out the cable issue.

So I just broke down the useless adapter to yield a male DB25 port and a female DB9 port. After some mental struggle - with serial ports there is the funny reversal of pin numbering happening all the time which confuses the hell out of me - I soldered wires to the pins needed for a DB25-DB9 null-modem cable with extended interface (ie. not 3-wire which has only GND, TX and RX). I noticed in the instrument's programming guide that pin 20 is kept high during operation, so I checked that too with a multimeter to get confidence in the pinout. After I had the wires soldered on the connectors I figured I'll test the connection on a breadboard so if I need to change something it's easy to do. While doing this I found some good resources for serial cable pinouts and assorted info, I used the DB25-DB9 null-modem diagram in the first link for reference:

Here's a picture of the breadboard "adapter" I have if you want to see a real but ugly example (obviously I need to make a more permanent solution):

Extremely configurable serial adapter

And a couple of more pics if you really need to see more detail:

After I had the breadboard set up I configured the instrument's settings in the following fashion:

  • in System / Communications select RS-232C as the interface
  • select 19200, 8 bits, no parity, no XON/XOFF (unless you use 3-wire)
  • note that the Printer Setup should now show HPIB selected (ie. not RS-232C)
  • save settings and set them to autoload

Ok. Now what? The serial connection is physically there, but how to test it?

How do you...? Eh..? ...

I had downloaded all of the PDF manuals I could find for the instrument and the modules I had. This amounts to about 2000 pages and I haven't even found the Programmer's Guide for the 16532A, just the user reference and service manual. The documentation for these instruments is surely thorough, but not very accessible. The manuals are also from 20 years ago or perhaps revised in 1997 if you're lucky.

The programming examples are in archaic HP 9000 Series 300 BASIC 6.2, which makes them more intimidating than necessary. Obviously I won't start learning an obsolete BASIC variant to interface with the device and while the guide does state it's just one possible host language, just skimming through it makes your head hurt because the actual command language of the instrument is not BASIC, instead it conforms to IEEE 488.2 standard. It was interesting to learn IEEE 488 has been around since the late 1960s, but IEEE 488.2 is the fairly modern (late 80s, revised in early 90s) specification for "Standard Codes, Formats, Protocols, and Common Commands" used for the IEEE 488 Instrumentation Bus. My case would be specific to communicating over RS-232 instead of the GPIB (HP-IB) bus. Oh, and naturally IEEE 488 - called IEEE 488.1 since 1987 - refers to the hardware implementation and IEEE 488.2 is just the syntax part of things.

And the programming guide mentions: "RS-232C program messages and response messages for the HP 16500B Logic Analysis System are structured very similar to those described by IEEE 488.2. In most cases, the same structure shown in this chapter for IEEE 488.2 will also work for RS-232C. Because of this, no additional information has been included for RS-232C."

Well, isn't that even more intimidating. I still tried to look for a simple way, just some code to actually talk to the device to verify everything is ok or a program to remotely control it. Getting a byte across is one thing but anything useful seemed impossible to find. Unfortunately most of the resources that turn up online seem to concern the GPIB bus and its specifics, not highlighting enough that RS-232 is the poor man's bus for these purposes. I did find very nice resources about GPIB though:

Then I found this:

It was a humble script posted by Venkat Iyer on a Tcl programming website. The script would rely on having the RS-232C port on the instrument configured as a printer, and would listen for PCL data from the serial port when you "print" something from the instrument. This sounded a bit cumbersome because I was worried that if I get the screenshots like this, the controlling interface would have to be HPIB because the printing and communication interfaces can't both be configured to be RS-232C. Nevertheless it was the best I could find, an actual program to interface with the device, even with source! Thank you Venkat.

So I tried the script out after tweaking it a bit and yes! It worked! Well, sort of. The script creates a black-and-white GIF image out of the data it receives - very slowly - but maybe some parameters would need to be adjusted because the resulting picture seemed to be missing some things. Most importantly the color, but also I think some text was missing and the background was transparent. I'm not sure exactly what one should expect if a picture destined to a serial printer ends up on your hard drive and I won't bother trying to get my head around PCL to replicate what the Tcl script does (I looked it up and it seems rendering PCL is another one of those cryptic ventures with no practical solutions you can just apt-get).

The script did however give me some hope in addition to verifying that data can be received from the instrument and I could pursue my goal, but I would need to do it using the RS-232C port for communications instead of printing.

After digging for quite some time more online, it turns out the vast majority of the nearly nonexistent documentation and examples available are convoluted and academic at best. The so-called solutions for instrument control are massive, vendor-specific I/O library software suites with awkward and impractical distribution ("register to download" -> "here's your 590MB Windows binary!") and licensing, or stuff like LabVIEW gimmicks that still rely on some external, humongous I/O library. It seemed to me from reading about VISA and such that to actually talk serial to some instruments you're expected to purchase a piece of software costing hundreds of dollars that does the bit-banging for the other software that supposedly let's you do something meaningful. Or maybe just send the instrument text strings, I don't know.

Slightly disappointed that after so much searching I had not found any ready solution, I figured I should just try to talk with the instrument myself. Since the code would run on a Raspberry Pi, it should be light and simple so I chose Python as the language to implement the program, because while I hardly know Python, its popularity on the platform and elsewhere is a big bonus. I got some beer and started reading the document called 'HP16500B/16501A Logic Analysis System Programmer's Guide', available here:

*PRE Parallel Poll Enable Register Enable

After browsing the document for some time I actually started to like it a bit. While not exactly casual reading, the IEEE 488.2 commands and how the mainframe will respond to them is the key, not the dust covered BASIC statements littered all over the command examples. While their intention is good, in 2013 it just looks so dated you know you're not going to program it that way. The syntax diagrams and command tree diagrams are very nice though and the guide seems thorough to the point of tautophony.

A tip, when you see a line like this:


What you should consider relevant is:


The OUTPUT XXX; and ENTER XXX; parts are specific to HP-BASIC and they're just there to remind you that you should use any program in any language as long as you substitute them with appropriate functions that either output a line to or read a line from the serial port. You do not write 'OUTPUT XXX' to the instrument.

The heading of this section is the name of one of the common commands, as you can see they're not necessarily very descriptive unless you read the guide enough to get to terms with the internals of the system and the various registers. The guide has tables listing what each bit in each of the registers means. Luckily there are also self-explanatory commands like BEEPer.

Consider the first program example listed in the guide:

10 CLEAR XXX  !Initialize instrument interface
20 OUTPUT XXX;":SYSTEM:HEADER ON" !Turn headers on
30 OUTPUT XXX;":SYSTEM:LONGFORM ON"     !Turn longform on
40 DIM Card$[100]   !Reserve memory for string variable
50 OUTPUT XXX;":CARDCAGE?" !Verify which modules are loaded
60 ENTER XXX;Card$  !Enter result in a string variable
70 PRINT Card$      !Print result of query
80 OUTPUT XXX;":MMEM:LOAD:CONFIG 'TEST E',5"   !Load configuration file
       !into module in slot E
90 OUTPUT XXX;":SELECT 5"  !Select module in slot E
100 OUTPUT XXX;":MENU 5,3: !Select menu for module in slot E
60 OUTPUT XXX;":RMODE SINGLE"     !Select run mode
70 OUTPUT XXX;":START"     !Run the measurement

It looks even more horrible in the PDF, I tried to format it similarly. Did you notice the typo? I don't mean the odd numbering. All the XXXs refer to the "device address" which is another way of saying "use your host language to write this string to the right port".

So basically a terminal program should do for simple queries. The above complete program (abbreviated to short form) could be executed by:

$ cat /dev/ttyS0 # read answer

Yes, you could operate it easily with shell scripts. Though, this assumes the serial port has been configured properly with stty or so first.

The response to the :CARDCAGE? query would be left in the instrument's output buffer to be read or until it is replaced with the result of the next query.

Inside the beast

The 16500B has three subsystems:

  • SYSTem subsystem - things like configuration, data queries etc.
  • MMEMory subsystem - filesystem
  • INTermodule subsystem - intermodule configuration

Each of the subsystems have their own set of commands. Additionally there are Common commands (you could call them "global" commands) and top-level commands for the mainframe. Each of the actual modules have their own programming manuals and I'm not going to cover them here, I also still lack the programming manual for the 16532A. Their commands should be similar though. When using module specific commands the appropriate module needs to be chosen with :SELect first.

What's with the caps? When the guide says :INTermodule:INSert you know that it can be abbreviated to :INT:INS. That, by the way, was a 'compound header', just meaning that from the root of the command tree we branch to the INTermodule subsystem, and from there we branch to the INSert command. Actually INSert requires arguments though, so :INT:INS is just a command header, not a complete command.

The instrument has an internal command parser that attempts to turn the IEEE 488.2 instructions it receives into meaningful operation, ie. find a valid command by traversing a command tree that includes global, top-level, subsystem-specific and module-specific commands.

  • A colon : in the input means that the parser is placed at root or to signify a branch in the command tree. No whitespace is allowed around colons.
  • You can traverse the command tree and perform multiple commands with a single command string.
  • Each command on the same input line should be separated by a semicolon ;.
  • The command or query is executed when the instrument receives a newline \n, except for some commands that expect block data, where the data content can be arbitrary binary data using a definite-length block header.
  • The parser is not case-sensitive.
  • If a command header ends with a questionmark ? - for example *OPC? - it's a query, which means the instrument is expected to answer back. Most commands that affect settings also have a query form to read the current setting, ie. *ESE <mask> would set the ESE ('Event Status Enable') register bits and *ESE? would query the current value.
  • String parameters must be quoted: :MMEM:CD 'dir'.

When executing a subsystem command, the parser is placed to that subsystem and subsequent commands within the same subsystem branch can be sent on the same command string without needing to prepend a command header for each, just separate the commands with a semicolon. It mostly saves some bytes, but is not that relevant unless you're manually typing the commands.

The instrument will only talk over the serial after it receives a valid query message. It doesn't babble on its own. In case of errors, they are not spurted out the serial line automatically, instead you need to ask the instrument to dequeue the oldest item in the error queue and then read it out. The front panel however has extraordinary error messages, usually highlighting the exact error clearly with some text to explain it if you send an invalid query or command.

A few examples

:MMEM:CD 'dir'                  # change directory (on selected drive)
:BEEP                           # beep!
:SYSTEM:DSP 'hello world'       # display message on CRT
:MENU 0,1                       # show hard disk menu (module 0 = system, menu 1 = hard disk)
:LOCK 1                         # lock front panel operation (use :LOCK 0 to unlock)


':MMEM:INITialize' is dangerous: it will format storage devices. The storage unit specifier is optional and without arguments it will immediately format the currently selected disk. I think. Yikes! If you happen to accidentally type this command to the instrument, hope you had the floppy selected or nothing of value there, because otherwise the data on its hard disk just got wiped. It's sort of funny that one of the first example snippets in the guide contains commands to first format the default disk and store a file on it, without explanation what the command does.


The hard disk is in 'DOS' format, supposedly byte swapped FAT, no partition table (reference: I should probably do the CF hack too eventually.

MMEMory subsystem houses the various commands to access the disks and manipulate them. 'INTernal0' refers to the hard disk and 'INTernal1' to the floppy disk. These strings can be used whenever a command has an <msus> parameter to designate the disk, otherwise the currently selected disk is used.

To select a disk, you can use :MMEM:MSI ("Mass Storage Is"). :MMEM:MSI INT1 selects the floppy. If you're familiar with DOS, there shouldn't be many surprises: 8+3 filenames, working directory separately for each drive. You can use either the slash / or backslash \ as a directory separator and the maximum pathname length the commands will accept as arguments is 64 characters. You can use either absolute or relative pathnames. A file at the root of the disk can be referred to with '\FILE'.

Getting shot, part II

After browsing through the guide for some time I found the :SYSTEM:PRINT command and noticed that it takes a filetype as parameter and one of the possible values was color "PCX". I got excited because this would mean that there is a way to get quality screenshots as long as I get them out of instrument somehow. Using floppies wouldn't cut it though.

To get things started I wrote a minimal program to write a line to the serial port, I used the :MENU 3 command to select the oscilloscope module (I have it in slot C). After I ran the program the instrument quickly changed to the scope, thus the program worked.

Encouraged by this I tried the :SYSTEM:PRINT command to save a .PCX to the hard disk. Seemed to work fine, now I just had to get the image out of the thing.

There is a MMEMory subsystem command called :MMEMory:DOWNload but this of course means downloading data into the instrument, not send it out. Then there's the :MMEMory:UPLoad? query which actually sends a file out of the instrument.

When the instrument sends binary data it formats it with a 'definite-length block data header'. This means the following format: #800000005Hello.

  • '#' starts the block header
  • '8' means that the next 8 decimal digits represent the length of the data
  • '5' means (in this example) that 5 bytes will follow
  • 'Hello' is the 5 bytes of actual data.

Another way to encode the 'Hello' would be #15Hello.

So, to get a file out I'd just need to make a little function to receive the data when the instrument sends it. After a few moments I had something like this:

blockpound =  # 1 is there for timeout reasons
if blockpound == '#':
        numdigits = int(
        numdata = int(
        data =

And turns out it worked, I could get the PCX screenshot back to the Pi! Yes! For the first time ever, I could view an actual color screenshot from the thing (ok, there is one screenshot on the internet:

Not stopping there

So now that I had the means to take the coveted screenshots, I figured I'd try to make a small module for using the instrument. So I started coding a Python class that would have enough functionality to work as the basis for high-level functions like:


Just without the BASIC. I first implemented methods to send commands and queries, then methods to send and read datablocks. The mainframe alone has 64 or so commands plus a query form for many of them, so I figured it should be enough to implement all of these and leave the module-specific programming out. While most of them are simple oneliner methods to call the command/query method with a certain string and format the replies, there are exceptions that need greater care. The query methods return proper Python objects instead of plain strings, ie. if the instrument gives you a string representing a list of numbers, the query method should give you a meaningful numeric list out of them.

Since the operation of the instrument's parser, command syntax and the data formats are well defined I felt that making all these methods that are essentially aliases to printing text was a bit silly since there are much more elegant solutions. But I just wanted results and didn't know Python well enough, so I decided to keep it simple instead of spending the week planning some domain-specific language and making it multi-threaded or whatever. It's still a step up from HP-BASIC.


Things were looking good, quickly I had the barebones implementation of all the commands the mainframe would support. After some tweaking they began to be useful: I could read files from the instrument, switch menus, configure run modes and start the acquisition and so on.

Then I tried sending files to the instrument and things turned sour. It seemed that if I sent too big a file (say, 78+ bytes or so) the instrument would go berserk and started beeping with red RS-232 Errors filling the screen for the rest of the input and the front panel was unresponsive until I gave it the data it expected much slower. I spent a couple of days debugging the issue, suspecting that the receive buffer on the instrument probably gets clogged since if I sent the data really slow (say, 100 bytes / s) it worked. This was way too slow, even though Agilent mentions that delays may be needed for RS-232, they probably didn't mean this.

Turns out I didn't pass the keyword parameter rtscts = True to the serialport constructor so the buffer was getting clogged. Doh! If you use the 3-wire interface, use xonxoff = True. I noticed this after temporarily adding something like this in the middle of the sendblock method:

while not serialport.getCTS():

Which would check that the CTS line is high before attempting to send more data to the instrument. This wasn't enough though, the thing still hung after big uploads unless I padded the input with 32 newlines. Go figure. I found this out after trying to wake it from the frozen state by just supplying it some extra data, after which it would eventually write the file to the disk and continue normal operation. I suppose this has to do with the instrument reading fixed size blocks when binary data transfer has begun and it just craved more data. The extra newlines don't hurt, I didn't find detailed documentation on the instrument's serial buffers (or overlooked it) and didn't study it further after noticing 32 seems to be enough.

So now I could also send files to the instrument, neat!

Getting shot, proper

A week ago I started this project by creating the serial adapter and now I have full control of the instrument with bi-directional file transfer. Since I could now do all sorts of fancy things with Python, I figured that I would give homage to these instruments and their beautiful screens that aren't well on display online. It's actually a CRT touchscreen, apparently the touch is implemented with an infrared led matrix instead of the resistive or capacitive layers you would typically find on touchscreens these days. It's also very sensitive and responsive, you can just hover your fingers over it to click without necessarily touching.

I thought that I should programmatically screenshot every available menu. Actually not all of them are available at all times: some appear only when you have certain settings such as state analysis enabled for a logic analyzer Machine, but I didn't bother finding all settings to maximize the visible content. And by menus I mean the displays you can switch to using the front panel's two main buttons at the top, not the actual module-specific menus which there are plenty in addition to these.

This can be done with:

screenshot_menus("HP16500B", menumap())

Menumap will iterate through all reasonable values for :MENU arguments (I wonder if there are hidden menus...), reading the error queue after each attempt to switch the menu. If there is an error (-211, "Legal command, but settings conflict") the menu doesn't exist for the current configuration. The beeper is also disabled during iteration so it's not annoying even though the screen will be flooded with red error messages. This is actually not very efficient because it first finds the menus and gives the result to screenshot_menus which will re-iterate the list. But the command should work with varying module configurations. It returns the menumap list so you don't have to find them again.

menumap() will return a list of all the menus available in the instrument, this of course varies with your module configuration. screenshot_menus expects a menumap that it will iterate through and call the screenshot method on each. Thus, for every existing menu a screenshot is saved onto the instrument's disk (using the same filename for all) and the screenshot is sent through serial and saved as a PNG image with the Python imaging library. With the above command the files will be called HP16500B_3-4.png and so on. You may want to copy-paste your menumap for further use so there's no need to search them again.

Here is a gallery of the resulting pictures for your enjoyment with the default color scheme (I usually use a darkish green hue):

HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot HP16500B screenshot

Using the module

To use the module, just import it: there are no big dependencies or files needed besides In addition you only need:

  • pySerial
  • Python Imaging Library
  • apt-get install python-serial python-imaging

You don't necessarily need the imaging library if you don't intend to convert the screenshots to PNG, which is the default.

To create a connection to your instrument, the easiest way is to run the supplied script which will start up a python REPL for you to interactively type commands to the instrument. It also creates an HPLA ('HP Logic Analyzer') object for you, just edit the file to have the right serial port settings. All of the module's functionality is defined within the HPLA class. A typical session might look something like this:

ghettoIB REPL

Do NOT turn command headers on (hp.syst_header(1)) because it will cause the instrument to prepend each query response with useless strings and I didn't bother handling them.

Note that during operation the methods print a lot of debug messages, to suppress output use the debug=False keyword argument when creating the instance. Most of them have been omitted in the examples below. If you don't like color, use color=False.

>>> import ghettoib
>>> hp = ghettoib.HPLA("/dev/ttyS0", 19200)      # adjust settings if necessary
[1368345449.51  initialize]     Opened serial port /dev/ttyS0
>>> hp.main_menu(3)                             # switch to module slot 3 (C)
>>> hp.cmd(":LOCK 1")                           # lock the front panel
>>> hp.query(":LOCK?")                          # should return 1 as string
>>> hp.cmd(":LOCK 0")                           # unlock the panel

Of course, you don't need to type the IEEE 488.2 commands, instead you can use methods provided by the class:

>>> hp.main_lockout(1)
>>> hp.main_lockout_query()
>>> hp.screenshot("woohoo.png")                 # capture screenshot
>>> hp.installed_modules()
[(3, 'HP 16532A Oscilloscope Card', 3), (4, 'HP 16520A Pattern Generator Master Card', 4), (5, 'HP 16550A Logic Analyzer Master Card', 5)]

By the way, you can also receive the current screen in PCL like the Tcl script did, the command for this would be :SYSTem:PRINt? SCReen. It's usefulness compared to PCX is limited though:

>>>"screen"), "screeny.pcl")

In state listings it seems to print out the complete listing appended with PCL data. I didn't examine it much.

The commands are categorized into groups with prefixes indicating which (sub)system they belong to: comm_ (common/global), main_ (mainframe top-level), syst_ (SYSTem), mmem_ (MMEMory) and inter_ (INTermodule). The query messages (*OPC? etc.) are sent with <category>_<command>_query(). The result is also read back and returned. For example:

>>> hp.comm_opc_query()                         # waits until other operations finish

The comm_opc_query() is useful for waiting for the instrument to finish whatever it's doing. When it's done, a "1" will appear in its output buffer. By default there also is a huge timeout for this method (15 minutes). You can override it with the keyword argument timeout=<seconds>.

To save and load block data and other files on the controlling computer's disk, you can use the save and load methods combined with commands or queries.

>>>"hello world", <filename>)
>>> hp.load(<filename>)
'hello world'

To transfer files back and worth, you can use put and get:

>>> hp.put("local.dat", "remote.dat") # sends local.dat and saves it as remote.dat on the instrument
>>> hp.get("local.dat", "remote.dat") # retrieves remote.dat and saves it as local.dat on the controller

You can get a directory listing with:

>>> hp.mmem_catalog_query()

It returns a list of (<filename>, <type>, <description>) or (<filename>, <type>, <description>, <date>) tuples. The longer form can be queried with:

>>> hp.mmem_catalog_query("all")   # any argument will do

To change the current drive:

>>> hp.mmem_msi("INT0")         # selects hard disk

If you mess up badly or Ctrl-C during a transfer or similar and the instrument appears frozen, you can usually recover by giving more data and flushing the instrument's output buffer before supplying further commands:

>>> hp.send('\n'*1000)          # repeat if necessary
>>> hp.flush()                  # read whatever the instrument wants to say
>>> hp.flush_errors()           # flush errors also for good measure

A practical example, switch to scope (in my case module 3), set repetitive run mode, start and stop the acquisition:

>>> hp.main_menu(3)                     # switch display to scope
>>>                        # select scope for further commands
>>> hp.main_rmode("repetitive")         # now the scope run mode will be changed
>>> hp.main_start()                     # start the acquisition
>>> hp.main_stop()                      # stop

I've only made a handful of the high-level methods but there is potential for much more. I was going to make a tablet UI for running scripts to command the instrument and perhaps even use speech recognition (or a foot switch) to enable autoscale/run/stop on the scope or so.

One useful feature is the saving and loading of acquisition data. It's not instantaneous because the roughly 64kB scope buffer for example with the maximum speed of 19200 bps takes around 30 seconds to transfer over serial (sending them back is a bit slower), but allows you to save and load your measurements nicely for further processing or safekeeping:

>>> hp.save_data()              # saves data from the current (selected) module
>>> hp.load_data(filename)      # loads the data to the current module

Both have a filename and module parameter so you can also:

>>>"scope-data.dat", 3)        # save data from module 3 to file

For settings, use save_settings and load_settings.

One funny method is dimscreen():

def dimscreen (self):
        """Set all colors black, one by one."""
        self.dbg("Dimming screen...")
        for c in range(1,8):
                self.main_setcolor(c, 0, 0, 0)

(there is togglescreen() to toggle blackness on and off)

This makes the UI elements disappear one by one into the darkness (use main_setcolor_default() to return the default colors back). You could also make a trippy 'screensaver' that would cycle all colors in a loop or indicate some sensor reading by changing the background color, or whatever.

I also added a confirmation prompt to mmem_initialize() so you have the chance to verify the right disk is selected if you do want to format the filesystem. With non-interactive use you may want to remove that feature.

To simulate what would be sent to the device, you can use socat to create virtual serial ports. Here's a nice post with instructions on how to do this: . Another useful tool might be jpnevulator so that you can sniff the serial line.

Read the accompanying README.txt for some more info on the code and a summary of all commands, I started writing it before I started on this post so it maybe tells mostly the same things. Both may have some inconsistencies so the best way is to read the source, probably at least 90% of it is comments, it's very simple and straightforward and lacks most error checking because for the next few weeks at least I have some other things to attend to. That's why there are also bugs and stuff that has been overlooked. I'd like to spend some more time on it but that'll have to wait.

The code itself contains excerpts typed from the programming guide mostly verbatim but with some fixes and abbreviations. I consider it fair use and in case the PDF at some point vanishes, some of the relevant information will still be with the source.


So, after some effort I got what I wanted. It took a lot of digging for information and while it's naively implemented, probably very unpythonic, clunky and almost devoid of error handling it's still more convenient than anything else I've found for now. I hope that posting this online will encourage a real programmer with perhaps even background in measurement instrument control to implement a reliable, hassle-free program to give these legacy (and modern!) devices new life, or at least save some time from someone trying to achieve the same things by demystifying what needs to be done.

That said, by hassle-free I mean NO unnecessary, huge dependencies for platforms you probably wouldn't be using anyway if your goal is to run this stuff on a microcontroller or such. In my case I wanted to use the Pi. Someone else might use an AVR or something for similar, simple purposes.

I would very much appreciate any bug reports or comments about the code in general, or if it was useful to anyone. I welcome any suggestions for improvement, however since the code is MIT licensed, feel free to do whatever you want with it. Mostly I would like tips on what would be the best way to:

  • make it more pythonic, less a shell script
  • implement a flexible command dispatcher that would handle formatting the string commands correctly for the instrument and would be easy to extend
  • make it multithreaded so you can control many instruments within the same REPL
  • make it fool-proof with error checks and reasonable corrective action

For lack of a better name I call it ghettoIB, you can get it with git as usual:

git clone

Jouko Strömmer

jouko dot strommer at iki dot fi