Emulating Electronics Lab Equipment
While developing software with the intent of running automated tests on hardware, it became an issue when said hardware was unavailable due to various reasons. I didn't want to have to stop working on my project every time the limited physical devices were being used. Before going any further, it is essential to describe the situation at hand.
Talking to test equipment over the network
Pretty much any modern test equipment allows over the network communication via various protocols, in our case the equipment uses a protocol called HiSLIP. The protocol is very well documented over at ivifoundation.org. One of the issues, however, is that the documentation isn't very clear on certain things and mentions that some vendors can put their spin on some of the requests. Luckily for me, I needed to emulate the equipment, which is the server in this protocol. That means that for the most part any of the vendor-specific things can be ignored, as they are usually implemented on the equipment its self rather than in the control software.
Once a connection from a client is established, most equipment will begin listening for commands. There are many commands that all do different things based on the specific equipment, but they all share the same mechanism for being sent down the wire. Each command is sent as a simple ASCII string with specific parameters that define what the equipment is supposed to do. As an example, the Keysight DSOZ204A has the following command: :MARKer:CURSor?. The question mark indicates it is a query meaning that it is sent with the intent of the device sending back a response. This is in contrast to a command where the string is sent, and no response is expected (the client will not wait for a response so sending one does nothing and will most likely result in the client/server pair going out of sync). Through commands and queries, it is possible to accomplish almost anything you can do while sitting in front of the scope.
The HiSLIP protocol
Figuring out how precisely the protocol works was probably the most time-consuming part of this project. Although it is documented fairly well, many subtle things can cause issues if you aren't careful. Here we're going to go over the basic form of the protocol as taken directly from the documentation.
|Prologue (ASCII “HS”)||2||0|
This is the official message header specification as found in the protocol documentation. Next, I'm going to go over each part and explain what it means along with how to deal with it.
The prologue is a very simple field. Its primary purpose is to act as a protocol sync check. Essentially, every packet you receive from the client should be prefixed with the two ASCII characters "HS". If these two characters are not at the start of the packet you know that the client has gone out of sync with the server and you can throw an error. Parsing this field is as simple as reading two bytes from the input stream and then converting that to a string. In C# that looks like this:
int read = inputStream.Read(prologue, 0, 2);
string prefix = Encoding.ASCII.GetString(prologue, 0, read);
Message type is a one-byte value that identifies the packet. Dealing with it is as simple as reading in a single byte. It is very important as the value of this byte will determine what logic is needed to read the rest of the packet, along with what each field in the packet represents.
The control code is a single byte field (8 bit) that is used by certain packets to convey extra information. An example is in the initalize packet it is used to define the overlap mode. If the packet does not use this field 0 will be sent.
Message parameter is a 4-byte field that is used by many packets to send extra information. The initialize packet uses this field to send the client protocol version along with the client-vendor id. Reading can be done simply with:
byte messageParam = new byte;
read = inputStream.Read(messageParam, 0, 4);
Payload length is fairly straightforward, it is a UInt64 or an unsigned long and contains 8 bytes. It represents the length of the following byte array that contains the packet's data.
The data field is a byte array that contains the number of bytes specified in the payload length. Depending on the message type this can contain anything from command strings to channel synchronization data.
Decoding a real packet
By searching the internet it is very easy to find a Wireshark capture file that contains a network exchange between a client and a server. We can use this capture file to demonstrate how the protocol works without needing to do any communication our selves.
00 e0 33 da 03 44 f8 b1 56 ab d0 d6 08 00 45 00 3f 3b b5 40 00 80 06 00 00 0a 40 00 7f 0a 00 48 c7 6d 13 10 9c 98 11 9f cc 76 44 dc 50 01 00 15 78 00 00 48 53 00 00 01 00 52 53 00 00 00 00 00 00 07 68 69 73 6c 69 70 30
This is an initialize packet. A lot of the stuff here is not actually important to us as it's TCP fluff that is required to get the packet from point A to point B. The part of this we care about is the following:
48 53 00 00 01 00 52 53 00 00 00 00 00 07 68 69 73 6c 69 70 30
Now let's go over each part:
48 53 -> This converted to decimal is 72 83, which converted to ASCII is HS. As per the specification, the first part of the message is, in fact, HS and we can continue.
00 -> The value here is the message type, which according to the protocol documentation is the initialize packet
00 -> Represents the control code, which for an initialize packet is not used (so 0 is sent)
01 00 52 53 -> Message parameters for the initialize packet are split in half, the upper half is the client protocol version and the lower half is the vendor. Client version is then 0x0100 and the vendor is RS (82 83)
00 00 00 00 00 07 -> Payload length. This is just a long that tells us the following payload has a length in bytes of 7.
68 69 73 6c 69 70 30 -> Payload. In this case, the initialize packet defines the payload as a sub-address which corresponds to the device's VISA name. In this case, these bytes become "hislip0" when converted to ASCII.
Doing something meaningful with this data
So now that we know the structure of a packet and have seen an example of it in action, let's define at a higher level what is needed to get some communication going. There are two channels that each client uses to communicate with the server, the synchronous channel and the asynchronous channel. The difference is basically that the async channel can do multiple things at once while the sync channel works in a one after the other fashion. The majority of the commands used by the test equipment I needed are sent over the sync channel, so consequently, I don't pay much attention to the async channel.
One thing that is important to remember is that when I say "channel" I really mean a separate TCP connection from the same client that is used for async communications. This means that for every client that connects to your server, there will be 2 incoming connections. One from the sync channel and one from the async channel. The first design challenge for our server is how we will link these two channels together such that data from one can be used in the other. I opted to solve this with a relatively simple parent class called ClientInfo that gets created at the same time as the sync channel does. Once the async initialize packet comes through it attempts to find the ClientInfo with the matching sessionId and then sets the channel's ClientInfo to the same one as the sync channel.
if (messageType == 17)
ClientInfo info = clients.Where(x => x.SessionId == info.SessionID).FirstOrDefault();
info = sync;
Example C# code that links the two channels together
Once the two channels are connected we can start receiving commands and queries from the client. The following is an example transaction between a client and server after the client opens a TCP connection to the server.
- Client -> Server:
- Server -> Client:
- Client opens new TCP connection to server
- Client -> Server:
- Server -> Client:
- Client -> Server:
For more information on each individual packet, consult the protocol specifications.
The way my code handles each packet is by placing each class that handles a packet into a dictionary and indexing into it by packetID. Then I call Packet.Read() on the returned packet instance and it parses the packet off the input stream. Now in the case of DataEND packets, the client sends commands and in order to respond, you must send back the DataEND with the same MessageID. This is where the fun begins as you can do anything you like with the incoming packets and then chose how you want to respond to each one. I used this to emulate specific parts being tested, where certain values are returned by the server for certain input values, allowing the design of test cases.