Structural Overview of JISA - OE-FET/JISA GitHub Wiki
Structural Overview of JISA - "How the darn thing works"
This page is intended to give an overview of how the JISA
library is structured and how it functions. It is not intended for those who simply wish to use it, create simple extensions or those faint of heart. This exists purely as a reference for those who really want to understand how it works down to a fine level of detail.
The library can generally be broken down into three parts:
- Instrument Control
- Data Handling
- GUI Creation
We shall cover each in its own section.
Instrument Control
JISA
provides the means to communicate with instruments over a multitude of different protocols. First and foremost, it is designed to work using a VISA
library (for example, National Instruments VISA). However, since most VISA
implementations usually have at least one weakness depending operating system (for example GPIB using NI-VISA on linux is normally a fools errand), it is capable of falling back onto more primitive libraries to provide I/O.
Drivers
When connecting to a device, JISA
will first try to use VISA
. However, in the event that this fails it will try other libraries until it finds one that works. If none work, it will throw an exception. To implement this functionality, JISA
has an interface called Driver
. All classes that implement Driver
are required to provide two methods:
public interface Driver {
// Attempt to connect to the device at the given address using this driver
Connection open(InstrumentAddress address) throws VISAException;
// Scan for any instruments that can be communicated with using this driver
StrAddress[] search() throws VISAException;
}
The first of these, open(...)
, should try to establish communications with the specified instrument, throwing an exception upon failure. If it succeeds, it should return a Connection
object which represents the now open connection to the instrument, more on this later. This is used by JISA
when trying to establish communications with an instrument.
The second method should scan using that driver for any connected instruments, returning an array of their addresses. This is used when VISA.getInstruments()
is called (eg when we use GUI.browseVISA()
).
Implementations of Driver
currently include:
VISADriver
- VISA LibraryGPIBDriver
- Linux-GPIB LibraryNIGPIBDriver
- National Instruments GPIB LibrarySerialDriver
- Native COM LibraryRawTCPIPDriver
- Native TCP-IP Library
VISA Class
The VISA
class is a purely static class that serves as a layer between the various driver objects and instrument objects. Upon initialisation (ie the first time it is accessed), it will try to load up each driver by instantiating each driver object. Each one that successfully loads is added to an array. The result is output to the terminal:
Attempting to load drivers.
Trying VISA driver... Success.
Trying Linux GPIB (libgpib) driver... Nope.
Trying NI-GPIB (ni4882) driver... Success.
Trying Serial driver... Success.
Trying Raw TCP-IP driver... Success.
Successfully loaded 4 drivers.
As we can see, in this example all but the linux-gpib library was successfully loaded (which makes sense considering this was run on a Windows installation with NI-VISA installed). If none load successfully, then the program will terminate in an error state.
The VISA
class now provides two important funcionalities:
public class VISA {
public static Connection openInstrument(InstrumentAddress address) throws VISAException {...}
public static StrAddress[] getInstruments() throws VISAException {...}
}
The first of these, openInstrument(...)
, will successively try to open the specified instrument using each driver's open(...)
method in the loaded drivers array until it finds one that does not throw an exception. It then returns the Connection
object from that driver. If all drivers throw an exception, then so will openInstrument(...)
, saying that the instrument could not be opened using any available driver.
The second method, getInstruments()
successively calls the search()
methods of each of the loaded drivers. It then returns a combined array of all unique addresses found.
In essence then, VISA
is a static class that acts to combine all the individual drivers into a standard means of opening communications with an instrument.
Connection Objects
JISA
also defines an interface called Connection
. This is what is returned when opening a connection using a Driver
visa VISA
. These objects are required to provide:
public interface Connection {
void write(String toWrite) throws VISAException;
String read(int bufferSize) throws VISAException;
void setEOI(boolean set) throws VISAException;
void setEOS(long character) throws VISAException;
void setTMO(long duration) throws VISAException;
void setSerial(int baud, int data, Parity parity, StopBits stop, Flow flow) throws VISAException;
void close() throws VISAException;
}
write(...)
should take a String
and send it to the instrument.
read(...)
should read from the instrument and return what it has read as a String
.
setEOI(...)
should set whether to use the EOI (End or Identify, a GPIB standard for indicating that a message is complete) line when using GPIB communications. For other forms of communication this method will do nothing.
setEOS(...)
(End of Stream) should set what character should be considered to indicate the end of a message. For example, some instruments will send a carriage-return (CR) character: \r
or a line-feed (LF) character: \n
or a CRLF: \r\n
to indicate that it has finished talking.
setTMO(...)
(time-out) should set how long the connection should wait on an operation before timing out. By default this is set to 2 seconds.
setSerial(...)
should set the serial parameters used when operating of a serial connection (such as RS-232). For other forms of communication this method will do nothing.
close()
simply closes the connection.
In essence then, a Connection
object is a handle on the newly-opened connection allowing for data to be sent and received through it.
VISADevice Class
To hide this complexity further, a base class called VISADevice
is defined. When instatiated, it takes an address and attempts to acquire and store a Connection
object using VISA.openInstrument(...)
. It then uses its Connection
object to provide the methods write(...)
, read(...)
and query(...)
.
A simplified version of the class is shown below:
public class VISADevice {
private Connection connection;
private InstrumentAddress address;
private String terminator = "";
private int readBufferSize = 1024;
public VISADevice(InstrumentAddress adrress) throws IOException {
try {
this.connection = VISA.openInstrument(address);
this.address = address;
} catch (VISAException e) {
throw new IOException(e.getMessage());
}
}
public void write(String command, Object... args) throws IOException {
String commandParsed = String.format(command, args).concat(terminator);
connection.write(commandParsed);
}
public String read() throws IOException {
return connection.read(readBufferSize);
}
public String query(String command, Object... args) throws IOException {
write(command, args);
return read();
}
public double queryDouble(String command, Object... args) throws IOException {
return Double.valueOf(query(command, args));
}
}
The result of this is that now a connection can be established by instatiating an object, allowing extensions of VISADevice
to be made which further utilise the read/write methods it provides to implement specific device commands like so:
public class MySMU extends VISADevice {
public MySMU(InstrumentAddress address) throws IOException {
super(address);
}
public double getVoltage() throws IOException {
return queryDouble("MEASURE:VOLTAGE?");
}
public double getCurrent() throws IOException {
return queryDouble("MEASURE:CURRENT?");
}
public void setVoltage(double voltage) throws IOException {
write("SOURCE:VOLTAGE %f", voltage);
}
public void setCurrent(double current) throws IOException {
write("SOURCE:CURRENT %f", current);
}
}