Hello SIAM: your first SIAM instrument service

The Instrument Service Framework simplifies the process of writing new instrument services by providing most of the basic facilities needed to acquire and log data. This example is intended to illustrate the basic steps involved in writing a very simple SIAM instrument service. We'll make an instrument service that collects data through a serial port from a dummy instrument, the Fake-O-Tron.

The Fake-O-Tron has a very simple serial API with three commands: "getData", "setInc" and "getInc"

This example illustrates several steps that most real-world service developers will want to know how to do.
Here's what you can learn with HelloSIAM:

What you'll need

The source code, binaries and associated files for this example may be found in siam-site/content/examples/code/HelloSIAM.
Please see the src directory, packages

Quick Start: Install Example Code

Instructions for installing and running the HelloSIAM example are found in this section below.
SIAM must be installed and the SIAM environment configured (see the SIAM Quick Start Guide for details).

To uninstall the code for this example, see this section below

Step 1: Choose a sampling strategy

The SIAM instrument service Framework uses PolledInstrumentService to implement basic services for instruments that are polled for their data. The Fake-O-Tron can be configured for polling (default) or streaming; we'll use PolledInstrumentService for our HelloSIAM service.

Step 2: Implement required methods

BaseInstrumentService defines several abstract methods that must be implemented by sub-classes. The table below briefly describes these:

Method Signature Description
HelloSIAM public HelloSIAM() throws RemoteException No-argument constructor
requestSample protected void requestSample() throws Exception Bytes to send to request a data sample
initializeInstrument protected void initializeInstrument() throws InitializeException, Exception Initialize the instrument (executed once at service start)
getInstrumentMetadata protected byte[] getInstrumentMetadata() Return metadata supplied directly by the instrument
initPromptString protected byte[] initPromptString() Initialize instrument prompt string (used for parsing instrument response)
initSampleTerminator protected byte[] initSampleTerminator() Specify character(s) that signal the end of sample data
initMaxSampleBytes protected int initMaxSampleBytes() Specify maximum bytes received in raw sample
createDefaultSampleSchedule protected ScheduleSpecifier createDefaultSampleSchedule() throws ScheduleParseException Return ScheduleSpecifier for default sampling schedule
getSerialPortParameters public SerialPortParameters getSerialPortParameters() throws UnsupportedCommOperationException Return serial port configuration parameters
initInstrumentStartDelay protected int initInstrumentStartDelay() Specify device startup delay between power on and sample request (millisec)
setClock public void setClock() throws NotSupportedException Set instrument internal clock or throw NotSupportedException
initCurrentLimit protected int initCurrentLimit() Specify current limit (mA) (the Fake-O-Tron doesn't use any current, so this may be set to anything)
initInstrumentPowerPolicy protected PowerPolicy initInstrumentPowerPolicy() Return initial value of instrument power policy
initCommunicationPowerPolicy protected PowerPolicy initCommunicationPowerPolicy() Return initial value of communication power policy (whether to power down RS232 tranceivers, etc. when not in use if supported by hardware
test public int test() Perform instrument self-test
enterSafeMode public void enterSafeMode() Enter safe mode, i.e. prepare for immediate power down. This method may be invoked by the power management system, allowing critical instruments to reduce sample rates, configure internal logging etc. and continue operating for as long as possible during an unscheduled power outage.

The HelloSIAM source has example implementations of these.

Step 3: Implement the instrument-specific code

Data members

We'll add a few member variables to represent instrument commands, and we'll also use extend the default service attributes.


	/** Command used to request a data sample */
	public static final String CMD_GET_DATA="getData";
	/** Command used to change instrument data increment */
	public static final String CMD_SET_INC="setInc";
	/** Command used to change instrument data increment */
	public static final String CMD_GET_INC="getInc";

	/** Extended attributes */
	protected HelloAttributes _attributes;

Methods

The HelloSIAM constructor is pretty simple. Since we are going to use an extension of the default ServiceAttributes class, we need to instantiate our special attributes in the constructor:


	public HelloSIAM() throws RemoteException {
		super();
		_attributes = new HelloAttributes(this);
	}

DeviceService has a protected member variable called _attributes. When we declare a member _attributes with a HelloAttributes type, it is distinct from the _attributes member in the base class.
Remember to cast it correctly ((HelloAttributes)_attributes).someMember when accessing HelloAttributes members.

The requestSample() and readSample() methods are overridden to acquire a sample from the instrument.
These are called by the acquire() method in BaseInstrumentService, which manages power, comms, data logging etc. each time a sample is requested.
The BaseInstrumentService does have a default readSample(), and we'll override it to handle the echoed commands and a prompt string issued by the instrument.

We'll also override the getInstrumentStateMetadata() method to return the dataIncrement setting of the instrument.

Here are our versions of these methods:

 
	 protected void requestSample() throws Exception {
		 // request data with getData command
		 _toDevice.write(CMD_GET_DATA.getBytes());
		 
		 // flush the output stream
		 _toDevice.flush();
		 
		 // Skip the echoed command characters
		 StreamUtils.skipUntil(_fromDevice, CMD_GET_DATA.getBytes(), 2000);
	 }
	
	protected int readSample(byte[] sample) throws TimeoutException,
	IOException, Exception{
		// use the super class readSample to read the data
		super.readSample(getSampleBuf());
		
		// Also read the prompt that follows the data
		StreamUtils.skipUntil(_fromDevice, getPromptString(), 2000);
	}
	
	protected byte[] getInstrumentStateMetadata()
	throws Exception {
		String metadata=null;
		try{
			// Get the current instrument setting for the data increment value
			int dataInc=getInc();
			metadata="inc:"+dataInc;
		}catch (Exception e) {
			metadata="Error: could not get metadata ["+e.getMessage()+"]";
		}
		return metadata.getBytes();
	}

We'll add a few methods of our own. Two methods, setInc() and getInc() expose the corresponding commands in the Fake-O-Tron API. Additional methods are added for convenience and re-use:


	//////////////////////////////
	// Instrument specific methods
	//////////////////////////////
	/** Expose setInc function in Fake-O-Tron API */
	public void setInc(int newInc) 
	throws Exception{
		_log4j.debug("writing setInc");
		writeRead((CMD_SET_INC+" "+newInc));			
	}
	
	/** Expose getInc function in Fake-O-Tron API */
	public int getInc() 
	throws Exception{
		_log4j.debug("writing getInc");
		String inc=writeRead(CMD_GET_INC);
		if(inc!=null){
			return 	Integer.parseInt(inc.trim());
		}	
		throw new Exception("could not get increment [writeRead returned null]");	
	}
	
	/** Utility method to send a Fake-O-Tron command and return the response */
	public String writeRead(String dataToDevice)
	throws Exception{
		sendData(dataToDevice);
		return readData();
	}
	
	/** Send an output line and clean up the echoed characters */
	public void sendData(String data) 
	throws Exception{
		_log4j.debug("writing data ["+data+"]");
		byte[] outBytes=(data+"\n").getBytes();
		_toDevice.write(outBytes);
		_toDevice.flush();
		// Look for response
		int len=StreamUtils.skipUntil(_fromDevice, outBytes, 2000);
		_log4j.debug("skipping echoed command...");
		_log4j.debug("skipped ["+len+"] bytes");
	}
	
	/** Read to sample terminator and skip any bytes up to the prompt */
	public String readData() 
	throws Exception{
		_log4j.debug("reading data");
		byte[] dataIn=new byte[getSampleBuf().length];
		Arrays.fill(dataIn,(byte)0);
		String returnData=null;
		try{
			// read the data
			int len=StreamUtils.readUntil(_fromDevice, dataIn, getSampleTerminator(),
								  _instrumentAttributes.sampleTimeoutMsec);
			_log4j.debug("read "+len+" bytes");
			// Skip the prompt
			StreamUtils.skipUntil(_fromDevice, getPromptString(), 2000);
			if(len<=0){
				returnData= null;
			}else{
				returnData=new String(dataIn);
			}
		}catch (Exception e) {
			e.printStackTrace();
		}
		return returnData;
	}

There are other methods we could override, but will not for this example:

Method Description
protected void prepareToSample() throws Exception Prepare the device for sampling; called before requestSample(). By default this method does nothing.
protected void postSample() Called after sample has been acquired, processed and logged. By default this method does nothing.
protected void validateSample(byte[] sampleBuf, int nBytes) throws InvalidDataException This method can optionally be overriden so the sub-class can determines the validity of the sampled bytes. By default this method does nothing.

Attributes

For illustration, we've also extended the instrument service attributes to expose the dataIncrement value. We can use this attribute to initialize the value when the service starts, and to change it at run time. First create an integer member and define a constructor for our attributes:


	protected class Attributes extends InstrumentServiceAttributes {
		// amount to add to data counter for each request
		int dataIncrement = 1;

		protected Attributes(DeviceServiceIF service) {
			super(service);
		}
	}

The value of this attribute may be set via setProperty() method in the BaseInstrumentService (there is a setProperties utility that does this at run-time). There are callbacks that may be overridden to perform validation or other actions before or after the value is changed.

We'll implement a callback to validate the value (we'll apply a policy that it must be non-zero for our service, although the Fake-O-Tron will allow any integer), and also to change the instrument setting when the property is set:


	protected void setAttributeCallback(String attributeName, String valueString)
	throws InvalidPropertyException {
		if(attributeName.equals("dataIncrement")){
			try{
				// try to parse the value as an integer (type should be pre-validated)
				int test=Integer.parseInt(valueString);
				// if a non-zero int, try to set the instrument value
				if(test!=0){
					setInc(test);
					// if successful update the attribute value
					dataIncrement=test;
				}
			}catch (Exception e) {
				// if we fail, throw an exception
				throw new InvalidPropertyException("set dataIncrement failed ["+e.getMessage()+"]");
			}
			throw new InvalidPropertyException("Invalid dataIncrement value ["+valueString+"]");
		}

Step 4: Build the service classes

Before building the HelloSIAM example, ensure that your SIAM environment is correctly configured (you should be able to build the SIAM source tree). This configuration is described in the SIAM Quick Start
The environment variables SIAM_HOME, SIAM_CLASSPATH, JAVA_DEV_ROOT and JAVA_HOME should be set.

We'll install the example code into the main SIAM source tree, modify the Makefiles and environment and build it there.

Step 5: Make an instrument service JAR

Instrument service JAR files are built using the mkpuck and mksiamjar utilties (described here). These utilities are used to configure the service attributes at start up, for example the device ID of the service, as well as sample schedule, and instrument-specific attributes like HelloSIAM's dataIncrement.

These utilties may be run from the command line, a shell script or a Makefile. For HelloSIAM, we'll use a Makefile; installing the HelloSIAM code should have installed $SIAM_HOME/make/Makefile.helloSIAM

You may optionally configure service attribute defaults (e.g., sample schedule, dataIncrement) by editing Makefile.helloSIAM. If you change the device ID, remember also to change the name of the service XML file.
Here is the HelloSIAM JAR configuration line in Makefile.helloSIAM:


	hello.service:
		mkpuck org.mbari.siam.devices.hello.HelloSIAM 'Hello Service' 9999 $(JAVA_DEV_ROOT)/ports/Hello-9999.jar $(JAVA_DEV_ROOT)/puckxml/9999.xml 'sampleSchedule = 30' 'registryName = Hello' 'dataIncrement=2' 'UUID = 00000000-0000-0000-0000-000000000000';

This should create the file $SIAM_HOME/ports/Hello-9999.jar

Step 6: Configure SIAM to run the service

In this example, we'll configure SIAM to load the HelloSIAM service from the SIAM host ($SIAM_HOME/ports directory)

Step 7: Configure and run the Fake-O-Tron

Before the service is run, we must connect and start the Fake-O-Tron.

HelloSIAM hardware connections

Step 8: Run the HelloSIAM service

Removing HelloSIAM

To uninstall the code for this example, run the uninstall script as follows: