Documentation:Tutorial:Devices - MdsWiki
Navigation
Personal tools

From MdsWiki

Jump to: navigation, search

Developing MDSplus Devices


Contents

Why Using MDSplus Devices

This section will cover in detail MDSplus devices, an important feature for customizing MDSplus to any target experiment.

So far, we have used MDSplus to store and retrieve data from pulse files, and to set-up automated sequences of operations to be performed during the experimental phases. If this were all the functionality of MDSplus, a lot of work would be required to build a real-world data acquisition system because every hardware device involved in the system would require the development of ad-hoc code for set-up and data retrieval. A software layer for integrating the device in the system would then be required for every kind of device used. If the same device were used somewhere else, however, it would be nice to avoid developing new stuff from scratch, by re-using existing code.

In order to ease re-using device-specific code, MDSplus provides a framework for integrating device-specific functionality into the system. Once integrated, the device support code becomes part of the MDSplus system, and can be promptly re-used elsewhere.

This section will explain in detail how to define a new device in MDSplus and how to integrate the support code for in the framework so that it can be re-used not only in the same system, but also by every other MDSplus user.

After a first introduction explaining the general concepts of MDSplus devices, we shall use a simple test case: a simulated Transient Recorder and will show in detail all the steps which are required to integrate such a device.

General Concepts

Every hardware device used in data acquisition requires some associated information. For example, a generic ADC device may require:

  • HW Address: the hardware address of the ADC device;
  • Sampling frequency: the sampling speed;
  • Post-Trigger Samples: the number of samples taken after a trigger

In addition to setup information, sampled signals will be read from the device and stored in the pulse file. In MDSplus all the information associated with a given device will be stored in a subtree of the pulse file. A different set of nodes, organized as a subtree, will be defined for every device declared in the pulse file, but the structure of such subtrees will be the same for all the devices of the same kind. Borrowing some Object Oriented terminology (we shall see that there are several similarities between objects and MDSplus devices), we define a device instance as the actual set of data items organized in a subtree and bringing all the information associated with the device, and a device type (or class), as the description of the way device specific data are organized, as well as the set of operations (methods) which are defined for that kind of device.

In order to add a device instance to a pulse file (this operation is typically carried out when we are editing the experiment model to be used afterwards for the pulse file creation), it is necessary to provide to MDSplus the following information:

  • The path name of the device subtree root;
  • The type of the device.

Based on the type information, MDSplus will then execute the constructor method of the device, which will in turn build the device-specific subtree. Therefore the minimum required development for integrating a new device type in MDSplus is the device constructor.

Other methods will carry out the specific device functionality. For example, an ADC device will likely define an init method, which will read the information associated with that device instance and will then initialize the hardware device, and a store method, to be called after the ADC has been triggered, and which will read sampled data and store them in the appropriate data items of the device subtree.

Typically, in addition to the data items describing the device configuration and the acquired data, an action node associated with each device method is defined in the device subtree. In this way, device methods will be called in the automated sequence based on the action data stored in the pulse file (see the previous section on how to configure an automated experiment sequence).

In order to call the appropriate device methods (and constructor), MDSplus uses a naming convention, in which the name of the method is appended to the name of the device type. It is possible to develop the device support methods in native C language, but in this tutorial we shall use a scripting language to develop such methods. The use of a scripting language has the great advantage of hiding the internals of MDSplus in the development of device methods, and this is the current approach in device support development.

The native scripting language of MDSplus is TDI (briefly described in the previous sections), but here we shall use Python for the implementation of the test case device. Python has been recently integrated in MDSPlus: it retains all the advantages of TDI, but provides a much more complete and rugged scripting environment. We therefore expect that Python will be used extensively in MDSplus. MDSplus now supports Python implementation of all the device methods.

In addition to the constructor and the other methods for interacting with the hardware device, a method will provide the device specific graphical forms for entering the device configuration in jTraverser. A device configuration is represented by a set of data items which are written in the experiment model and then used during data acquisition. It is possible to write configuration data using jTraverser browsing the device subtree and putting the appropriate value into each data item. However such operations would be lengthy and tedious (very often device subtrees define tens or hundreds of data items). It would be much better to let jTraverser activate a specific form which will present the device configuration in the most appropriate graphical layout, so that data can be easily entered by users during the configuration of the device.

This can be achieved by developing a Setup method, which is then called by jTraverser when requested via the Popup "Setup Device" option.

Image:DevicesTutorialSetupDevice.gif

Unlike other device methods, the Setup method is written in Java. We shall see in the test case how it is possible to develop a device graphical form without writing a single line of Java code. This is possible because MDSplus provides a set of specialized Java Beans which can be used in a Bean Builder tool to visually develop new graphical forms.

The Test Case

In this section we shall develop the required methods for a sample ADC module with the following characteristics:

  • 4 channels;
  • +/-10V input range;
  • 16 bit A/D conversion;
  • 64K samples stored for each channel

Once initialized, the ADC will start recording data cyclically until the preset number of Post Trigger Samples is recorded after the receipt of a trigger signal.

The device is simulated by a shared library provided in the MDSplus distribution (examples/demoadc) which exports two functions:

int initialize(char *name, int clockFreq, int postTriggerSamples)

where:

  • name is a string specifying some sort of hardware address;
  • clockFreq is the sampling clock frequency which can have the values 1 (1 kHz), 2 (5 kHz), 3 (10 kHz), 4 (50 kHz), 5 (100 kHz).
  • postTriggerSamples is the number of samples acquired after the occurrence of a trigger.

and

int acquire(char *name, short *chan1, short *chan2, short *chan3, short *chan4)

where the first argument is the string address of the device and the other argument are pointers to a short arrays (of 64K elements) which will contain the acquired samples. The routine will return four different sinusoidal signals sampled at the preset sampling frequency.

Even if this is a simulated device, it reflects a very common situation in which a hardware device is shipped together its software driver, typically providing a library with some functions for device configuration and data readout.

In order to develop the MDSplus device support for this device, called afterward DEMOADC, we need first to define how the associated subtree will look. A possible choice is shown below:

Image:DevicesTutorialDemoAdcStructure.gif

The meaning of the above field is summarized as follows:

  • CLOCK_FREQ contains the sampling clock frequency;
  • COMMENT contains a string comment associated with the device instance. It will be shown in the device graphical form.
  • NAME contains the string address of the device;
  • PTS defines the number of Post Trigger Samples;
  • TRIG_SOURCE defines the time of the trigger. Typically it a reference to some other data item containing the expected time of the trigger occurrence. This information is required to build the time associated with the stored samples;
  • INIT_ACTION is the action data specifying the execution of the init method for this device instance;
  • STORE_ACTION is the action data specifying the execution of the store method for this device.

In addition to configuration parameters, every channel is represented by a subtree defining two data items START_IDX and END_IDX. These fields define, for each channel, the initial and final sample referred to the sample associated with the trigger. For example START_IDX = -1000 and END_IDX = 1000 specify that the store method will store in the database a subset of the acquired 64K samples, composed of 2001 samples centered around the trigger occurrence. Finally, node data will contain, for each channel, the acquired signal.

Before describing methods in detail we need to specify where the device support methods will be put in the MDSplus directory tree. MDSplus expects that device methods will be stored in a (sub)directory of $MDS_PATH (normally pointing to subdirectory tdi in the MDSplus directory tree). In order to organize the large number of supported devices, a further directory level has been defined. For example, tdi/MitDevices will contain the device support methods for devices developed at MIT CMOD; tdi/RfxDevices will contain the device methods developed at RFX and so on. You may define your own subdirectory of TDI and put your specific device support methods there. The DEMOADC support methods are hosted in tdi/RfxDevices.

The Python Device Class

In this example, we shall implement all the device functionality (except for the graphical interface) in Python. In particular, we are going to develop a Python class named DEMOADC. A basic knowledge of the Python language is required to read this and the following sections.

Some methods of this class, such as the constructor, are required by MDSplus and other methods will describe the specific behavior of this device.

CAVEAT: please note that all device classes as well as the corresponding files must be specified in UPPERCASE.

A common Device superclass is defined in MDSplus, and all user-provided Python device classes will inherit from Device. This superclass defines the basic methods required by MDSplus, in particular the Add method, which is called when a new instance of this device needs to be created in a tree being edited.

It is only necessary to develop the device-specific methods such as init and store. However the superclass needs to know the structure of the associated subtree, so that the superclass' Add method can build the appropriate subtree. This information is specified as a list of Python dictionaries, where every dictionary element of the list specifies a subtree node. The header of DEMOADC Python class is listed below:

from MDSplus import *
class DEMOADC(Device):
    parts=[{'path': ':NAME', 'type': 'text'}, {'path': ':COMMENT', 'type': 'text'},
	{'path': ':CLOCK_FREQ', 'type': 'numeric', 'value': 10000},
   	{'path': ':TRIG_SOURCE', 'type': 'numeric', 'value': 0},
   	{'path': ':PTS', 'type': 'numeric', 'value': 1000}]
    for i in range(4):
	parts.append({'path': '.CHANNEL_%d' % (i), 'type': 'structure'})
	parts.append({'path': '.CHANNEL_%d:START_IDX' % (i), 'type': 'numeric', 'value': 0})
	parts.append({'path': '.CHANNEL_%d:END_IDX' % (i),'type':'numeric', 'value': 1000})
	parts.append({'path': '.CHANNEL_%d:DATA' % (i), 'type': 'signal', 'options'
                 :('no_write_model', 'compress_on_put')})
    parts.append({'path': ':INIT_ACTION', 'type': 'action',
	'valueExpr': "Action(Dispatch(2, 'CAMAC_SERVER', 'INIT', 50, None),
                                 Method(None, 'init', head))", 'options': ('no_write_shot',)})
    parts.append({'path': ':STORE_ACTION', 'type': 'action',
	'valueExpr': "Action(Dispatch(2, 'CAMAC_SERVER', 'STORE', 50, None), 
                                 Method(None, 'store', head))", 'options':('no_write_shot',)})

The field parts is expected to contain the definition of the subtree. You may check this code with the figure above showing the structure of the device subtree.

The mandatory dictionary items for each node are:

  • path : the path name relative to the subtree root of the node
  • type : the type (usage) of the node, which can be either 'text', 'numeric', 'signal', 'action', 'structure'

Optional dictionary items for each node are:

  • value : initial value for that node
  • valueExpr : initial value when specified as an expression
  • options : a list, or tuple, of NCI options for the tree node, such as 'no_write_model', 'no_write_shot', and/or 'compress_on_put'

The last two appended dictionaries refer to two action nodes included in the device subtree. Putting action nodes in the device subtree ensures that the actions for that device will be automatically carried out when using the dispatcher to handle the automated data acquisition sequence. Here, the first action (node :INIT_ACTION) refers to method init, and specifies that this method will be called in the phase named "INIT" at sequence number 50, and will be executed by the CAMAC_SERVER action server. The second action (node :STORE_ACTION) refers to method store, and specifies that this method will be called in the phase named "STORE" at sequence number 50, and will be executed by the CAMAC_SERVER action server.

Once the field parts has been filled, the superclass has all the required information to provide the specific constructor, so we shall concentrate now on the implementation of the init and store methods. These methods will require access to the data items of the device subtree in order to read the configuration and write acquired data. These data items are available to the code via a set of fields which are created by the superclass in initialization, based on the parts contents. Every data item is represented by a field referred by the syntax: self.<field_name>, where field_name is derived by the path of the corresponding tree node relative (as specified by the corresponding path dictionary item), where letters are lowercase and the dots and colons are replaced by underscores (except the first one). For example, tree node :NAME is accessed by field self.name and .CHANNEL_1:DATA by field self.channel_1_data. All of these fields are TreeNode instances and therefore all TreeNode methods such as data() or putData() can be used.

Once DEMOADC.py has been written and put in the appropriate tdi subdirectory (see the following sections), MDSplus becomes aware of this new device type. You can add a new device instance when editing a tree with the following TCL command:

TCL> add node <Device PATH>/model = <device type>

for example you can add a new instance of DEMOADC device whose root is name DEMO with the following command:

TCL> add node DEMO/model=DEMOADC

Alternatively, you can do the same thing with jTraverser after opening an experiment model in edit mode, via the popup command "Add Device". You need to type the device root path name and the device type in the dialog which gets activated.

As a final remark, observe that creating new device instances is an operation which does not occur frequently. In normal operation, the experiment model defines the pulse file structure, a new pulse file is created simply by copying the experiment model and in this case device constructors are not activated. They are only involved when changing the experiment model structure, e.g. for adding new components in our experiment description.

The init and store Methods

The init method will simulate the initialization of the ADC device and consists of the following steps:

  • Readout of the current configuration, i.e. of the content of the data items of the subtree associated with the device. The configuration data are normally read calling method data() in the corresponding class field. For example, the statement
    	    name = self.name.data()

will read in python variable name the data item stored in the subtree node :NAME. It is also possible to specify this in a more concise way, i.e.

     	    name = self.name

in this case method data() (self.name is a TreeNode instance) is implicitly called upon assignment.

  • Send configuration information to the hardware device. This is normally achieved by calling some routine of the device driver interface, Here, we shall call routine initialize() of our simulation DemoAdc shared library. To call native code in shared libraries from Python, we shall use the ctypes package.

Following is the Python code for method init:

    def init(self, arg):
      	from MDSplus import TreeNode
       from ctypes import CDLL, c_int, c_char_p
	try:
      	    deviceLib = CDLL("libDemoAdcShr.so")
	except:
	    print 'Cannot link to device library'
	    return 0
	try:
   	    name = self.name.data()
	except:
	    print 'Missing Name in device'
	    return 0
	clockDict = {1000: 1, 5000: 2, 10000: 3, 50000: 4, 100000: 5}
	try:
	    clockFreq = self.clock_freq.data()
	    clockMode = clockDict[clockFreq]
	except:
	    print 'Missing or invalid clock frequency'
	    return 0
	try:
	    pts = self.pts.data()
	except:
	    print 'Missing or invalid Post Trigger Samples'
	    return 0
	deviceLib.initialize(c_char_p(name), c_int(clockMode), c_int(pts))
	return 1

The first line of init method:

     	from MDSplus import TreeNode

imports class TreeNode from the MDSplus Python package. The Python MDSplus package defines a fairly large set of classes each describing a MDSplus data type or component. Here, it suffices using the TreeNode class, which describes a node in the pulse file.

Every data access operation is within a try block: if for some reason the data access fails (e.g. there are no data there) an exception is generated by MDSplus and handled in the Python code (here by issuing an error message and returning with failure status).

The init method will read the device name, clock frequency and post-trigger samples (observe that the clock frequency stored in the pulse file has to be converted to the corresponding code; this is easily achieved in Python using dictionaries), and will pass those parameters to device library routine initialize().

The second line:

       from ctypes import CDLL, c_int, c_char_p

imports the classes used by ctypes package. This package allows calling native libraries routines written in C directly in Python, without developing any additional interface, and is used here to call library routine initialize() and acquired(). We suggest using ctypes every time you need to call library routines from Python (a typical case when developing device support methods). You can find a tutorial of the ctypes package here.

Line:

      	    deviceLib = CDLL("libDemoAdcShr.so")

instantiates a CDLL object (from ctypes package) which represents the image in Python of the simulation library of the DEMOADC device (provided in MDSplus). Afterwards, library routines can be called directly from Python as methods of deviceLib.

Line

	deviceLib.initialize(c_char_p(name), c_int(clockMode), c_int(pts))

will finally call routine initialize() passing the parameters read from the pulse file. Functions c_char_p() and c_int() will convert the Python argument into the expected C type (char * and int, respectively).

Following is the listing of the store method:

    def store(self,arg):
#import required symbols from MDSSplus and ctypes packages
      	from MDSplus import Tree, TreeNode, Int16Array, Float64Array, Int32, Int64, Float32, Float64, Signal, Data, Dimension, Window, Range
      	from ctypes import CDLL, c_char_p, c_short, byref
#instantiate library object
    	try:
      	    deviceLib = CDLL("libDemoAdcShr.so")
	 except:
	    print 'Cannot link to device library'
	    return 0
#get name
	try:
   	    name = self.name.data()
	except:
	    print 'Missing Name in device'
	    return 0
#instantiate four short arrays with 65536 samples each. They will be passed to the acquire() external routine
	DataArray = c_short * 65536
	rawChan = []
	rawChan.append(DataArray())
	rawChan.append(DataArray())
	rawChan.append(DataArray())
	rawChan.append(DataArray())
	status = deviceLib.acquire(c_char_p(name), byref(rawChan[0]), byref(rawChan[1]), byref(rawChan[2]), byref(rawChan[3]))
	if status == -1:
	    print 'Acquisition Failed'
 	    return 0
#at this point the raw signals are contained in rawChan1-4. We must now:
#1) reduce the dimension of the stored array using the start idx and end idx parameters for each channel, which define
#   the number of samples around the trigger which need to be stored in the pulse file (for this purpose the value of 
#   post trigger samples is also required)
#2) build the appropriate timing information
#3) put all together in a Signal object
#4) store the Signal object in the tree
#read PostTriggerSamples
	try:
	    pts = self.pts.data()
	except:
	    print 'Missing or invalid Post Trigger Samples'
	    return 0
#for each channel we read start idx and end idx
	startIdx = []
	endIdx = []
	try :
	    for chan in range(0,4):
		currStartIdx = self.__getattr__('channel_%d_start_idx'%(chan)).data()
		currEndIdx = self.__getattr__('channel_%d_end_idx'%(chan)).data()
		startIdx.append(currStartIdx)
		endIdx.append(currEndIdx)
	except:
	    print 'Cannot read start idx or end idx'
	    return 0
#1)Build reduced arrays based on start idx and end idx for each channel
#recall that a transient recorder stores acquired data in a circular buffer and stops after acquiring 
#PTS samples after the trigger. This means that the sample corresponding to the trigger is at offset PTS samples
#before the end of the acquired sample array.
#the total number of samples returned by routine acquire()
	totSamples = 65536
#we read the time associated with the trigger. It is specified in the TRIG_SOURCE field of the device tree structure.
#it will be required in order to associate the correct time with each acquired sample
	try:
	    trigTime = self.trig_source.data()
	except:
	    print 'Missing or invalid trigger source'
	    return 0
#we need clock frequency as well
	try:
	    clockFreq = self.clock_freq.data()
	    clockPeriod = 1./clockFreq
	except:
	    print 'Missing or invalid clock frequency'
	    return 0
#the following steps are performed for each acquired channel 
	reducedRawChans = []
	for chan in range(0,4):
	    actStartIdx = totSamples - pts + startIdx[chan]  #first index of the part of interest of the sample array
	    actEndIdx = totSamples - pts  + endIdx[chan]   #last index of the part of interest of the sample array
#make sure we do not exceed original array limits
	    if actStartIdx < 0:
		actStartIdx = 0
	    if actEndIdx > totSamples:
		actEndIdx = totSamples - 1
#build reshaped array
	    reducedRawChan = rawChan[chan][actStartIdx:actEndIdx] 
 
#2)Build timing information. For this purpose we use a  MDSplus "Dimension" object which contains two fields:
# "Window" and "Axis". Window object defines the start and end index of the associated data array and the time which is
# associated with the sample at index 0. Several possible combination of start and end indexes are possible (the can also be
#negative numbers). We adopt here the following convention: consider index 0 as the index of the sample corresponding
#to the trigger, and therefore associated with the trigger time. From the way we have built the reduced raw sample array, 
#it turns out that the start idx and end idx defined
#in the Window object are the same of the start and end indexes defined in the device configuration.
#
#The "Range" object describes a (possibly multispeed or busrt) clock. Its fields specify the clock period, the start and end time 
#for that clock frequency. In our case we need to describe a continuous single speed clock, so there is no need to 
#specify start and end times(it is a continuous, single speed clock).
#
#build the Dimension object in a single call
	    dim = Dimension(Window(startIdx[chan], endIdx[chan], trigTime), Range(None, None, clockPeriod))

#3) Put all togenther in a "Signal" object. MDSplus Signal objects define three fields: samples, raw samples, dimension
#   raw samples are contained in reducedRawChan. The computation required to convert the raw 16 bit sample into a +-10V
#   value is: sample = 10.*rawSample/32768. We may compute a new float array containing such data and store it together
#   with the raw sample (in the case we would like to reain also raw data. There is however a better way to do it 
#   by storing only the required information, i.e. the raw(16 bit) samples and the definition of the expression which
#   converts raw data into actual voltage levels. Therefore, the first field of the Signal object will contain only the
#   definition of an expression, which refers to the raw samples (the second field) of the same Signal object.
#   The MDSplus syntax for this conversion is:  10.*$VALUE/32768.
#   We shall use Data method compile() to build the MDSplus internal representation of this expression, and the stick it
#   as the first field of the Signal object
  	    convExpr = Data.compile("10.* $VALUE/32768.")
#use MDSplus Int16Array object to vest the short array reducedRawChan into the appropriate MDSplus type
	    rawMdsData = Int16Array(reducedRawChan)
#every MDSplus data type can have units associated with it
	    rawMdsData.setUnits("Count")
	    convExpr.setUnits("Volt")
#build the signal object
	    signal = Signal(convExpr, rawMdsData, dim)
#write the signal in the tree 
    	    try:
		self.__getattr__('channel_%d_data'%(chan)).putData(signal)
            except:
    		print 'Cannot write Signal in the tree' 
 		return 0
#endfor chan in range(0,4):
#return success (odd numbers in MDSplus)
	return 1

The method first reads the string address of the device (required by the acquire() library routine) and builds a list of 4 native C short arrays as follows:

	DataArray = c_short * 65536
	rawChan = []
	rawChan.append(DataArray())
	rawChan.append(DataArray())
	rawChan.append(DataArray())
	rawChan.append(DataArray())

Once the DataArray object has been defined as in the first line using ctypes classes, its instantiation allocates a native C short array, which is then passed to the acquire() call as follows:

	status = deviceLib.acquire(c_char_p(name), byref(rawChan[0]), byref(rawChan[1]), byref(rawChan[2]), byref(rawChan[3]))

At this point the acquired samples have been acquired. The index of the sample corresponding to the trigger will be at offset PostTriggerSamples with respect to the last sample of the array.

We need now to restrict the acquired arrays to the desired number of samples as specified for each channel by parameters Start Idx and End Idx. To do this we need first to read the number of PostTrigger samples and then, for each channel, the actual value of Start Idx and End Idx, which are appended in a list. At this point we can compute the actual start and end index of the region of interest of the sample array, i.e.

#first index of the part of interest of the sample array
	    actStartIdx = totSamples - pts + startIdx[chan]
#last index of the part of interest of the sample array  
	    actEndIdx = totSamples - pts  + endIdx[chan]   

and reshape the array:

 	    reducedRawChan = rawChan[chan][actStartIdx:actEndIdx]

The next step is to build a time descriptor for the sample array, so that a time can be associated with every sample. For this purpose we use a MDSplus Dimension object which contains two fields: "window" and "axis". The first field is associated with Window object which defines the start and end index of the associated data array and the time which is associated with the sample at index 0. Several possible combination of start and end indexes are possible (they can also be negative numbers). We adopt here the following convention: consider index 0 as the index of the sample corresponding to the trigger, and therefore associated with the trigger time. From the way we have built the reduced raw sample array, it turns out that the start idx and end idx defined in the Window object are the same of the start and end indexes defined in the device configuration. The second Window field is the trigger time.

The axis field is represented by a Range object describing a (possibly multispeed or burst) clock. The Range fields specify the start and end time and the period for the associated sampling clock. In our case we need to describe a continuous single speed clock, so there is no need to specify start and end times (it is a continuous, single speed clock).

Trigger time is directly read from TRIG_SOURCE device field, while the clock period is derived from the frequency value read from the CLOCK_FREQ field. The dimension object, for each channel, is instantiated in a single line:

 dim = Dimension(Window(startIdx[chan], endIdx[chan], trigTime), Range(None, None, clockPeriod))

using the appropriate constructors. Every MDSplus Data object defines a constructor taking as arguments its field instances, so this is the usual way for building complex data type instances.

The final step is to put everything together in a Signal Data object. MDSplus Signal objects define three fields: samples, raw samples and dimension. The raw (16 bits) sample array is contained in variable reducedRawChan. The computation required to convert the raw 16 bit sample into a +/-10V value is:

sample = 10.*rawSample/32768

We may compute a new float array containing such data and store it together with the raw sample in the case we would like to retain also raw data. There is however a better way to do it, by storing only the required information, i.e. the raw (16 bit) samples and the definition of the expression which converts raw data into actual voltage levels. Therefore, the first field of the Signal object will contain only the definition of an expression, which refers to the raw samples (the second field) of the same Signal object.

The MDSplus syntax for this conversion is:

 10.*$VALUE/32768.

and shall use the Data method compile() to build the MDSplus internal representation of this expression, and to use it as the first field of the Signal object.

In summary, the following steps are required to build the Signal Data object (for each channel) and write it in the pulse file:

  • Define the Data object representing the conversion expression using Data method compile() (data is the common superclass for every MDSplus data object):
 convExpr = Data.compile("10.* $VALUE/32768.")
  • vest the short array reducedRawChan into the appropriate MDSplus type (Int16Array):
 rawMdsData = Int16Array(reducedRawChan)
  • set units for raw samples (counts) and voltage values (Volt), so that they can be displayed, for example, by jScope:
 rawMdsData.setUnits("counts")
 convExpr.setUnits("Volt")
  • put all together in a Signal object:
 signal = Signal(convExpr, rawMdsData, dim)
  • and finally write the signal into the pulse file
self.__getattr__('channel_%d_data'%(chan)).putData(signal)

observe that this statement is within a for loop, where variable chan contains the current channel index. For channel 1, the name of the corresponding data field would be

self.channel_1_data

using method __getattr__ allows accessing a field given whose name is passed as argument. Method putData() is called to store the computed signal into the tree. A shortcut for this operation would have been:

self.__getattr__('channel_%d_data'%(chan)) = signal

as method putData() is implicitly called in the assignment operation.

At this point we have finished writing the Python class for implementing init and store methods. The final steps are to install a Python package containing DEMOADC class so that MDSplus can activate the methods when required to do so, e.g. during an automated sequence, or manually via the TCL command:

TCL> do/method <Device root path> <method name>

Suppose the source for class DEMOADC has been put in tdi/MyDevices directory, the following two files must be put in the same directory: setup.py and __init__.py. They are required for installing the python package and a sample is listed below:

setup.py:

#!/usr/bin/env python

from setuptools import setup, Extension
version='0.2'
setup(name='MyDevices',
     version=version,
     description='RFX Python Device support',
     long_description = """
     This module provides the RFX device support classes
     """,
     author='MDSplus tutorial reader',
     author_email='his/her e-mail',
     url='http://www.mdsplus.org/',
     package_dir = {'MyDevices':'.',},
     packages = ['MyDevices',],
     platforms = ('Any',),
     classifiers = [ 'Development Status :: 4 - Beta',
     'Programming Language :: Python',
     'Intended Audience :: Science/Research',
     'Environment :: Console',
     'Topic :: Scientific/Engineering',
     ],
     keywords = ('physics','mdsplus',),
     zip_safe = False,
    )

__init__.py:

"""
MyDevices
==========
@authors: MDSplus tutorial reader
@copyright: 2008
@license: GNU GPL

"""
from DEMOADC import DEMOADC

Once files setup.py and __init__.py have been put in directory tdi/MyDevices, the following command will install the package (superuser privilege is normally required):

python setup.py install

When developing the support methods for another device type and writing their implementation in a file named <device type>.py, the only action required is to add another line to __init__.py:

from <device type> import <device type>

and to re-install the Python package.

The final step, is to add the new device to the list of devices known by MDSplus. You need to edit tdi/MdsDevices.fun and add the following name pair to the list

	'DEMOADC\0',		'MyDevices\0';

MdsDevices.fun is a TDI script which is used by MDSplus to find the Python package containing the device class.

A Few Tips for Debugging Python Devices

It is not always easy to debug Python devices, especially in the initial part, that is making sure Python is correctly invoked by MDSplus when executing device methods. Afterwards, the old-fashioned usage of print statements inside the Python code can help debugging the Python code.

First of all it is necessary to make sure that MDSplus is able to invoke Python. To test it you can run program tditest which is a simple program for testing the evaluation of MDSplus expression. In tditest you type expressions and you get the result of their evaluation. Try typing the following expression

Py("print 'hello world' ")

and make sure the message is printed.
A common cause of error is the missing or wrong definition of environment variable PyLib which helps MDSplus locate the Python shared library or DLL. On Windows systems you typically would define "PyLib" to be something like "python25". This should correspond to the name of the Python DLL installed in your main windows directory or in a subdirectory under it. On Linux systems "PyLib" should be something such as "python2.6"


Then you must make sure that no compilation errors are present in the code. If this happens, you simply get a generic error when trying to invoke device methods. Therefore it is convenient to give the following command from a Python shell:

from <Device Class> import *

in order to make sure that there are no compilation errors. Device methods can be tested from the MDSplus TCL utility using the following commands:

TCL> set tree <experiment >
TCL> do/method <device root> <method name>

For testing the constructor you need to open the tree in edit mode and then add a new instance of the device, using the following commands:

TCL> edit <experiment>
TCL> add node <path of the device root>/model=<device type>

Writing a Custom Device Graphical Form

As mentioned before, configuring a device by individually accessing its data fields can be a very tedious procedure, even when using a graphical configuration tool such as jTraverser. For this reason, the support methods for every device type typically include a Setup method which provides a graphical form for entering the configuration for that device instance. jTraverser has the ability of activating device specific graphical forms provided that:

  • the Java class for that form is reachable via CLASSPATH
  • the java class is named <Device type>Setup

Developing a device graphical form to be displayed by jTraverser would require the knowledge of the internals of MDSplus and therefore would not be easily developed by MDSplus users. It is however possible to develop such a form without writing a single line of Java code, by using a set of specialized Java Beans for MDSplus. Any Java IDE supporting Java Beans can be used. We recommend the usage of NetBeans, which is the IDE tool used in this tutorial. NetBeans is a free IDE for Java Development from Sun and can be downloaded from http://www.netbeans.org/downloads/

In the following, we shall describe step by step the actions required to let the IDE tool be aware of the MDSplus Java Beans so that they can be used for graphically assembling the device form. Afterwards, the building process of a graphical interface for DEMOADC is shown in a movie (you need Adobe Flash Player plugin in your Web browser).

We are going to develop the following form:

Image:DevicesTutorialDEMOADCSetup.gif

As you can see, this is a much more user-friendly way of inserting the device configuration!

Once you have installed the NetBeans IDE, follow the steps below for making NetBeans aware of MDSplus java beans. Those beans are contained in the jar file DeviceBeans.jar which is in directory java/classes of the MDSplus distribution.

  • Select option Tools->Palette->Swing/AWT Components
  • in the displayed Palette Manager select button New Category...
  • type MDSplus in the category name and press Ok
  • in the palette manager select Add from JAR...
  • select file DeviceBeans.jar in the java/classes directory of the MDSplus distribution
  • select all the displayed Bean names and click Next
  • select the newly created category MDSplus, click finish and then close the palette manager.

At this point NetBeans is aware of the MDSplus Java Beans, which can be used together with the other Swing and AWT beans for graphically building the device form. Once dragged into the form under construction, the MDSplus beans need to be configured. The list of the associated properties is displayed by the bean builder, but it is much easier to activate the bean specific customizer. The customizer is a graphical interface which allows to set the bean configuration. A configuration item which is required for almost all the MDSplus beans is the "Offset nid", i.e. the specification of the data item within the device subtree whose content has to be reported by the bean in the graphical interface. As mentioned before, the data items can be specified by their offset with respect of the device subtree, but it would be not easy to derive such offset values (it would be necessary to look at the way the device subtree is created by the device constructor). For this reason, the customizer will connect to a support mdsip server program in order to find out the names of the data items associated with the device, and to allow the selection of the target data item in a user-friendly way.

Below you can see the whole form construction process. A few things to be noted:

  • the logical name device_beans_path must be set to a writable directory. It will contain a temporary pulse file used by the mdsip server below;
  • an mdsip server program has to be started before starting the visual assembly of the device form;
  • the device Setup class is a subclass of DeviceSetup. Before proceeding with the visual assembly it is necessary to define at least the following properties deviceProvider and deviceType. The first one is the IP address of the mdsip server (if run locally type localhost). The second one is the device type (DEMOADC in our example).

Following is a list of the MDSplus Java Beans commonly used in the construction of a device form, with a brief description of the associated properties, displayed in the corresponding customizer interface. You will see that there are also other MDSplus java beans in the distribution: they are intended for more specialized interfaces and are therefore outside the scope of this tutorial.

DeviceField: it is the most common bean and provides visualization and (optional) editing of a single device data item as a text field. Its properties are:

  • Label : the associated label in the form;
  • Num. columns: number of columns in the displayed text field;
  • Show state: defines whether or not to display the on/off state of that data item;
  • Text only: defines whether the associated data is a text string;
  • Editable: defines whether the field can be edited in the form;
  • Offset Nid: the specification of the target data item in the device subtree
  • Display Evaluated: specifies whether the evaluated value of the field has to be displayed in case the target item contains an expression;
  • Opt identifier: intended for specialized interface, outside the scope of this tutorial.

DeviceChoice: used to provide a menu among different possible configurations. Its properties are:

  • Label : the associated label in the form;
  • Show state: defines whether or not to display the on/off state of that data item;
  • Offset Nid: the specification of the target data item in the device subtree;
  • Mode: specified whether the choice is among integer, float or string values. A further option (code) allows to store in the data item an integer code corresponding to the visualized value;
  • Items: the items displayed by the menu;
  • Opt identifier, Update identifier: intended for a specialized interface, outside the scope of this tutorial.

DeviceButtons: displays the buttons for entering the configuration, closing the form and resetting it to its initial values. No customization required.

DeviceDispatch: allows the configuration of the action data items contained in the device subtree. It is normally used to change the sequence number of that action within the automated sequence. No customization is required.

And here is a movie showing the whole process of building the graphical form for DEMOADC device.

The Final Result

Now a fully operational device has been added to the MDSplus framework. You can play now with DEMOADC by configuring it, and running its init and store methods. Remember that the store method cannot be run on the experiment model (DATA items are declared as /nowrite_model in the constructor). You can create a pulse file from the experiment model via the following TCL commands:

TCL> set tree <tree name>
TCL> create pulse <shot num>

you can then exercise the init and store methods via the following TCL commands:

TCL> set tree <tree name>/shot=<shot num>
TCL> do/method <demoadc path> init
TCL> do/method <demoadc path> store

If everything works properly, you will see in the DATA items of the device subtree four sinusoidal waveforms. Check it using jScope (Tip: to avoid writing in jScope setup data source form the whole path of the data items of DEMOADC, you can select those items with jTraverser, press <Ctrl>C and then <Ctrl>V in the Y Axis definition of the jScope setup form).

If you succeed, you are now a new MDSplus developer! Compliments!!