November 2001
TACO can be compared to other distributed object toolkits like CORBA, DCOM and OPC (on Windows) with the main differences being : (1) TACO is easy to use and understand, (2) TACO is freely available, (3) TACO is based on ONC/RPC (now part of the GNU C library), (4) TACO is multi-platform.
In TACO all control points are represented as devices. Devices are objects which belong to a control class. The class implements the control logic necessary to control the device hardware/software. Devices are served by processes called device servers. Device servers are distributed over one or any number of machines. Clients which need to accesses devices do so through a application programmer's interface. The clients can access devices synchronously, asynchronously or by events. The network layers are kept entirely hidden from the device server and client programmer's by TACO. TACO supports a database for storing persistant information and keeping track of where devices are running.
TACO is used to control an accelerator complex, experimental setups on beamlines (using synchrotron radiation and neutrons), a radio telescope and other smaller projects. It is ideal for adding Ethernet control to embedded and non-embedded devices in a research, industrial or home environment. Refer to the appendix for a list of existing device servers.
This manual is a compendium of all important TACO documents which have been written over the years by the various TACO programmers. This way there is only one single TACO manual for all important TACO documentation. The information is brought uptodate on a regular basis and should be useful to new and experienced users of TACO.
TACO can be downloaded from the TACO website1.3 and installed from the source code. TACO is made available under the GNU Public Licence (see Licence) without warranties. For news about recent developments in TACO go to the website.
This manual is organised as follows :
For more information about TACO refer to the website regularly or subscribe to taco@esrf.fr by sending an email to majordomo@esrf.fr with subscribe taco in the body of the email.
The device class implements methods and actions. The actions can be considered as special methods which can be executed by local and remote clients. They have a fixed number of input and output parameters where the parameters can be simple or complex (self-defined) types.
The DSAPI consists the following basic calls :
The data collector has accessed through an object oriented API very similar to the DSAPI.
//nethost/d/f/mWhere nethost is the name of the host where the database of the second control system is running. This concept is sometimes referred to as multi-nethost in the documentation.
TACO can also be used to distribute pure logic where no hardware is involved e.g. for doing image processing, or for sharing data between applications.
TACO has been used in the research environment (synchrotron radiation sources, reactors and telescopes) but is also being used to control robots and soon in the home to automate light switches, heaters, messaging systems etc.
The following people have written client interfaces to TACO :
TACO would not be of much use without the device servers therefore it is only fair to mention the (long and incomplete) list of device server programmers :
Here is a step by step description of the above recipe :
ftp://ftp.esrf.fr/pub/computing/cs/taco/src_release_Vx.y.tar.gzwhere x.y is the latest version of the TACO source code release (2.6 in July 2000). Download using anonymous ftp (login=anonymous, password=your email address) e.g.
cd ~/pub/cs/taco bin get src_release_Vx.y.tar.gz quit
tar -xzvf src_release_Vx.y.tar.gz
export DSHOME=`pwd` ./configure make all make install
make testwrite a device server - copy the test device server or a template and adapt it to your hardware, compile it
db_update TEST/mydevice.resstart TACO - start TACO manager and database :
etc/taco.startup
export NETHOST=`hostname` myds test&
TACO has been developed at the ESRF about 10 years ago but has only recently been started to be used by groups external to the ESRF. It is obvious that to give these external groups as much autonomy as possible they need access to the source code. To satisfy this request the TACO source code release has been prepared. It is basically a copy of the source code development tree maintained at the ESRF. In order to make a quick release not much effort has gone into changing up the directory tree structure and source code. What you have on your disk is a copy of the latest release of the Unix development tree. The main aim is to allow external users to have access to the source code and (re)compile for whatever (Unix) platform they need to. For Windows compilation look under WINDOWS.
The release is organised with a main Makefile which calls the underlying Makefiles for compiling the different packages. All the underlying Makefiles are based on the GNU Make which supports conditional statements. Before trying to compile anything you must have a version of GNU make which is accessible from your $PATH environment when you type "make". GNU make is standard with Linux. For other platforms you can find a release of GNU make in the directory "gmake" with this release. Configure, compile and install it for your platform if you don't have it.
In order to simplify compilation + installation a simple script called "configure" is povided which prompts for what platform you want to compile on. Run configure by typing "./configure" and answer the questions. Before running configure set the environment variable DSHOME It will also prompt for the TACO home directory ($DSHOME) where you plan to keep all the TACO libraries and include files. This could be anywhere. At the ESRF we normally have a user account "dserver" which we use as home directory for TACO.
If you need the TACO libraries to be compiled with additional CFLAGS (e.g.) -D_REENTRANT) for your system then it is possible to set and environment variable EXTRACFLAGS before calling configure. This will be added to CFLAGS during compilation of all libraries (DSAPI, DSXDR, DBAPI). The configure script prompts for this flag.
Once you have configured the platform you can call "make all" to make all the libraries and system processes.
Will copy the libraries and include files to $DSHOME/lib/$OS and $DSHOME/include. Some of the libraries and incldue files are copied when you do "make all" as part of the TACO boot-strapping process. Will also remake dsapi and dsapi++ because of the "make clean" rule in the makefile.
Will fill the TACO database up with some default resources, start a TACO Manager and then start a test device server (Inst_verify) and client (Inst_verify_menu).
Will remove all object files.
Will do a clean and remove all libraries. It is a good idea to do a clobber before compiling on a new platform to avoid mixing object files and/or libraries.
The TACO system has three fundamental libraries - DSAPI, DSXDR and DBAPI. These libraries are fundemental to creating any TACO server or client. The source code release contains all the source code for them and Makefiles for generating archive and shared library versions. They can be found in the following directories :
DSAPI - ./dserver/system/api/apilib/src ./dserver/classes/main/src ./dserver/classes++/device/src DSXDR - ./dserver/system/xdr/src DBAPI - ./dbase/srcThe libraries are installed in :
./lib/$OS
The corresponding include files in :
./include ./include/private
TACO requires three system process to run - the Manager, Database and Message servers. The source code release contains the source code and Makefiles to generate them. They can be found in :
MANAGER - ./dserver/system/manager/src DBSRVR - ./dbase/server/src MSGSRVR - ./dserver/system/msg/src
Once compiled they are installed in :
./system/bin/$OS
TACO supports a simple database based on the GNU DBM library. DBM is based on a single key and one file per table. Some tools are provided for analysing the contents of the database. They can be found in :
DBTOOLS - ./dbase/tools/src
Once compiled they are installed in :
./system/bin/$OS
This release assumes you have a running TACO installation and know a bit about TACO. If this is your case all you need to do is point your shared library path ($LD_LIBRARY_PATH on Linux/Solaris) to the directory where you have created the shared libraries and restart your device server/client. Alternatively you can recompile you device server/client if you are using archive libraries. The main advantage of the source code release is you will be able to modify and generate new versions of the TACO libraries at will now.
If you have never used TACO before then you better send an email to "taco@esrf.fr" for more detailed instructions. In brief you have to start setup a database, start the Manager and then start as many device server/clients as necessary. Device server/clients which know about your hardware will have to be written. An example for C++ can be found in dserver/classes++/powersupply. It consists of a superclass PowerSupply.cpp and the subclass AGPowersupply.cpp. A second example of a real device server for controlling a serial line under Linux can be found in dserver/classes++/serialline. An example for C (using the Objects In C methodology) can be found in dserver/classes/instverify.
Of course you will have some. Please report them to "taco@esrf.fr". and we will do our best to answer you and include your problem in this section in the future.
Here is a (non-exhaustive) list of problems you can encounter :
This source code release is intended only for Unix platforms. If you need the Windows port which uses Visual C++ then refer to the web page http://www.esrf.fr/computing/cs/taco/dsapiNT/readme.html where you can find a source code distribution for Windows (based on DSAPI V5.15).
C:\TACO\DBASE +---res +---clnt +---svc + +---rtdb +---win32 + +---Debug + +---Release +---include C:\TACO\DSERVER +---dev + +---classes + + +---main + + + +---src + + + +---include + +---system + + +---api + + + +---admin + + + + +---include + + + +---apilib + + + + +---include + + + + +---src + + + + +---win32 + + + + + +---Debug + + + + + +---Release + + + +---cmds_err + + + +---include + + + +---res + + + +---src + + +---xdr + + + +---include + + + +---src + + + +---win32 + + + +---Debug + + + +---Release + + +---dc + + + +---include +---include +---classes + +---powersupply + + +---ag + + + +---include + + + +---src + + + +---win32 + + + +---ps_menu + + + + +---Release + + + +---Release + + +---src + + +---include + +---TextTalker + +---Release + +---src + +---include + +---TextTalker_menu + +---Release +---lib + +---win32 + +---Debug + +---Release
libdsapi.lib libdsxdr.lib libdbapi.lib oncrpc.lib version.lib wsock32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib comctl32.liband set the library path to:
/libpath:"/taco/dserver/lib/win32/Release"Hint: Use the find facility to search for the correct location of header files and libraries if you don't succeed to compile and link the samples. And study the makefiles!
libdsapi.lib libdbapi.lib libdsxdr.lib libTTS.lib oncrpc.lib version.lib wsock32.lib kernel32.lib user32.lib gdi32.lib winspool.libcomdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.libodbc32.lib odbccp32.lib comctl32.lib /subsystem:windows /incremental:no/ /pdb:"$(OUTDIR)/TextTalkerds.pdb" /debug /machine:I386/ /out:"$(OUTDIR)/TextTalkerds.exe" /libpath:"/taco/dserver/lib/win32/Release"The server (TextTalkerds.exe) needs a slave server (TTSApp.exe) to run correctly. TTSApp.exe is a DDE server, that receives requests either interactively from it's GUI or through DDE messages. TextTalkerds.exe uses these DDE messages as interface to Microsoft's text to Speech engine. That means that you have to install Microsoft's Speech SDK and the TTSApp.exe server before you can use TextTalkerds. You find Microsoft's Speech SDK as a self-extracting archive on our ftp server called sdk30s.exe (11,799KB) and the TTSApp project called ttsapp.zip. (166KB) To start TextTalkerds.exe, you first start manually TTSApp.exe, and then TextTalkerds.exe, or you copy TTSApp.exe into your system PATH. TextTalkerds will launch TTSApp automatically if it is in the system PATH.
C:\TACO\ONCRPC +---win32 +---drivers + +---etc +---librpc + +---lib + +---Release + +---Debug +---rpcgen +---rpcinfo +---service +---test +---wintest + +---vers1 + +---Release +---bin +---include +---rpcIn the project file for Microsoft Visual C++ 6.07.3 oncrpc.lib and oncrpc.dll are found in the Release and Debug directories, respectively. All .h header files are placed in: /oncrpc/win32/include. You have to copy oncrpc.lib to /taco/dserver/lib/win32/Release to build the Release versions of the DSAPI libraries or sample applications. You also have to copy ./Release/oncrpc.dll to the Windows system directory c:/winnt/system32, before running the Release versions. The same holds for the corresponding files in ./Debug if you want to build and run the Debug versions. If you want to build the oncrpc library yourself, you have to define the preprocessor macro _X86_ on Intel platforms.
0 no debug output, like standard printf 1 level adds error messages, 2 level adds trace messages, 3 level adds more details on trace and errors, 4 level adds dumps of data. extern int giDebugLevel; // 0 is defaultThe library provides two functions to manipulate this global:
extern void SetDebugLevel(int i); extern int GetDebugLevel();The library provides a replacement function to printf that accepts as the first argument a format string compatible with the formats printf uses, followed by a variable list of arguments:
extern void cdecl DbgOut(LPSTR lpFormat, ...);Instead of calling DbgOut directly, you should use one of the following macros in your code for the corresponding debug level as stored in giDebugLevel.
#define dprintf DbgOut #define dprintf1 if (giDebugLevel >= 1) DbgOut #define dprintf2 if (giDebugLevel >= 2) DbgOut #define dprintf3 if (giDebugLevel >= 3) DbgOut #define dprintf4 if (giDebugLevel >= 4) DbgOutThere is another helpful macro defined in the header file macros.h:
#ifdef _NT : #ifdef WIN32 #define PRINTF(a) MessageBox(NULL,a,NULL,MB_OK|MB_ICONASTERISK); #endif #else /* not _NT */ #define PRINTF(a) printf(a) : #endif /* _NT */To make printf(char *format,?) compatible with Windows, you should use sprintf(buff, char* format,?) first and PRINTF(buff) afterwards instead. This provides the standard printf functionality on UNIX, but pops up a MessageBox on Windows instead.
/* Function called from 'libdsapi' for delayed startup. Useful for * Windows applications to perform startup operations when Window's * GUI has been initialized. If function pointer is NULL, no delayed * startup will take place. */ extern long (*DelayedStartup)(); /* * Function called from 'libdsapi' for clean shutdown. Useful for * Windows applications to perform shutdown operations before the Window's * process is shutdown. If function pointer is NULL, no delayed * startup will take place. */ extern void (*OnShutDown)();There is the possibility to pass some lines of text to the application's startup. This text will be displayed in the main Windows's backdrop and can be used to inform the user of the server's identity and version. Here the definition of the corresponding structure in DevServer.h:
/* an array of strings to be displayed on the main window backdrop */ typedef struct { int lines; char **text; } MainWndTextDisplay; extern MainWndTextDisplay gMWndTxtDisplay;If you want text to appear in the main window you have to place something similar like that into the startup routine:
: : /* * Here is the place to define what to put into * the main window's backdrop. */ static char* info[]= { {"TACO Server that speaks ASCII text"}, {"32 bit Version rev. 1.0 for Windows 95/98/NT, Oct 2001"}, {"ESRF, BP 220, 38043 Grenoble, France"} }; : : /* * Here is the place to assign what to put into * the main window's backdrop. */ gMWndTxtDisplay.lines= 3; gMWndTxtDisplay.text= info; : :
extern HWND ghWndMain; // the main window handle extern char* gszAppName; // the application's name extern HINSTANCE ghAppInstance; // the application's module handle
The following platforms have been ported to in the past but are not used anymore and are therefore not uptodate :
This chapter will describe the first C++ implementation of Device Servers taking as an example the AGPowerSupply class. The advantages and disadvantages of this new implementation will be discussed plus the possible future directions which sh/could be explored.
Because the DSM has proved to be successful and in order to stay backwards compatible the DSM has been kept as is and only the OIC part has been replaced. However replacing OIC by C++ has meant a new terminology and technology for implementing the individual elements of the DSM. In the C++ implementation the invidual elements of the DSM are implemented as follows :
In addition to the above basic elements the following additional points can be made about the C++ implementation of the DSM :
The following comments can be made about the present implementation :
%\include{/segfs/dserver/dev/classes++/device/include/Device.h} //static char RcsId[] = "$Header: /segfs/taco/doc/manual/cppdserver.tex,v 1.1 2000/07/24 09:42:46 goetz Exp goetz $"; //+********************************************************************** // // File: Device.h // // Project: Device Servers in C++ // // Description: public include file containing definitions and declarations // for implementing the device server Device base class in C++ // (DeviceClass). // // Author(s): Andy Goetz // // Original: February 1995 // // $Revision: 1.1 $ // // $Date: 2000/07/24 09:42:46 $ // // $Author: goetz $ // // $Log: cppdserver.tex,v $ // Revision 1.1 2000/07/24 09:42:46 goetz // Initial revision // // //+********************************************************************** #ifndef _DEVICE_H #define _DEVICE_H // Some remarks about the Device class definition // // 1 - Members class_name and dev_type should not be defined as static members // otherwise, there will be only one copy of them for the device server // process and it is not possible to correctly handle device server // with several embedded classes // Therefore, don't forget to initialize them in the object constructor // and not in the class_initialise function which is executed only once // for a class. // // 2 - The State and Status member function are declared as public. This is due // to the OS-9 C++ compiler. To reuse them in a device derived class // (by specifying a pointer to them in the command list), the OS-9 compiler // needs the function to be declared as public !! // class Device { // // private members // private : // // private virtual functions which should be defined in each new sub-class // static short class_inited; virtual long ClassInitialise( long *error ); virtual long GetResources (char *res_name, long *error) = 0; // pure virtual // // public members // public: typedef long (Device::* DeviceMemberFunction)(void*, void*, long* ); typedef struct _DeviceCommandListEntry { DevCommand cmd; DeviceMemberFunction fn; DevArgType argin_type; DevArgType argout_type; long min_access; } DeviceCommandListEntry; typedef struct _DeviceCommandListEntry *DeviceCommandList; virtual long State(void *vargin, void *vargout , long *error); virtual long Status(void *vargin, void *vargout, long *error); // // class variables // char* class_name; char dev_type[24]; char* name; Device (DevString name, long *error); ~Device (); virtual long Command ( long cmd, void *argin, long argin_type, void *argout, long argout_type, long *error); long Get_min_access_right(long,long *,long *); void Get_command_number(unsigned int *); long Command_Query(_dev_cmd_info *,long *); // // protected members - accessible only be derived classes // protected: // // the following virtual commands must exist in all new sub-classes // virtual long StateMachine( long cmd, long *error); long state; // device state long n_state; // convenience variable for storing next device state long n_commands; DeviceCommandList commands_list; }; #define TYPE_DEFAULT "DevType_Default" #define TYPE_INIT "DevType_" #endif /* _DEVICE_H */
//+===================================================================== // // Function: Device::Device() // // Description: constructor to create an object of the base class Device // // Input: char *name - name (ascii identifier) of device to create // // Output: long *error - error code returned in the case of problems // //-===================================================================== Device::Device (char *devname, long *error) { static DeviceCommandListEntry dev_cmd_list[] = { {DevState, &Device::State, D_VOID_TYPE, D_SHORT_TYPE}, {DevStatus, &Device::Status, D_VOID_TYPE, D_STRING_TYPE}, }; static long no_commands = sizeof(dev_cmd_list)/ sizeof(DeviceCommandListEntry); dev_printdebug(DBG_TRACE,"Device::Device() called, devname = %s\n",devname); *error = DS_OK; // // check if ClassInitialise() has been called // if (Device::class_inited != 1) { if (Device::ClassInitialise(error) != DS_OK) { return; } } // // initialise class_name (this should be done here because class_name // is NOT a static member of the device class for the case of device // server with several embedded classes. Also initialises, device // type // this->class_name = "DeviceClass"; sprintf(this->dev_type,TYPE_DEFAULT); // // initialise the device name // this->name = (char*)malloc(strlen(devname)+1); sprintf(this->name,"%s",devname); // // initialise the commands list // this->n_commands = no_commands; this->commands_list = dev_cmd_list; this->state = DEVON; }
long Device::Command (long cmd, void* argin, long argin_type, void* argout, long argout_type, long *error) { int i; DeviceMemberFunction member_fn; printf("Device::Command() called, cmd = %d\n",cmd); // add code to execute a command here for (i = 0; i < this->n_commands; i++) { if (cmd == this->commands_list[i].cmd) { if (argin_type != this->commands_list[i].argin_type || argout_type != this->commands_list[i].a { *error = DevErr_IncompatibleCmdArgumentTypes; return(DS_NOTOK); } // check state machine if (this->StateMachine(cmd,error) != DS_OK) { return(DS_NOTOK); } // now execute the command member_fn = this->commands_list[i].fn; if ((this->*member_fn)(argin,argout,error) != DS_OK) { return(DS_NOTOK); } else { return(DS_OK); } } } *error = DevErr_CommandNotImplemented; return(DS_NOTOK); };
The following points can be made about this implementation :
class PowerSupply : public Device { // private members private : long ClassInitialise( long *error ); long GetResources (char *res_name, long *error); // protected members protected: float set_val; float read_val; long channel; long n_ave; long fault_val; float cal_val; float conv_val; char *conv_unit; float set_offset; float read_offset; float set_u_limit; float set_l_limit; float idot_limit; long polarity; float delta_i; long time_const; long last_set_t; long CheckReadValue(DevBoolean *check, long *error); virtual long StateMachine( long cmd, long *error)=0; // pure virtual function // public members public: PowerSupply (char *name, long *error); ~PowerSupply (); };
long PowerSupply::GetResources (char *res_name, long *error) { static db_resource res_powersupply[] = { {"delta_i", D_FLOAT_TYPE}, {"time_constant", D_LONG_TYPE}, }; static unsigned int res_powersupply_size = sizeof(res_powersupply)/ sizeof(db_resource); register int ires; *error = DS_OK; // // setup the db_resource structure so that we can interrogate the database // for the two resources "delta_i" and "time_constant" which are needed // by all powersupplies to implement the read<>set check // ires = 0; res_powersupply[ires].resource_adr = &(this->delta_i); ires++; res_powersupply[ires].resource_adr = &(this->time_const); ires++; if (db_getresource(res_name, res_powersupply, res_powersupply_size, error) != DS_OK) { printf("PowerSupply::GetResources() db_getresource failed, error %d\n", *error); return(DS_NOTOK); } return(DS_OK); }
The class definition can be found in the public include file (AGPowerSupply.h). The following comments can be made on present implementation :
class AGPowerSupply : public PowerSupply { // private members private : long ClassInitialise (long *error ); long GetResources (char *res_name, long *error); // protected members protected: // commands long Off (void *argin, void *argout, long *error); long On (void *argin, void *argout, long *error); long Status (void *argin, void *argout, long *error); long SetValue (void *argin, void *argout, long *error); long ReadValue (void *argin, void *argout, long *error); long Reset (void *argin, void *argout, long *error); long Error (void *argin, void *argout, long *error); long Local (void *argin, void *argout, long *error); long Remote (void *argin, void *argout, long *error); long Update (void *argin, void *argout, long *error); long StateMachine (long cmd, long *error); // public members public: AGPowerSupply (char *name, long *error); ~AGPowerSupply (); };
long AGPowerSupply::ClassInitialise (long *error) { static AGPowerSupply *agps_template = (AGPowerSupply*)malloc(sizeof(AGPowerSu pply)); int iret=0; printf ("AGPowerSupply::ClassInitialise() called\n"); // AGPowerSupplyClass is a subclass of PowerSupplyClass class_name = (char*)malloc(strlen("AGPowerSupplyClass")+1); sprintf(class_name,"AGPowerSupplyClass"); class_inited = 1; // initialise the template powersupply so that DevMethodCreate has // default values for creating a powersupply, these values will be // overridden by the static database (if defined there). // default is to start with powersupply switched OFF; the state // variable gets (ab)used during initialisation to interpret the // initial state of the powersupply: 0==DEVOFF, 1==DEVON. this is // because the database doesn't support the normal state variables // like DEVON, DEVSTANDBY, DEVINSERTED, etc. agps_template->state = 0; agps_template->n_state = agps_template->state; agps_template->set_val = 0.0; agps_template->read_val = 0.0; agps_template->channel = 1; agps_template->n_ave = 1; agps_template->conv_unit = (char*)malloc(sizeof("AMP")+1); sprintf(agps_template->conv_unit,"AMP"); agps_template->set_offset = 0.0; agps_template->read_offset = 0.0; agps_template->set_u_limit = AG_MAX_CUR; agps_template->set_l_limit = AG_MIN_CUR; agps_template->polarity = 1.0; // interrogate the static database for default values if(GetResources("CLASS/AGPS/DEFAULT",error)) { printf("AGPowerSupply::ClassInitialise(): GetResources() failed, error %d\ n",error); return(DS_NOTOK); } agps_template->state = state; agps_template->set_val = set_val; agps_template->read_val = read_val; agps_template->channel = channel; agps_template->n_ave = n_ave; agps_template->conv_unit = (char*)malloc(sizeof(conv_unit)+1); sprintf(agps_template->conv_unit,conv_unit); agps_template->set_offset = set_offset; agps_template->read_offset = read_offset; agps_template->set_u_limit = set_u_limit; agps_template->set_l_limit = set_l_limit; agps_template->polarity = polarity; printf("returning from AGPowerSupply::ClassInitialise()\n"); return(iret); }
static Device::DeviceCommandListEntry commands_list[] = { {DevState, (DeviceMemberFunction)&Device::State, D_VOID_TYPE, D_SHORT_TYPE}, {DevStatus, (DeviceMemberFunction)&Device::Status, D_VOID_TYPE, D_STRING_TYPE},
long AGPowerSupply::Off (void *vargin, void *vargout,long *error) { printf("AGPowerSupply::Off(%s) called\n",name); *error = DS_OK; read_val = 0.0; set_val = 0.0; state = DEVOFF; return (DS_OK); }
long AGPowerSupply::Update ( void *vargin, void *vargout, long *error) { DevStateFloatReadPoint *vargout_sfrp; DevShort darg_short; DevFloatReadPoint darg_frp; printf("AGPowerSupply::Update(%s) called\n",name); vargout_sfrp = (DevStateFloatReadPoint*)vargout; // update state State(NULL, &darg_short, error); vargout_sfrp->state = darg_short; // get latest set and read ReadValue(NULL, &darg_frp, error); vargout_sfrp->set = darg_frp.set; vargout_sfrp->read = darg_frp.read; return(DS_OK); }
#include <API.h> #include <Device.h> #include <DevServer.h> #include <PowerSupply.h> #include <AGPowerSupply.h> #define MAX_DEVICES 1000 extern "C" long startup(char *svr_name, long *error); unsigned int n_devices, i; Device *device[MAX_DEVICES]; long startup(char *svr_name, long *error) { char **dev_list; short state; long status; printf ("startup++() program to test dserver++ (server name = %s)\n",svr_name); // get the list of device name to be served from the static database if (db_getdevlist(svr_name,&dev_list,&n_devices,error)) { printf("startup(): db_getdevlist() failed, error %d\n",*error); return(-1); } printf("following devices found in static database: \n\n"); for (i=0;i<n_devices;i++) { printf("\t%s\n",dev_list[i]); } // now loop round creating and exporting the devices for (i=0; i<n_devices; i++) { device[i] = new AGPowerSupply(dev_list[i],error); if ((device[i] == 0) || (*error != 0)) { printf("Error when trying to create %s device\n",dev_list[i]); return(DS_NOTOK); } else { // test calling Device::State via Device::Command method device[i]->Command(DevState, NULL, D_VOID_TYPE, (void*)&state, D_SHORT_TYPE, error); // export the device onto the network status = dev_export((char*)device[i]->name,(Device*)device[i],(long*)error); printf("startup++() dev_export() returned %d (error = %d)\n",status,*error); } } return(DS_OK); }
Two possibilities of including OIC classes in C++ considered were :
The first method (C++ calls OIC C directly) poses the problem of what happens when the programmer wants to export a mixture of C++ and C objects onto the network ? The device server main() routine assumes can manage a list of either all OIC DevServer's or all C++ Device's but not both. It was decided therefore to use the second method (C++ wrapper class) and write a class called OICDevice.
OICDevice is a C++ wrapper class for OIC classes. OICDevice is a generic class for creating objects of any OIC class, it is derived from the Device root class. The result is a C++ OICDevice object which has a pointer to the actual OIC object. Seen from the C++ programmer's point of view it appears as a C++ object. It has the same interface as all other C++ objects dervied from Device. Executing commands on the object will result in the OIC command method handler being called.
Some points to be aware of when wrapping your OIC objects with OICDevice :
Note the OICDevice class is only a wrapper class for encapsulating OIC objects and not classes. Because of the differences between the OIC and C++ implementations it is not possible to derive new C++ classes from existing OIC classes as sub-classes. It is however possible to instantiate OIC classes in C++. If you want to use an existing OIC class as a super-class for C++ then you have to rewrite the OIC class in C++.
//static char RcsId[] = "$Header: /segfs/taco/doc/manual/cppdserver.tex,v 1.1 2000/07/24 09:42:46 goetz Exp goetz $"; //+********************************************************************** // // File: OICDevice.h // // Project: Device Servers in C++ // // Description: public include file containing definitions and declarations // for implementing OICDevice class in C++. The OICDevice class // wraps (old) OIC classes in C++ so that they can be used // in C++ classes derived from the Device base class. // // Author(s): Andy Goetz // // Original: November 1996 // // $Revision: 1.1 $ // // $Date: 2000/07/24 09:42:46 $ // // $Author: goetz $ // // $Log: cppdserver.tex,v $ // Revision 1.1 2000/07/24 09:42:46 goetz // Initial revision // // // //+********************************************************************** #ifndef _OICDEVICE_H #define _OICDEVICE_H class OICDevice : public Device { // // private members // private : // // private virtual functions which should be defined in each new sub-class // static short class_inited; long ClassInitialise( long *error ); // // not many OIC classes have this method // long GetResources (char *res_name, long *error); // // public members // public: long State(void *vargin, void *vargout , long *error); long Status(void *vargin, void *vargout, long *error); // // class variables // OICDevice (DevString devname, DevServerClass devclass, long *error); ~OICDevice (); long Command ( long cmd, void *argin, long argin_type, void *argout, long argout_type, long *error); inline short get_state(void) {return(this->ds->devserver.state);} inline DevServer get_ds(void) {return(this->ds);} inline DevServerClass get_ds_class(void) {return(this->ds_class);} // // protected members - accessible only from derived classes // protected: long StateMachine( long cmd, long *error); // // OICDevice member fields // DevServer ds; // pointer to the old OIC object DevServerClass ds_class; // pointer to the old OIC class }; #endif /* _OICDEVICE_H */
static char RcsId[] = "$Header: /segfs/taco/doc/manual/cppdserver.tex,v 1.1 2000/07/24 09:42:46 goetz Exp goetz $"; //+********************************************************************** // // File: startup.cpp // // Project: Device Servers in C++ // // Description: startup source code file for testing the OIC AGPowerSupply class // in C++. AGPowerSupply class implements a simulated powersupply // derived from the base classes PowerSupply and Device (root // class). // // // Author(s): Andy Goetz // // Original: November 1997 // // $Revision: 1.1 $ // // $Date: 2000/07/24 09:42:46 $ // // $Author: goetz $ // // $Log: cppdserver.tex,v $ // Revision 1.1 2000/07/24 09:42:46 goetz // Initial revision // // //+********************************************************************** #include <iostream.h> #include <API.h> #include <Device.H> #include <DevServer.h> #include <DevServerP.h> #include <OICDevice.H> #include <PowerSupply.h> #include <PowerSupplyP.h> #include <AGPowerSupply.h> #include <AGPowerSupplyP.h> #define MAX_DEVICES 1000 long startup(char *svr_name, long *error) { char **dev_list; unsigned int n_devices, i; OICDevice *device[MAX_DEVICES]; short state; long status; printf ("startup++() program to test dserver++ (server name = %s)\n",svr_name); // // get the list of device name to be served from the static database // if (db_getdevlist(svr_name,&dev_list,&n_devices,error)) { printf("startup(): db_getdevlist() failed, error %d\n",*error); return(-1); } printf("following devices found in static database: \n\n"); for (i=0;i<n_devices;i++) { printf("\t%s\n",dev_list[i]); } // // now loop round creating and exporting the devices // for (i=0; i<n_devices; i++) { // // DO NOT create AGPowerSupply (C++) objects // // device[i] = new AGPowerSupply(dev_list[i],error); // // // create old (OIC) AGPowerSupply objects // device[i] = new OICDevice(dev_list[i],(DevServerClass)aGPowerSupplyClass,error); // // test calling Device::State via Device::Command method // device[i]->Command(DevState, NULL, D_VOID_TYPE, (void*)&state, D_SHORT_TYPE, error); // // export the device onto the network // status = dev_export((char*)device[i]->name,(Device*)device[i],(long*)error); printf("startup++() dev_export() returned %d (error = %d)\n",status,*error); } return(DS_OK); }
To implement device servers in C++ the following modifications were made:
#ifndef __cplusplus /* * OIC version */ client_data.status = (ds__method_finder (ds, DevMethodCommandHandler)) ( ds, server_data->cmd, server_data->argin, server_data->argin_type, client_data.argout, client_data.argout_type, &client_data.error); #else /* * C++ version */ client_data.status = device->Command(server_data->cmd, (void*)server_data->argin, server_data->argin_type, (void*)client_data.argout, client_data.argout_type, &client_data.error); #endif /* __cplusplus */
#ifndef __cplusplus DevServer ds; ds = (DevServer) ptr_ds; #else Device *device; device = (Device*) ptr_ds; #endif /* __cplusplus */
extern "C" long dev_export PT_((char* dev_name, Device *ptr_dev, long *error))
The first C++ implementation was done in 1995 (by AG) using the HP CC compiler on the HP 9000/700 series. This compiler is a 2.x C++ compiler and supports symbolic debugging. When compiling the following symbols have to be defined __STDC__, unix, and _HPUX_SOURCE.
In 1996 this work was repeated (by ET) for the Kicker Powersupply at the ESRF using the Ultra-C++ compiler from Microware and the GNU g++ compiler on HP-UX.
For the future we propose that wherever possible the GNU g++ compiler must be used. Where it is not possible the best adapted native compiler should be used.
This is clearly the case for OS9 where the native Ultra-C++ compiler from Microware is the obvious choice. This is not so clear for HP-UX - the GNU g++ compiler does not support exceptions but is otherwise a good choice. For the present g++ is supported under HPUX (i.e. the C++ libraries are compiled only with the g++ compiler9.2).
The templates can be found in libra:/users/d/dserver/classes++/template :
Device servers have been developed at the ESRF in order to solve the main task of the Control System viz. provide read and write access to all devices in a distributed system. The problem of distributed device access is only part of the problem however. The other part of the problem is providing a programming framework for a large number of devices programmed by a large number of programmers each having different levels of experience and style.
Device servers have been written at the ESRF for a large variety of different devices. Devices vary from serial line devices to devices interfaced by field-bus to memory mapped VME cards to entire VME/VXI data acquisition systems. The definition of a device depends very much on the user's requirements. In the simple case a device server can be used to hide the serial line protocol required to communicate with a device. For more complicated devices the device server can be used to hide the entire complexity of the device timing, configuration and acquisition cycle behind a set of high level commands.
A model (referred to as the Device Server Model or DSM) has been developed to satisfy the main two requirements. In order to do this the DSM has a number of parts to it. It defines the concept of a generic device which is created and managed in a server - a device server. The device is accessed by an application programmers interface (api) which is network transparent. Device specific details get treated in the device servers thereby freeing applications to do application-oriented work. Multiple access is implemented by queuing requests - the queuing is handled automatically by the network software.
In this manual the process of how to write device servers will be treated. The manual has been organised as follows - chapter 2 presents an historical account of device servers. The device server model (DSM) is treated in chapter 3. This is followed by a chapter on Objects in C (the Object Oriented Programming methodology used to implement the device servers). Chapter 5 describes how to write a device server. Chapter 6 is devoted to techniques in using classes. Chapter 7 is reserved for Frequently Asked Questions. Finally there is a discussion of limitations in the present device server model and what improvements are planned.
Throughout this manual examples of source code will be given in order to illustrate what is meant. The examples have been taken from the AGPowerSupplyClass - a simulation of a powersupply which illustrates how a typical device server for a powersupply at the ESRF functions. The simulation runs under OS9 and Unix operating systems and requires no hardware in order to run.
A.Götz (the author) took over the original server in the late Spring of 1990. The first goals were to replace the Berkeley sockets with the CERN NC/RPC interface, to write servers for real devices, and to setup a team of programmers who would write the servers. This was just the time that X11 and MIT Widgets started appearing on commercial platforms. The Widget model (implemented by MIT's Intrinsics Toolkit) struck the author as being very appealing. It is easy to use, very powerful and manages to hide the complexity of the implementation from the user. It also demonstrated how Classes and Objects can be implemented in C.
Armed thus with the original powersupply api and the Widget model from MIT work begun (mid-1990) in earnest on the device server concept. Assistance was provided by R.Wilcke (who ported the CERN NC/RPC software to OS9) and H.Witsch (who acted as the first guinea-pig device server programmer). The first device server implemented the same functionality as the WDKPowerSupply. The server ran on OS9 and the client on HPUX.
Today (almost three years later) more than 500 device servers exist for the ESRF's Machine and Beamline Control Systems and for Data Acquisition Systems. They run on a range of Operating Systems i.e. OS9, HPUX and SunOS. There are approximately 16 programmers involved in writing device servers. The CERN NC/RPC has been replaced by the SUN NFS/RPC thanks to J.Meyer. A resource database has been added which is accessible via a standard set of rpc calls developed by E.Taurel. Device servers are implemented using classes and clients access devices via a standardised api. If the powersupply server process is considered as the first prototype and the NC/RPC based device servers as the first generation, then it would be true to say that device servers are now well into their second generation.
The term ``device server'' first appeared in an internal ESRF document by W.D.Klotz and S.M.Keogh in June 1989. It reappeared in a paper written for a GULAP (Group for Upper Level Applications Programming) Meeting in January 1990 and has been a common word in the ESRF daily vocabulary ever since.
Commands are executed across the network using the application
programmers interface function dev_putget().
dev_putget() calls a special method implemented in the
root class - the command_handler method.
The command_handler
calls the state_handler method implemented
in the device class before calling the command itself.
The state_handler implements the state machine for all devices
belonging to that device class.
The state machine checks to see wether the command to be executed is
compatible with the present state.
The command function is only executed if the state handler returns DS_OK.
The control flow at command execution is represented in figure .
The three fundamental API calls are -
dev_import (name,ds_handle,access,error) char *name; devserver *ds_handle; long access; long *error;
dev_putget (ds_handle,cmd,argin_ptr,in_type,argout_ptr,out_type,error) devserver ds_handle; short cmd; DevArgument *argin_ptr; DevType in_type; DevArgument *argout_ptr; DevType out_type; long *error;
dev_free (ds_handle,error) devserver ds_handle; long *error;
Using these three calls clients can execute all commands implemented in the device class on an imported device. All calls are synchronous calls. This means that the clients waits for the call to complete before continuing. If the device server does not respond or the remote machine is down a timeout will occur. If the client continues to try executing dev_putget() calls and in the meantime the device server is running again the device will be automatically reimported. This last feature assumes that the device has been imported correctly before the connection was lost.
Although the advantages of OOP are obvious the choice of an OOP language is not always so obvious. Any choice made had to be compatible with the operating systems SunOS, HP-UX and OS9 (the operating systems being used presently at the ESRF). The lowest common denominator in this list is OS9. Operating system compatibility means compatibility with the OS9 C language compiler from Microware (the authors of OS9). This reduces the choice to a C-like OOP language (e.g. C++ or Objective C) or developing an OOP programming technique in C. Seeing as at the time the choice for an OOP language was made (1990) , none of the C-like OOP languages available on OS9 were compatible with the Microware C compiler the only solution left was to use OOP programming technique in C.
OOP programming techniques are numerous. This is partly due to the fact that C lends itself to OOP by its ability to support new types via the typedef construct. OOP techniques in C are 90% discipline and 10% implementation. The technique which has been developed for the DSM is called Objects in C or OIC. OIC is based on the MIT Widget programming model. This chapter will describe OIC and how to program in it. No prior knowledge is assumed about Widget programming. The reader is assumed to be conversant in C however.
The principal idea behind MIT's Widgets is to treat graphical interaction objects (e.g. a push button or a scrollbar) as separate objects. Each object is represented as a variable of a certain type. The code and data necessary to implement each graphical object are hidden from the user. The user has a set of functions for interacting with the object i.e. reading or setting any of its resources. Widgets are objects which can be created and destroyed. Every Widget belongs to a class. All Widgets are derived from the same root class - the CoreClass.
The advantage of this method is that all the common code (and data) which every Widget has to have (e.g. creating an X11 window and storing it's id) are provided in the root class. For every new Widget written only the code which is new to this widget has to be written (and maintained).
In order for this to work it is necessary to be able to pass code automatically from one class (e.g. the root class) to other classes, this process is called inheritance. An elegant and natural way of doing this with classes is to implement sub-classes. By declaring a new class to be a sub-class of another class, code and data can be automatically inherited. Widgets implement classes in C using structures.
Instead of reinventing the wheel therefore it was decided to use the MIT Widget model as much as possible and only write/modify those parts which either did not exist or were not suited to the device server model.
Amongst those items which were adopted are (1) the Widget naming convention, (2) the organisation of the private include files, public include files and source code files and (3) the implementation of classes by structures. Amongst those things which were added are (1) a method finder which supports inheritance of methods by subclasses from superclasses, (2) a network manager and (3) a database accessible over the network. The remote database replaces the X11 resource database which is implemented in the X Server. The database is accessible over the network via a database server. The Widget root class (CoreClass) has been replaced by a new root class (the DevServerClass). DevServerClass has been designed to deal with the network and its resources instead of graphics. It implements (a) the remote procedure calls for the network access, (b) creates a connection to the static database (so the resources can be accessed), (c) keeps a list of exported devices (so that network clients can import devices). It also implements a number of standard methods required for the DSM (e.g. DevMethodCommandHandler, DevMethodExport, DevMethodDestroy).
The following guidelines should be followed when writing device servers :
/*static char RcsId[] = " $Header: AGPowerSupplyP.h.tex,v 1.1 93/04/05 18:16:00 goetz Exp $ ";*/ /********************************************************************* File: AGPowerSupplyP.c Project: Device Servers Description: private include file for the class of AG simulated powersupplies. Author(s); Andy Goetz Original: March 1991 $Log: AGPowerSupplyP.h.tex,v $ Revision 1.1 93/04/05 18:16:00 18:16:00 goetz (Andy Goetz) Initial revision Copyright (c) 1991 by European Synchrotron Radiation Facility, Grenoble, France *********************************************************************/ #ifndef _AGPOWERSUPPLYP_h #define _AGPOWERSUPPLYP_h /* * as subclass of the powerSupplyClass include PowerSupplyClass private * definitions */ #include <PowerSupplyP.h> typedef struct _AGPowerSupplyClassPart { int nada; } AGPowerSupplyClassPart; typedef struct _AGPowerSupplyPart { int nada; } AGPowerSupplyPart; typedef struct _AGPowerSupplyClassRec { DevServerClassPart devserver_class; PowerSupplyClassPart powersupply_class; AGPowerSupplyClassPart agpowersupply_class; } AGPowerSupplyClassRec; extern AGPowerSupplyClassRec aGPowerSupplyClassRec; typedef struct _AGPowerSupplyRec { DevServerPart devserver; PowerSupplyPart powersupply; AGPowerSupplyPart agpowersupply; } AGPowerSupplyRec; /* * private constants to be used in the AGPowerSupplyClass */ #define AG_MAX_CUR 100.0 #define AG_MIN_CUR 0.0 #define AG_PER_ERROR 0.001 /* fault values */ #define AG_OVERTEMP 0x01 #define AG_NO_WATER 0x02 #define AG_CROWBAR 0x04 #define AG_RIPPLE 0x08 #define AG_MAINS 0x10 #define AG_LOAD 0x20 #define AG_TRANSFORMER 0x40 #define AG_THYRISTOR 0x80 #endif _AGPOWERSUPPLYP_h
/*static char RcsId[] = " $Header: AGPowerSupply.h.tex,v 1.1 93/04/05 18:15:57 goetz Exp $ ";*/ /********************************************************************* File: AGPowerSupply.h Project: Device Servers Description: public include file for implementing the class of AG simulated powersupplies. Author(s); Andy Goetz Original: March 1991 $Log: AGPowerSupply.h.tex,v $ Revision 1.1 93/04/05 18:15:57 18:15:57 goetz (Andy Goetz) Initial revision Copyright (c) 1991 by European Synchrotron Radiation Facility, Grenoble, France *********************************************************************/ #ifndef _AGPowerSupply_h #define _AGPowerSupply_h typedef struct _AGPowerSupplyClassRec *AGPowerSupplyClass; typedef struct _AGPowerSupplyRec *AGPowerSupply; extern AGPowerSupplyClass aGPowerSupplyClass; extern AGPowerSupply aGPowerSupply; /* * public symbols */ #endif
The source code can be divided into two parts (a) the code to implement the device class (e.g. AGPowerSupply.c), and (b) the code to implement the startup procedure. The startup is only required when a server process is being customised. This will be treated in chapter 5.
The source file implementing the device class normally contains the entire code for implementing the device class. The class implementation is private and is meant to be accessed only via the class structure i.e. via its methods. For this reason all functions appearing in this file, especially the class methods and all device server commands are declared as static in C. The source file initialises the class structure defined in the Private include file. The method_list and n_methods variables are initialised by static assignments before load time. All other initialisation is done at runtime - this is more flexible and makes the code upwards compatible.
Here is an example of the header and all related declarations for the AGPowerSupplyClass .c file -
static char RcsId[] = "@(#) $Header: class_header.c.tex,v 1.1 93/04/05 18:16:11 goetz Exp $ "; /********************************************************************* File: AGPowerSupply.c Project: Device Servers Description: Code for implementing the AG Power Supply class The AG Power Supply is a simulation of a typical power supply at the ESRF. This means it has two main state DEVON and DEVOFF, DEVSTANDBY is unknown. All the common power supply commands are implemented. The simulation runs under OS9 and Unix. It has been developed for application program developers who want to test their applications without accessing real devices Author(s); A. Goetz Original: March 1991 $Log: class_header.c.tex,v $ Revision 1.1 93/04/05 18:16:11 18:16:11 goetz (Andy Goetz) Initial revision * Revision 1.1 91/05/02 08:25:31 08:25:31 goetz (Andy Goetz) * Initial revision * Copyright (c) 1991 by European Synchrotron Radiation Facility, Grenoble, France *********************************************************************/ #include <API.h> #include <DevServer.h> #include <DevErrors.h> #include <DevServerP.h> #include <PowerSupply.h> #include <AGPowerSupplyP.h> #include <AGPowerSupply.h> /* * public methods */ static long class_initialise(); static long object_create(); static long object_initialise(); static long state_handler(); static DevMethodListEntry methods_list[] = { {DevMethodClassInitialise, class_initialise}, {DevMethodCreate, object_create}, {DevMethodInitialise, object_initialise}, {DevMethodStateHandler, state_handler}, }; AGPowerSupplyClassRec aGPowerSupplyClassRec = { /* n_methods */ sizeof(methods_list)/sizeof(DevMethodListEntry), /* methods_list */ methods_list, }; AGPowerSupplyClass aGPowerSupplyClass = (AGPowerSupplyClass)&aGPowerSupplyClassRec; /* * public commands */ static long dev_off(); static long dev_on(); static long dev_state(); static long dev_setvalue(); static long dev_readvalue(); static long dev_reset(); static long dev_error(); static long dev_local(); static long dev_remote(); static long dev_status(); static long dev_update(); static DevCommandListEntry commands_list[] = { {DevOff, dev_off, D_VOID_TYPE, D_VOID_TYPE}, {DevOn, dev_on, D_VOID_TYPE, D_VOID_TYPE}, {DevState, dev_state, D_VOID_TYPE, D_SHORT_TYPE}, {DevSetValue, dev_setvalue, D_FLOAT_TYPE, D_VOID_TYPE}, {DevReadValue, dev_readvalue, D_VOID_TYPE, D_FLOAT_READPOINT}, {DevReset, dev_reset, D_VOID_TYPE, D_VOID_TYPE}, {DevStatus, dev_status, D_VOID_TYPE, D_STRING_TYPE}, {DevError, dev_error, D_VOID_TYPE, D_VOID_TYPE}, {DevLocal, dev_local, D_VOID_TYPE, D_VOID_TYPE}, {DevRemote, dev_remote, D_VOID_TYPE, D_VOID_TYPE}, {DevUpdate, dev_update, D_VOID_TYPE, D_STATE_FLOAT_READPOINT}, }; static long n_commands = sizeof(commands_list)/sizeof(DevCommandListEntry); /* * a template copy of the default powersupply that normally gets created * by the DevMethodCreate. it is initialised in DevMethodCLassInitialise * to default values. these defaults can also be specified in the resource * file or via an admin command. */ static AGPowerSupplyRec aGPowerSupplyRec; static AGPowerSupply aGPowerSupply = (AGPowerSupply)&aGPowerSupplyRec; /* * template resource table used to access the static database */ db_resource res_table[] = { {"state",D_LONG_TYPE}, {"set_val",D_FLOAT_TYPE}, {"channel",D_SHORT_TYPE}, {"n_ave",D_SHORT_TYPE}, {"conv_unit",D_STRING_TYPE}, {"set_offset",D_FLOAT_TYPE}, {"read_offset",D_FLOAT_TYPE}, {"set_u_limit",D_FLOAT_TYPE}, {"set_l_limit",D_FLOAT_TYPE}, {"polarity",D_SHORT_TYPE}, }; int res_tab_size = sizeof(res_table)/sizeof(db_resource);
In OIC each device class is represented by a C structure. Understanding this structure is vital to understanding OIC. This section will describe the various components of the class structure. The next section will describe how they should be initialised.
Each class structure is made up of a number of fields (cf. figure
).
Each of these fields is in itself a structure, called a partial
structure.
Each device class defines (in the private include file) its own partial
structure.
The partial structure contains all data
which are common to all members of that class.
A class hierarchy is defined by the hierarchy of partial structures. For example if a class Z contains the partial structures X, Y and Z (in that order) then one knows that it belongs to the root class X, is a member of the subclass Y and is itself the class Z. Because all device classes are members of the root class DevServerClass, the first partial structure of any device class must be the DevServerClass partial structure, DevServerClassPart.
The DevServerClassPart plays a very special role in the implementation of OIC. It defines the fields necessary for implementing and inheriting methods. This is a fundamental part of OIC because the Objects In C method finder depends completely on the first partial structure of every class structure being of type DevServerClassPart.
The fact that the DevServerClass has a dual purpose i.e. implementing methods in OIC and device access can be confusing. The reasons for this are (as usual) historical. These two functions could have been implemented separately10.1. In OIC this has not been done and device server programmers have to be aware of this. The implications of this are that today only one root class exists - the DevServerClass, and that OIC is used only to implement device servers.
In the same way that the first partial structure of any device class has to be the root class (DevServerClass) so the device classes own partial structure should be the last partial structure. All partial structures in between should be in hierarchical order of the superclasses of the class.
A copy of each device classes C structure is created (space is reserved and it is initialised) once in every program where the device class is used. The structure has the same name as the device classes structure type except that the first character is a small letter. For example the class AGPowerSupplyClass has the device class structure type AGPowerSupplyClassRec whereas the copy of the device class structure is called aGPowerSupplyClassRec.
For each device class structure there is a corresponding device class. The device class is a pointer to the copy of the class structure10.2. The same convention is followed for the device class as for device class structure. For example for the device class type AGPowerSupplyClass the actual device class (which must begin with a small letter) is aGPowerSupplyClass. A program which wants to instantiate a device of a certain class or wants to use a device class as one of its superclasses uses the pointer to the copy (the one which starts with a small letter) of the device class (aGPowerSupplyClass in this example). The device class pointer is defined as external in the public include file and defined and initialised in the .c source code file which implements the device class. Referring to the class pointer in a program forces the loader to link the object code for the class being referred to with the program. This simple but efficient mechanism allows classes to be linked with a program without referring to any of the class source code.
![]() |
Each device class is a subclass of DevServerClass (the root class). This means that the first structure within a device class structure is the partial part of the DevServerClass i.e. DevServerClassPart. DevServerClassPart structure contains :
typedef struct _DevServerClassPart { int n_methods; /*number of methods*/ DevMethodList methods_list; /*pointer to list of methods*/ DevServerClass superclass; /*pointer to superclass*/ DevString class_name; /*name of class*/ DevBoolean class_inited; /*flag indicating if class initialised*/ int n_commands; /*number of commands*/ DevCommandList commands_list; /*pointer to list of commands*/ DevString server_name; /*server name*/ DevString host_name; /*host name*/ long prog_number; /*NFS/RPC program number of server*/ long vers_number; /*NFS/RPC version number of server*/ } DevServerClassPart;
All device classes have their own copy of this structure pointed to by the class pointer e.g. aGPowerSupplyClass. This is necessary so that each class can have its own list of implemented methods, its own superclass, its own class name, its own class_inited flag and its own commands list. The server name, host name, program number and version number are stored only once - in the DevServerClassPart of the DevServer class.
The n_methods and methods_list are crucial for the implementing of classes. The method_finder (cf. below) uses these two fields to locate the method which will be executed. In order not to be tied down by the definition of the DevServerClassPart structure it was decided very early on in the development of the device servers that these two fields will be the only ones which are initialised at compile time i.e. in static data area. The other fields will be initialised in the class_initialise method by assignment statements. This makes existing code upwards compatible even if the DevServerClassPart structure is reorganised or other fields added to it in the future. The fields n_methods and methods_list have to be initialised with the number of methods and the list of methods in the .c file before any code is executed i.e. at compile and load time.
The following fields of DevServerClassPart are initialised in the class_initialise method -
After initialising the DevServerClassPart the class should initialise its own partial part. Taking the same example as used above - this means initialising AGPowerSupplyClassPart (of the structure pointed to by aGPowerSupplyClass).
Each device has its own copy of the device structure. This means each device has its own copy of the all data stored in the device structure. Programmers should be aware of this when defining the device structure. Any data which is common to all devices should be stored in the device's class structure. Devices only contain data not code. All code implemented in a class is common to all devices.
The first part of each device structure is the DevServerPart structure. DevServerPart contains the following fields :
typedef struct _DevServerPart { char *name; /*name of device*/ char dev_type[24]; /*pointer to string containing device type*/ DevServerClass class_pointer; /*pointer to class type*/ long state; /*device state*/ long n_state; /*next device state*/ } DevServerPart;
All these values (except the n_state variable) have to be initialised at device creation time. The most important is the class_pointer variable which is used by the method finder to locate the device class structure.
Each class has a template copy of a device which is initialised at runtime by the class_initialise method. It is used for initialising devices of this class. Analogous to the class pointer, aGPowerSupplyClass, the template device is called aGPowerSupply. It is defined and should only be accessible from the classes source code. It should be initialised in the class_initialise() to the predefined code defaults which can be overridden by the class defaults stored in the static database. This means the class_initialise() should access the database after it has initialised the default object. This object will be used to initialise all newly created objects of that class. In C this is achieved by a single structure assignment statement. The defaults in the template object can be overridden by the devices defaults stored in the database.
Methods, like traditional C functions, do not have a fixed argument syntax. A function implementing a method may use any argument syntax. The application executing the method should know what arguments are required, their type and in what order to pass them.
All methods should return an integer value which reflects the execution status of the method. For the return value the following convention has been adopted - DS_NOTOK is returned if the method fails to execute correctly, and DS_OK if it succeeds. These symbols are defined in the DevServer.h file.
All methods should be defined as being static in C i.e. only directly accessible from within the classes source code file. Doing this forces applications and subclasses to use the method_finder to execute a class's methods. This is what is commonly termed code-hiding and is one of the advantages of Object Oriented Programming. Another advantage of the method_finder is that it supports code inheritance. A subclass can inherit code from its superclass(es). This is the case for all subclasses of the DevServerClass for example - they inherit the network interface code which is implemented in DevServerClass.
The method finder has following calling syntax -
DevMethodFunction ds__method_finder(DevServer ds, DevMethod method);DevMethodFunction is defined as a pointer to function i.e.
typedef long int (*DevMethodFunction)();The returned function pointer points to the function implementing the desired method which was found by the method_finder. It is then necessary to call the function. An example of using the method finder to search for and execute the object_initialise method is -
ds__method_finder(ds_list[i],DevMethodInitialise)(ds_list[i],error)
The device create function has following calling syntax -
long ds__create (char *name, void *ptr_ds_class, void *ptr_ds_ptr, long *error)
The device destroy function has following syntax -
long ds__destroy (void *ptr_ds_class, void *ptr_ds, long *error)
The class initialise method must have following calling syntax -
static long class_initialise(long *error)
Here is an example class initialise method (for the AGPowerSupplyClass) -
/*====================================================================== Function: static long class_initialise() Description: Initialise the AGPowerSupplyClass, is called once for this class per process. class_initialise() will initialise the class structure (aGPowerSupplyClass) and the default powersupply device (aGPowerSupply). Arg(s) In: none Arg(s) Out: long *error - pointer to error code if routine fails. =======================================================================*/ static long class_initialise(error) long *error; { AGPowerSupply ps; int state; /* * AGPowerSupplyClass is a subclass of PowerSupplyClass */ aGPowerSupplyClass->devserver_class.superclass = (DevServerClass)powerSupplyClass; aGPowerSupplyClass->devserver_class.class_name = (char*)malloc(sizeof("AGPowerSupplyClass")); sprintf(aGPowerSupplyClass->devserver_class.class_name,"AGPowerSupplyClass"); /* * commands implemented for the AG PowerSUpply class */ aGPowerSupplyClass->devserver_class.n_commands = n_commands; aGPowerSupplyClass->devserver_class.commands_list = commands_list; aGPowerSupplyClass->devserver_class.class_inited = 1; /* * initialise the template powersupply so that DevMethodCreate has * default values for creating a powersupply, these values will be * overridden by the static database (if defined there). */ aGPowerSupply->devserver.class_pointer = (DevServerClass)aGPowerSupplyClass; /* * default is to start with powersupply switched OFF; the state * variable gets (ab)used during initialisation to interpret the * initial state of the powersupply: 0==DEVOFF, 1==DEVON. this is * because the database doesn't support the normal state variables * like DEVON, DEVSTANDBY, DEVINSERTED, etc. */ aGPowerSupply->devserver.state = 0; aGPowerSupply->devserver.n_state = aGPowerSupply->devserver.state; aGPowerSupply->powersupply.set_val = 0.0; aGPowerSupply->powersupply.read_val = 0.0; aGPowerSupply->powersupply.channel = 1; aGPowerSupply->powersupply.n_ave = 1; aGPowerSupply->powersupply.conv_unit = (char*)malloc(sizeof("AMP")); sprintf(aGPowerSupply->powersupply.conv_unit,"AMP"); aGPowerSupply->powersupply.set_offset = 0.0, aGPowerSupply->powersupply.read_offset = 0.0; aGPowerSupply->powersupply.set_u_limit = AG_MAX_CUR; aGPowerSupply->powersupply.set_l_limit = AG_MIN_CUR; aGPowerSupply->powersupply.polarity = 1.0; /* * interrogate the static database for default values */ ps = aGPowerSupply; res_table[0].resource_adr = &(ps->devserver.state); res_table[1].resource_adr = &(ps->powersupply.set_val); res_table[2].resource_adr = &(ps->powersupply.channel); res_table[3].resource_adr = &(ps->powersupply.n_ave); res_table[4].resource_adr = &(ps->powersupply.conv_unit); res_table[5].resource_adr = &(ps->powersupply.set_offset); res_table[6].resource_adr = &(ps->powersupply.read_offset); res_table[7].resource_adr = &(ps->powersupply.set_u_limit); res_table[8].resource_adr = &(ps->powersupply.set_l_limit); res_table[9].resource_adr = &(ps->powersupply.polarity); if(db_getresource("CLASS/AGPS/DEFAULT",res_table,res_tab_size,error)) { printf("class_initialise(): db_getresource() failed, error %d\n",error); return(DS_NOTOK); } else { printf("default values after searching the static database\n\n"); printf("CLASS/AGPS/DEFAULT/state D_LONG_TYPE %6d\n", ps->devserver.state); printf("CLASS/AGPS/DEFAULT/set_val D_FLOAT_TYPE %6.0f\n", ps->powersupply.set_val); printf("CLASS/AGPS/DEFAULT/channel D_SHORT_TYPE %6d\n", ps->powersupply.channel); printf("CLASS/AGPS/DEFAULT/n_ave D_SHORT_TYPE %6d\n", ps->powersupply.n_ave); printf("CLASS/AGPS/DEFAULT/conv_unit D_STRING_TYPE %6s\n", ps->powersupply.conv_unit); printf("CLASS/AGPS/DEFAULT/set_offset D_FLOAT_TYPE %6.0f\n", ps->powersupply.set_offset); printf("CLASS/AGPS/DEFAULT/read_offset D_FLOAT_TYPE %6.0f\n", ps->powersupply.read_offset); printf("CLASS/AGPS/DEFAULT/set_u_limit D_FLOAT_TYPE %6.0f\n", ps->powersupply.set_u_limit); printf("CLASS/AGPS/DEFAULT/set_l_limit D_FLOAT_TYPE %6.0f\n", ps->powersupply.set_l_limit); printf("CLASS/AGPS/DEFAULT/polarity D_SHORT_TYPE %6d\n", ps->powersupply.polarity); } printf("returning from class_initialise()\n"); return(DS_OK); }
The device create method must have following calling syntax -
static long object_create(char *name, DevServer *ds_ptr, long *error)
Here is an example object create method (for the AGPowerSupplyClass) -
/*====================================================================== Function: static long object_create() Description: create a AGPowerSupply object. This involves allocating memory for this object and initialising its name. Arg(s) In: char *name - name of object. Arg(s) Out: DevServer *ds_ptr - pointer to object created. long *error - pointer to error code (in case of failure) =======================================================================*/ static long object_create(name, ds_ptr, error) char *name; DevServer *ds_ptr; long *error; { AGPowerSupply ps; printf("arrived in object_create(), name %s\n",name); ps = (AGPowerSupply)malloc(sizeof(AGPowerSupplyRec)); /* * initialise server with template */ *(AGPowerSupplyRec*)ps = *(AGPowerSupplyRec*)aGPowerSupply; /* * finally initialise the non-default values */ ps->devserver.name = (char*)malloc(strlen(name)); sprintf(ps->devserver.name,"%s",name); *ds_ptr = (DevServer)ps; printf("leaving object_create() and all OK\n"); return(DS_OK); }
The device initialise method should have following calling syntax -
static long object_initialise(DevServer ds, long *error)
Here is an example device initialise method (for the AGPowerSupplyClass) -
/*====================================================================== Function: static long object_initialise() Description: initialise a AGPowerSupply object. This involves retrieving all resources for this device from the resource database. Arg(s) In: AGPowerSupply *name - name of object. Arg(s) Out: long *error - pointer to error code (in case of failure) =======================================================================*/ static long object_initialise(ps,error) AGPowerSupply ps; long *error; { printf("arrived in object_initialise()\n"); /* * initialise powersupply with values defined in database */ res_table[0].resource_adr = &(ps->devserver.state); res_table[1].resource_adr = &(ps->powersupply.set_val); res_table[2].resource_adr = &(ps->powersupply.channel); res_table[3].resource_adr = &(ps->powersupply.n_ave); res_table[4].resource_adr = &(ps->powersupply.conv_unit); res_table[5].resource_adr = &(ps->powersupply.set_offset); res_table[6].resource_adr = &(ps->powersupply.read_offset); res_table[7].resource_adr = &(ps->powersupply.set_u_limit); res_table[8].resource_adr = &(ps->powersupply.set_l_limit); res_table[9].resource_adr = &(ps->powersupply.polarity); if(db_getresource(ps->devserver.name,res_table,res_tab_size,error)) { printf("class_initialise(): db_getresource() failed, error %d\n",error); return(DS_NOTOK); } else { printf("initial values after searching the static database for %s\n\n", ps->devserver.name); printf("state D_LONG_TYPE %6d\n",ps->devserver.state); printf("set_val D_FLOAT_TYPE %6.0f\n",ps->powersupply.set_val); printf("channel D_SHORT_TYPE %6d\n",ps->powersupply.channel); printf("n_ave D_SHORT_TYPE %6d\n",ps->powersupply.n_ave); printf("conv_unit D_STRING_TYPE %6s\n",ps->powersupply.conv_unit); printf("set_offset D_FLOAT_TYPE %6.0f\n",ps->powersupply.set_offset); printf("read_offset D_FLOAT_TYPE %6.0f\n",ps->powersupply.read_offset); printf("set_u_limit D_FLOAT_TYPE %6.0f\n",ps->powersupply.set_u_limit); printf("set_l_limit D_FLOAT_TYPE %6.0f\n",ps->powersupply.set_l_limit); printf("polarity D_SHORT_TYPE %6d\n",ps->powersupply.polarity); /* * interpret the initial state of the powersupply */ if (ps->devserver.state == 1) { printf("switching ON\n"); dev_on(ps,NULL,NULL,error); /* * if switched ON then set the current too */ dev_setvalue(ps,&(ps->powersupply.set_val),NULL,error); } else { printf("switching OFF\n"); /* * default is to assume the powersupply is OFF */ dev_off(ps,NULL,NULL,error); } } return(DS_OK); }
Hopefully after going through the seven stages the programmer has a device server ready to be used. However the world is not a perfect place - the device might not correspond to what the user had in mind or the user might change his mind. Consequently even if the device server is ready and tested it might be necessary to modify it, add new functionalities to it or even rewrite it. Do not be afraid to go back to stage 1 and start again. The important thing is to deliver useful software which does the job well. The process should be repeated as often as necessary.
It is often hard to get hold of this essential information. But the Device Server Programmer should not give up. In very difficult cases it might be necessary to apply pressure on the Equipment Responsible (by explaining the situation to his superior for example) to produce the relevant information (i.e. device description and specifications).
It is very important to have a good understanding of the device interfacing before starting designing a new class. Some of the classic interfaces encountered while writing device servers are -
The above points have to be taken into account when designing the level of device abstraction. The definition of what is a device for a certain hardware is primarily the job of the Device Server Programmer and the Applications Programmer but can also involve the Equipment Responsible. The Device Server Programmer should make sure that the Applications Programmer agrees with her definition of what is a device.
Here are some guidelines to follow while defining the level of device abstraction -
The list of commands to be implemented depends on the capabilities of the hardware, the list of sensible functions which can be executed at a distance and of course the functionality required by the application. This implies a close collaboration between the Equipment Responsible, Device Server Programmer and the Application Programmer.
When drawing up the list of commands particular attention should be paid to the following points -
The command names should be drawn up by the Device Server Programmer and the Device Server Source Custodian. All commands are presently stored in a single header file (DevCmds.h) and therefore the names have to be chosen so that there is no name clash and that they fit in with the naming convention used. It is often possible to reuse existing commands for new devices. The argument types which need to be passed for the commands should also be discussed with the Device Server Source Custodian. He can give advice where necessary on what types can be used how.
The list of device commands should be written and discussed with the Equipment Responsible and the Applications Programmer before any coding is started. The commands list should be used as the basis for the Device Servers User Guide, a document which has to exist for each device server. Don't forget documentation is part of the design and as such should be finished before the program.
The commands and the types they use are defined in the header of the class .c file. Here is an example of the commands defined for the AGPowerSupplyClass -
static DevCommandListEntry commands_list[] = { {DevOff, dev_off, D_VOID_TYPE, D_VOID_TYPE}, {DevOn, dev_on, D_VOID_TYPE, D_VOID_TYPE}, {DevState, dev_state, D_VOID_TYPE, D_SHORT_TYPE}, {DevSetValue, dev_setvalue, D_FLOAT_TYPE, D_VOID_TYPE}, {DevReadValue, dev_readvalue, D_VOID_TYPE, D_FLOAT_READPOINT}, {DevReset, dev_reset, D_VOID_TYPE, D_VOID_TYPE}, {DevStatus, dev_status, D_VOID_TYPE, D_STRING_TYPE}, {DevError, dev_error, D_VOID_TYPE, D_VOID_TYPE}, {DevLocal, dev_local, D_VOID_TYPE, D_VOID_TYPE}, {DevRemote, dev_remote, D_VOID_TYPE, D_VOID_TYPE}, {DevUpdate, dev_update, D_VOID_TYPE, D_STATE_FLOAT_READPOINT}, }; static long n_commands = sizeof(commands_list)/sizeof(DevCommandListEntry);
Examples of these commands for the AGPowerSupplyClass are -
/*====================================================================== Function: static long dev_state() Description: return state of simulated power supply. Arg(s) In: AGPowerSupply ps - object on which to execute command. DevVoid *argin - void. Arg(s) Out: DevShort *argout - state returned as short integer long *error - pointer to error code (in case routine fails) =======================================================================*/ static long dev_state (ps, argin, argout, error) AGPowerSupply ps; DevVoid *argin; DevShort *argout; long *error; { /* this command can be always executed independent of the state */ *argout = ps->devserver.state; return (DS_OK); }
/*====================================================================== Function: static long dev_status() Description: Return the state as an ASCII string. Interprets the error flag as well if the status is FAULT. Arg(s) In: AGPowerSupply ps - object on which to execute command. DevVoid *argin - void. Arg(s) Out: DevString *argout - status is returned as a string. long *error - pointer to error code (in the case of failure) =======================================================================*/ static long dev_status (ps, argin, argout, error) AGPowerSupply ps; DevVoid *argin; DevString *argout; long *error; { static char mess[1024]; int fault = ps->powersupply.fault_val; long p_state; p_state = ps->devserver.state; switch (p_state) { case (DEVOFF) : sprintf(mess,"%s","Off"); break; case (DEVON) : sprintf(mess,"%s","On"); break; case (DEVLOCAL) : sprintf(mess,"%s","Local"); break; case (DEVFAULT) : sprintf(mess,"%s","Fault\n"); break; default : sprintf(mess,"%s","Unknown"); break; } /* translate fault into a string */ if ((fault != 0) && (p_state == DEVFAULT)) { if ((fault & AG_OVERTEMP) != 0) { sprintf(mess+strlen(mess)," %s","Overtemp"); } if ((fault & AG_NO_WATER) != 0) { sprintf(mess+strlen(mess)," %s","No Cooling"); } if ((fault & AG_CROWBAR) != 0) { sprintf(mess+strlen(mess)," %s","Crowbar"); } if ((fault & AG_RIPPLE) != 0) { sprintf(mess+strlen(mess)," %s","Ripple"); } if ((fault & AG_MAINS) != 0) { sprintf(mess+strlen(mess)," %s","Mains"); } if ((fault & AG_LOAD) != 0) { sprintf(mess+strlen(mess)," %s","Load"); } if ((fault & AG_TRANSFORMER) != 0) { sprintf(mess+strlen(mess)," %s","Transformer"); } if ((fault & AG_THYRISTOR) != 0) { sprintf(mess+strlen(mess)," %s","Thyristor"); } } *argout = mess; return(DS_OK); }
Standard commands ensure uniform behaviour of all devices and allow standard utilities to be used for interrogating and displaying device status to be developed. Subsets of standard commands exist for devices belonging to the same superclass. For example all powersupplies should implement the same minimum set of commands. The reader is referred to the Device Server Notes for a description of the major superclasses.
All data types supported by the device servers require conversion routines for serialising and deserialising data from local format to network format (XDR format). For this reason in practice only a subset of data types are supported. The list of supported types can be found in xdr_typelist.h and in the related _xdr.h files.
All basic C types and also variable arrays thereof exist. Programmers should try as much as possible to restrict themselves to only these types. This reduces the number of data types which have to be supported and makes it easier to interface device servers to other software packages. These basic types are -
When using variable length data types don't forget that network transfers are restricted to 8 kbytes for UDP/IP protocol exchanges. If it is necessary to transfer more data then use TCP/IP. The switching between the two protocols occurs on the client side.
Presently all known data type conversion routines are linked with every device server. This is not at all efficient and wastes quite a lot of memory. In the future (summer 1993) a scheme will be introduced where only the basic types will be linked with each server and/or client and any additional types will require including an include file which contains the type definitions.
The design can be in terms of a simple description (if the device server is simple) or it can consist of data flow diagrams and algorithms. The design should be documented in a computer readable form and this documentation stored with the device server source.
When designing a device server account should be taken of the DSM. The device server is primarily there to accept and execute commands from the network. It spends most of its time waiting for commands or clients to connect on the network and then to serve these requests. Because only one process exists per device server if the device server spends a lot of time doing something else all connections to it (and thereby all devices served by it) are blocked. In severe cases this can cause clients to timeout. Consequently the device server should not spend a lot of time executing any one command. All commands should be executed immediately so that the device server can go back to servicing the same or other clients.
Where the device server is required to treat other events which might be time consuming or require their own polling it is best to consider using a multi-process solution. Time consuming commands should be relegated to independent processes which do not block the device server. The capabilities of the operating system should be used to communicate between the device server and its coprocesses. Most operating systems offer an adequate range of possibilities for synchronising and communicating between process (for example shared memory, events, signals fifos etc.). Although the DSM constrains the programmer to a single event loop within the device server it does not prevent the device server from using the operating system to its fullest. Refer to the section on Advanced programming techniques (later on) and to the Device Server Notes for solutions already in use by existing device servers.
The following documents should exist for each device server
Coding is best done using the automatic class generator written by Laurent Claustre (1992). Two versions of this exist (1) an ascii version (classgen) which requires file input, and (2) a Motif/X11 based version (xclassgen) which uses a graphic interface. Consult the user's manual for the class generator for details on how to use it.
It is also possible to take an existing class and use it as the starting point for a new device class. A global edit can very quickly turn an existing class into the beginnings of a new class.
Symbolic debuggers exist on all platforms and can be used to assist the debugging process. Debugging options should be described in the Design Documentation.
It is useful to always have a debugging version of each class always ready so that in the case of doubt or problems this version can be loaded and used to identify the problem(s).
Here is an example of menu program for the AGPowerSupplyClass -
/********************************************************************* File: ps_menu.c Project: Device Servers Description: Code for a menu driven test program for AGPowerSupplies. Allows each command to be executed on a given device. Device name is specified on the command line. Author(s); A. Goetz Original: March 1991 $Log: ps_menu.c.tex,v $ Revision 1.1 93/04/05 18:16:41 18:16:41 goetz (Andy Goetz) Initial revision * Revision 1.1 91/05/02 08:25:31 08:25:31 goetz (Andy Goetz) * Initial revision * Copyright (c) 1991 by European Synchrotron Radiation Facility, Grenoble, France *********************************************************************/ #include <API.h> #include <DevServer.h> /* * include AGPowerSupply public file to get DevRemote command definition */ #include <AGPowerSupply.h> main(argc,argv) unsigned int argc; char **argv; { devserver ps; DevArg arg; long readwrite = 0, error; int cmd, status, nave, chan; float setcurrent, setvoltage; DevFloatReadPoint readcurrent, readvoltage; DevStateFloatReadPoint statereadpoint; short devstatus; char *ch_ptr,cmd_string[256]; if (argc < 2) { printf("usage: %s device-name\n",argv[0]); exit(1); } status = dev_import(argv[1],readwrite,&ps,&error); printf("dev_import(%s) returned %d\n",argv[1],status); if (status != 0) exit(1); while (1) { printf("Select one of the following commands : \n\n"); printf("0. Quit\n\n"); printf("1. On 2. Off 3. State\n"); printf("4. Status 5. Set 6. Read\n"); printf("7. Update 8. Local 9. Remote\n"); printf("10.Error 11.Reset\n\n"); printf("cmd ? "); /* * to get around the strange effects of scanf() wait for something read */ for( ; gets(cmd_string) == (char *)0 ; ); status = sscanf(cmd_string,"%d",&cmd); switch (cmd) { case (1) : status = dev_putget(ps,DevOn,NULL,D_VOID_TYPE,NULL, D_VOID_TYPE,&error); printf("\nDevOn dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (2) : status = dev_putget(ps,DevOff,NULL,D_VOID_TYPE,NULL, D_VOID_TYPE,&error); printf("\nDevOff dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (3) : status = dev_putget(ps,DevState,NULL,D_VOID_TYPE, &devstatus,D_SHORT_TYPE,&error); printf("\nDevState dev_putget() returned %d\n ",status); if (status == 0) { printf("status read %d , %s \n",devstatus,DEVSTATES[devstatus]); } break; case (4) : status = dev_putget(ps,DevStatus,NULL,D_VOID_TYPE, &ch_ptr,D_STRING_TYPE,&error); printf("\nDevStatus dev_putget() returned %d\n ",status); if (status == 0) { printf(" %s \n ",ch_ptr); } break; case (9) : status = dev_put(ps,DevRemote,NULL,D_VOID_TYPE,&error); printf("\nDevRemote dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (5) : printf("set current to ? "); for( ; gets(cmd_string) == (char *)0 ; ); sscanf(cmd_string,"%f,",&setcurrent); status = dev_putget(ps,DevSetValue,&setcurrent,D_FLOAT_TYPE,NULL,NULL,&error); printf("\nDevSetValue dev_putget() returned %d, ",status); printf("current should be set to %6.2f amps\n",setcurrent); if (status < 0) dev_perror(NULL); break; case (6) : status = dev_putget(ps,DevReadValue,NULL,D_VOID_TYPE, &readcurrent,D_FLOAT_READPOINT,&error); printf("\nDevReadValue dev_putget() returned %d, ",status); printf("current set to %6.3f read %6.3f\n",readcurrent.set, readcurrent.read); if (status < 0) dev_perror(NULL); break; case (11) : status = dev_put(ps,DevReset,NULL,D_VOID_TYPE,&error); printf("\nDevReset dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (10) : status = dev_put(ps,DevError,NULL,D_VOID_TYPE,&error); printf("\nDevError dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (8) : status = dev_put(ps,DevLocal,NULL,D_VOID_TYPE,&error); printf("\nDevLocal dev_put() returned %d\n",status); if (status < 0) dev_perror(NULL); break; case (7) : status = dev_putget(ps,DevUpdate,NULL,D_VOID_TYPE, &statereadpoint,D_STATE_FLOAT_READPOINT,&error); printf("\nDevUpdate devputget() returned %d (error %d)\n",status,error); if (status >= 0) { printf("status read %d , %s \n",statereadpoint.state,DEVSTATES[statereadpoint.state]); printf("current set to %6.3f read %6.3f\n",statereadpoint.set, statereadpoint.read); } break; case (12) : dev_free(ps,&error); exit(0); default : break; } } }Which provides the user with the following menu -
$ ps_menu tl1/ps-d/d dev_import() returned 0 Select one of the following commands : 0. Quit 1. On 2. Off 3. State 4. Status 5. Set 6. Read 7. Update 8. Local 9. Remote 10.Error 11.Reset cmd ?
An example of using the resource database can be found above under the object initialise section.
For very simple devices the state_handler has very little to do - any command can be executed at anytime. For other more complicated devices the state_handler is used to reflect the internal state of the device.
All devices must however use the state_handler to control access to the device. It should be used to reflect the availability of the device. Any device (even the simplest) should support the following two states
An example state machine for the AGPowerSupplyClass is given below,
it implements the state diagram depicted in figure -
/*====================================================================== Function: static long state_handler() Description: Check if the command to be executed does not violate the present state of the device. Arg(s) In: AGPowerSupply ps - device to execute command to. DevCommand cmd - command to be executed. Arg(s) Out: long *error - pointer to error code (in case of failure). =======================================================================*/ static long state_handler( ps, cmd, error) AGPowerSupply ps; DevCommand cmd; long *error; { long iret = DS_OK; long int p_state, n_state; p_state = ps->devserver.state; /* * before checking out the state machine assume that the state * doesn't change i.e. new state == old state */ n_state = p_state; switch (p_state) { case (DEVOFF) : { switch (cmd) { case (DevOn) : n_state = DEVON; break; case (DevError) : n_state = DEVFAULT; break; case (DevLocal) : n_state = DEVLOCAL; break; /* following commands are ignored in this state */ case (DevSetValue) : case (DevReadValue) : iret = DS_NOTOK; *error = DevErr_CommandIgnored; break; /* default is to allow commands */ default : break; } break; } case (DEVON) : { switch (cmd) { case (DevOff) : n_state = DEVOFF; break; case (DevError) : n_state = DEVFAULT; break; case (DevLocal) : n_state = DEVLOCAL; break; /* following commands violate the state machine */ case (DevRemote) : case (DevReset) : iret = DS_NOTOK; (*error) = DevErr_AttemptToViolateStateMachine; break; /* default is to allow commands */ default : break; } break; } case (DEVLOCAL) : { switch (cmd) { case (DevRemote) : n_state = DEVOFF; break; /* the following commands violate the state machine */ case (DevOn) : case (DevOff) : case (DevRun) : case (DevReset) : case (DevStandby) : case (DevError) : iret = DS_NOTOK; (*error) = DevErr_AttemptToViolateStateMachine; break; /* following commands are ignored */ case (DevSetValue) : iret = DS_NOTOK; *error = DevErr_CommandIgnored; break; /* default is to allow commands */ default : break; } break; } case (DEVFAULT) : { switch (cmd) { case (DevReset) : n_state = DEVOFF; break; /* the following commands violate the state machine */ case (DevOff) : case (DevRemote) : case (DevOn) : case (DevLocal) : iret = DS_NOTOK; (*error) = DevErr_AttemptToViolateStateMachine; break; /* following commands are ignored */ case (DevSetValue) : case (DevReadValue) : iret = DS_NOTOK; *error = DevErr_CommandIgnored; break; /* default is to allow commands */ default : break; } break; } default : break; } /* * update powersupply's private variable n_state so that other methods * can use it too. */ ps->devserver.n_state = n_state; #ifdef DS_DEBUG printf("state_handler(): p_state %2d n_state %2d, iret %2d\n", p_state,n_state, iret); #endif return(iret); }
Sub-classes of the same super-class usually represent similar
devices and should therefore have the same or similar state machine.
A diagram (like in fig. ) representing the state machine of
each new class
should be included as part of the standard device server documentation.
Errors in command execution should be indicated by the status DS_NOTOK being returned.
Faults in the device which prevent a command from being executed should be signalled by the status DS_NOTOK being returned.
The startup() has the job of creating all devices of a given class and exporting them onto the network. It can also be used to do global initialisation or other non-standard actions like exporting sub-objects. The startup should return a long status which indicates whether the startup has worked or not. A non-zero status will be interpreted as a failure and the main will do an exit.
The startup function is called by main() with the following syntax -
long startup(char *svr_name, long *error)Where svr_name is the personal name referred to below.
An example is the startup for the AGPowerSupplyClass -
/********************************************************************* File: startup.c Project: Device Servers Description: Startup procedure for AGPowerSupplyClass. The startup procedure is the first procedure called from main() when the device server starts up. All toplevel devices to be created for the device server should be done in startup(). The startup should make use of the database to determine which devices it should create. Initialisation of devices is normally done from startup(). Author(s); A. Goetz Original: March 1991 $Log: startup.c.tex,v $ Revision 1.2 93/04/05 18:16:44 18:16:44 goetz (Andy Goetz) *** empty log message *** Copyright (c) 1990 by European Synchrotron Radiation Facility, Grenoble, France *********************************************************************/ #include <Admin.h> #include <API.h> #include <DevServer.h> #include <DevErrors.h> #include <DevServerP.h> #include <AGPowerSupplyP.h> #include <AGPowerSupply.h> /***************************/ /* AG PowerSupply startup */ /***************************/ long startup(svr_name, error) char *svr_name; long *error; { AGPowerSupply ps_list[MAX_NO_OF_DEVICES]; int i,status; /* * pointer to list of devices returned by database. */ char **dev_list; int dev_no; if (db_getdevlist(svr_name,&dev_list,&dev_no,error)) { printf("startup(): db_getdevlist() failed, error %d\n",*error); break; } else { printf("following devices found in static database \n\n"); for (i=0;i<dev_no;i++) { printf("%s\n",dev_list[i]); } } /* * create, initialise and export all devices served by this server */ for (i=0; i < dev_no; i++) { if (ds__create(dev_list[i], aGPowerSupplyClass, &(ps_list[i]),error) != 0) { break; } /* * initialise the newly created powersupply */ if (ds__method_finder(ps_list[i],DevMethodInitialise)(ps_list[i],error) != 0) { break; } /* * now export it to the outside world */ printf("created %s, going to export it\n",dev_list[i]); if (dev_export(dev_list[i],ps_list[i],error) != 0) { break; } printf("export worked !\n"); } printf("left startup and all's OK\n"); return(DS_OK); }
AGPSds/TL1/device: TL1/PS-D/D
The above resource attaches the device TL1/PS-D/D to the device server AGPSds which is started with the personal name TL1.
It is the job to the device server to retrieve the list of device names from the resource database using the database function db_getdevlist(). The syntax for db_getdevlist is -
db_getdevlist(char *svr_name,char ***dev_list,long *n_devices,long *error)
This call returns a list of device names which the startup can then create and initialise.
To export devices onto the network their are two possibilities - (1) calling the DevMethodDevExport directly with the method finder, or (2) using the convenience function dev_export() which calls the method finder. It is possible to export a device onto the network with a different name to its device name. This option is reserved for perverse device server programmers. For a device to be exportable it has to appear somewhere in a list of devices in the resource database (cf. above example).
ds__signal has the following syntax -
long ds__signal (int signo, void (*action)(), long *error);This call is used to register a function action for the signal signo. As soon as the device server receives a signal, it checks to see whether an action has been registered under this signal number and then calls it. Only one action can be registered per signal.
Signals allow the device server to set up asynchronous actions (e.g. timers) during execution of a command and return control to the client. On receipt of the signal (at a later time) the device server can then take appropriate action.
ds__signal() is the only to register actions with signals for device servers. This is because the device server has to exit gracefully and is always programmed for the signal SIGTERM. On receipt of the signal SIGTERM the device server will first check to see whether the class has registered its interest for this signal. If so it will call the corresponding function. After that it will exit gracefully by unregistering the device server from the static database.
For more information see the manual page ds__signal.
The calling syntax is -
long ds__svcrun (long *error);ds__svcrun will check all open sockets to see if there are any commands waiting to be executed and will then execute the next command. If there are no commands waiting the function will timeout after 10 ms (1 sec for OS9 !).
This section will describe some basic philosophy and techniques for implementing classes, subclasses and superclasses in OIC.
Classes programming represents a new approach to programming. Until recently the approach to programming was to use traditional languages (e.g. FORTRAN, PASCAL or C) to break down the problem into smaller problems. These smaller problems were then solved and coded up to produce libraries of subroutines, blocks or functions depending on the language used. Programs based on these functions consisted of a series of calls to the function (to use the C paradigm) implemented in the library(ies).
Classes represent a new approach to programming. A class can be best viewed as a generic description and solution for a particular problem. The art of good class programming is to find the description which best describes the problem. Instead of breaking down the problem into subproblems the problem is broken down into subclasses i.e. shorter descriptions, until eventually one arrives at an ensemble of generic descriptions which by taking specific instances of these descriptions will behave in such a way that they solve the particular problem.
Identifying which classes need to be implemented is not only an attempt at providing a generic description of a solution to a problem or task but also a hierarchical description of the solution. Programming classes are very closely modelled on biological classes. In this respect a class can be a member of other classes. However it is rarely an equal member. A designer of classes tries to organise her classes in order of rank. Some classes are more general than other classes. At the top of the hierarchy one finds a root class. This root classes contains a description of the characteristics and behaviour which are common to all members or sub-members of that class. The set of classes which constitute the solution cannot be defined by a single class. There will always be characteristics and behaviour patterns which are specific to only certain members. Therefore instead of having a top-heavy solution new classes are defined which inherit all or part of what is defined in the root class and then add what is new i.e. specific to them. These new classes are called subclasses because they inherit characteristics and behaviour from other (more generic) classes and because they appear lower within the hierarchical structure. A class which has subclasses is known as a superclass. An instance of a class which is used in another class is known as a subobject.
The first step in implementing a subclass is to specify its requirements. Subclasses are implemented by modifying the definition of the superclass (defined in the superclasses private include file). A definition is required for the partial object and class record structures. The partial object record structure contains those variables and constants which each member of the new class requires a personal copy thereof. The partial class structure contains those variables and constants which are required for the implementation of the class and which can be shared by all members of the class. The classes object and class record structures contain the full description of the class hierarchy. They are formed by adding the partial object and class record structures to the object and class record structures respectively of the superclass.
Once the class and object structures of the subclass are defined then the class behaviour can be implemented in the source file. This means implementing the minimum methods required by each class (e.g. class_initialise)plus the new methods which the new subclass requires. If the new subclass will be instantiated then it will also implement a list of device server commands.
It is necessary that each subclass initialises the root class (DevServerClass) class structure with at least the superclass pointer (superclass_pointer) in order for OIC to work. This is done in the DevMethodClassInitialise method implementation. The classes partial class structure is also initialised in the DevMethodClassInitialise method.
One very important implementation detail of OIC is that because it is only a programming technique and not a compiler a certain amount of redundancy exists which could confuse the beginner device server programmer. The class record definition includes the partial class record structures for the root class, all superclasses and the class itself. Because the superclasses do not know about the subclasses it is impossible for them to initialise 'their' partial class structures of each of their subclasses. Consequently the partial classes structures of the superclasses of a subclasses remain uninitialised. This (possibly confusing for beginner programmers) aspect has been retained in the OIC model for two reasons :
If the class needs to access data in one of its superclasses it should do so by following the superclasses class_pointer in the root class partial structure and thereby access the initialised copy of the superclasses partial structure.
The same doesn't apply to the object record structure however. Each object has its own private copy of the object record. A subclass can access the data defined in the superclasses object partial structure directly. All object data is accessible this way. This is because OIC does not distinguish between private and public data.
The idea behind a superclass is to abstract out what is common to a number of subclasses and implement this in a single class. This has the advantage of having only a single source to maintain. It also enforces reusability of code. Superclasses can be thought of as abstract classes which serve as place holders for data and a single common source for code. They are essential for implementing classes i.e. hierarchically organised generic descriptions.
Experience with class programming has shown that it is not a good idea to have too many levels of hierarchy. Nesting classes too deeply (i.e. more than five superclasses) is difficult to follow and dissuades programmers from reusing existing superclasses. The ideal level of nesting is three or in rare cases four levels of class hierarchy. Keep class hierarchies simple. It is more efficient to opt for a flat class structure with many toplevel classes than to go for heavily nested classes. Reusing existing classes implies reusing them as objects rather than as superclasses i.e. as subobjects.
To execute commands locally use the convenience function dev_cmd(). Syntax for dev_cmd is -
long dev_cmd (short cmd, DevArgument *argin_ptr, DevType in_type, DevArgument *argout_ptr, DevType out_type, long *error);
It is also possible to use remote devices as subobjects in a class by importing them (as opposed to creating them locally). This has the advantage that a new class can use existing classes across the network i.e. it is not obliged to be on the same physical machine as the imported device. It has the disadvantage that executing commands on the remote device takes longer because of the network overhead. Another disadvantage of this method is that methods cannot be executed remotely. Nonetheless it can be very useful sometimes to import devices in classes and it is done quite often.
This section will treat some of the most Frequently Asked Questions which device server programmers pose. It will also include a discussion on the limitations of the present device server programming model and improvements which need to be made to the present method of writing device server classes.
A device server is a single process which instantiates and exports object(s) of one or more classes. Once the object(s) have been exported the process waits for requests on the network to execute commands.
A device class is a software class which implements the generic behaviour and characteristics of a logical device. It is implemented in C using a method called Objects In C.
This question demonstrates a misunderstanding of the work of a Device Server Programmer. Device Server Programmers are writing software classes which describe and implement device access. These classes can be used by other classes or in conventional procedure base software. A device server (i.e. the process which serves a or many devices) is simply a way of packaging these classes into a process which provides a procedural interface on the network. If network access is requested and if timing is a problem subclasses can be combined in superclasses in such a way that all critical timing takes place within a single class (i.e. locally in one process), thereby removing the network access part from the critical path. Alternatively the critical code can be implemented in an independent processes and device server can be used to provide network access.
The DSM in no way prevents the programmer from using the operating system to its fullest - in theory it is possible to achieve the same response with a device server as with any other local process running under OS9 or Unix.
A method is a special function implemented in a class in the OIC programming methodology which can be inherited by subclasses of that class.
A command is a special function in the DSM which can be executed across the network using the device server api call dev_putget(). All commands have a fixed calling syntax. Commands as opposed to methods cannot be inherited by subclasses. The only way to inherit a command to a subclass is to implement it as a method.
A device server is only as complex as the device it has to implement and serve. The advantage of the DSM is that all common functions related to network access are standardised. What might appear complicated to beginners is the object oriented aspect of class programming. The advantages of class programming (e.g. hierarchical structuring, generic solutions, re-using code) are sufficient however that it is worth investing the time in learning how to write classes.
Device servers should not be used as an excuse not to write complex but maintainable software.
The answer is NO. In an ideal world both should exist. A device driver should be written to access the physical hardware by exploiting up to a maximum the I/O channels of the operating system. For example fast queued access with arbitration is offered by OS9 for drivers. A device server takes over from where the device driver leaves off. It offers higher level functions and network access. Although sometimes compared to a networked version of a device driver it is at a much higher level in terms of the way it presents information and the commands it offers.
Another limitation of the DSM is the lack of multiple inheritance. Multiple inheritance is the ability of a class to be derived from multiple superclasses at the same level. This limitation is due to the use of OIC. It can be partially overcome by using multiple superclasses arranged hierarchically but will only be completely overcome by either adding multiple inheritance to OIC or by implementing the DSM in an OOP language which supports multiple inheritance.
Timing is another area in the DSM which is treated in a limited way. A device server spends most of its time waiting for client requests. When a request is received it is executed completely i.e. synchronously, before the server goes back to waiting for client requests. The server by definition has only one thread of execution. If a server wants to communicate with other processes it has to use the mechanisms offered by the operating system or some of the advanced calls developed as part of the device server library (see Advanced programming techniques above). Timing has to be taking into account when designing device classes.
Other improvements which are planned are in the device server api. An asynchronous dev_putget will be added to complement the existing synchronous call. The asynchronous call be compatible with main event loop in X11/Motif applications. A second improvement to the api is the addition of a reliable protocol based on UDP/IP. To date only UDP/IP and TCP/IP are supported. The former is connectionless but not reliable while the latter is connection oriented and reliable. The aim is to add a third protocol which is connectionless but reliable i.e. based on UDP/IP. These improvements are planned for the summer of 1993.
Very few manuals are perfect and this is surely not one of them. The author will gladly accept any useful or constructive criticism on how to improve it.
This document is split into the following sections : ``Getting Started'' describes how to write a simple client which uses DSAPI, should be read by beginner's who want to get a quick start; ``C library'' is a reference guide to all DSAPI functions for clients and servers; ``XDR Types'' describes the XDR types supported by DSAPI; ``Changes'' describes what are the main changes in the different major releases; ``Platforms Supported'' lists the different platforms and compilers supported; ``Interfaces to other Languages'' contains a summary of DSAPI interfaces in other languages. Beginners should read ``Getting Started'' first, other programmers should read what changes have taken place in the latest version and use the reference guide.
Devices in a TACO control system are network objects created and served by processes called device servers. A device is identified by its ASCII name :
[//facility/]domain/family/member
Each device understands a set of commands. The commands enable a remote client to execute actions on a device e.g. for a powersupply switch it on or off, read the state, read the current.
The DSAPI gives remote and local clients access to device commands.
Using DSAPI it is possible to execute any command on any device (assuming the client has the necessary permission) in a TACO control system. Data is passed from the client to the device via the input and output parameters of the DSAPI.
Devices are organised into classes. Each class implements and understands a fixed set of commands. The list of commands for a device class is documented in the Device Server User's Guide (DSUG). The set of C functions which implement the DSAPI are archived in static or shared libraries for all platforms supported.
This example will take you through the steps of writing a simple program to send a ``Hello World'' string to a device synchronously.
The command we will use in this example is DevHello.
In the case of this example we want a program which sends a string to a device and reads one back.
The program is written in C and uses a simple ascii interface to interact with the user. The program listing can be found below (cf. section ``Code Example'').
ALL device access is done using DSAPI (of course). The main statements to note are :
Unix and OS9
To compile under Unix and OS9 you have to tell the compiler where to find the DSAPI include files and which libraries to link with.
Assuming the your program is called helloworld, $DSHOME is an environment variable which points to the root directory of your TACO installation and $OS the operating system type (s700 for HP-UX 9.x, hpux10.2 for HP-UX 10.2, solaris for Solaris, linux for Linux, vxworks for VxWorks, os9 for OS9) then simply type :
$CC $CFLAGS -I$DSHOME/include -L$DSHOME/lib/$OS -ldsapi -ldbapi -ldsxdr helloworld.c -o helloworld.
$CC and $CFLAGS have to be positioned for each platform (refer to the example Makefile). Windows-NT
To compile under Visual C++ 4.2 you need to set the following options using the graphical interface :
to be filled in ...
static char RcsId[] = "@(#)$Header: /segfs/dserver/doc/notes/DSN101/RCS/DSN101.tex,v 2.1 1997/11/13 14:16:40 goetz Exp $"; /*+******************************************************************* File : helloworld.c Project : Device Server Description: A simple test client to test using the synchronous device server API. Author(s) : Andy Goetz Original : November 1997 $Revision: 2.1 $ $Date: 1997/11/13 14:16:40 $ $Author: goetz $ $Log: DSN101.tex,v $ Revision 2.1 1997/11/13 14:16:40 goetz first release of DSAPI V6 Revision 1.5 1997/11/13 14:13:31 goetz totally reworked doc; added "Hello World" examples; asynchronous call; xdr types *-*******************************************************************/ #include <Admin.h> #include <API.h> main(argc,argv) unsigned int argc; char **argv; { devserver hw; long access = WRITE_ACCESS, error, status; char *ch_ptr,helloworld[256], dev_name[256]; switch (argc) { case 1: printf("enter device name [\"exp/hello/world\"]? "); if(NULL==gets(dev_name) || '\0'==dev_name[0]) strcpy(dev_name,"exp/hello/world"); break; case 2: strcpy(dev_name,argv[1]); break; default: printf("usage: helloworld [device name]\n"); exit(1); } status = dev_import(dev_name,access,&hw,&error); printf("dev_import(%s) returned %d\n",dev_name,status); if (status != 0) { printf("%s",dev_error_str(error)); exit(1); } sprintf(helloworld, "Hello World"); ch_ptr = NULL; status = dev_putget(hw,DevHello, &helloworld,D_STRING_TYPE, &ch_ptr,D_STRING_TYPE, &error); printf("\nDevHello dev_putget() returned %d\n",status); if (status == 0) { printf("device answered : %s\n",ch_ptr); dev_xdrfree(D_STRING_TYPE, &ch_ptr, &error); } else { dev_printerror_no(SEND,NULL,error); } dev_free(hw,&error); exit(0); }
Asynchronous command execution is more difficult to program than synchronous. However it is more efficient and is particularly useful for windowing programs and for programs which want to start multiple commands on multiple devices executing simultaneously and don't want to wait for the command to finish execution.
This example is identical to the above example excepting for the fact that DevHello command is executed asynchronously. A callback function specified. This makes the code longer and more slightly more complicated to read.
The new calls are :
Not support under Windows-NT (yet).
Not supported under Windows-NT (yet).
static char RcsId[] = "@(#)$Header: /segfs/dserver/doc/notes/DSN101/RCS/DSN101.tex,v 2.1 1997/11/13 14:16:40 goetz Exp $"; /*+******************************************************************* File : helloworld_asyn.c Project : Asynchronous Device Server's Description: A simple test client to test using the asynchronous device server API using callbacks. Author(s) : Andy Goetz Original : January 1997 $Revision: 2.1 $ $Date: 1997/11/13 14:16:40 $ $Author: goetz $ $Log: DSN101.tex,v $ Revision 2.1 1997/11/13 14:16:40 goetz first release of DSAPI V6 Revision 1.5 1997/11/13 14:13:31 goetz totally reworked doc; added "Hello World" examples; asynchronous call; xdr types *-*******************************************************************/ #include <API.h> #include <DevStates.h> /*+********************************************************************** Function : void hello_callback() Description: callback function to be called asynchronously after executing the DevHello commands ***********************************************************************-*/ void hello_callback(ds, user_data, cb_data) devserver ds; void *user_data; DevCallbackData cb_data; { long error; printf("hello_callback(%s): called with asynch_id=%d, status=%d (error=%d) user data = %s\n", ds->device_name,cb_data.asynch_id, cb_data.status, cb_data.error, (char*)user_data); printf("hello_callback(%s): time executed by server = {%d s,%d us}\n", ds->device_name,cb_data.time.tv_sec,cb_data.time.tv_usec); if (cb_data.status == DS_OK) { printf("hello_callback(%s): device answered=%s\n", ds->device_name,*(DevString*)cb_data.argout); dev_xdrfree(D_STRING_TYPE, &cb_data.argout, &error); } else { dev_printerror_no(SEND,NULL,cb_data.error); } return; } /*+********************************************************************** Function : main() Description: main function to test asynchronous DSAPI. ***********************************************************************-*/ main(argc,argv) unsigned int argc; char **argv; { devserver hw; long access = WRITE_ACCESS, error, status; char ch_ptr, helloworld[256], dev_name[256]; struct timeval timeout_25s = {25,0}; long asynch_id; char *user_data="my data"; switch (argc) { case 1: printf("enter device name [\"exp/hello/world\"]? "); if(NULL==gets(dev_name) || '\0'==dev_name[0]) strcpy(dev_name,"exp/hello/world"); break; case 2: strcpy(dev_name,argv[1]); break; default: printf("usage: helloworld_asyn [device name]\n"); exit(1); } imported = dev_import(dev_name,access,&hw,&error); printf("dev_import(%s) returned %d\n",dev_name,imported); if (imported != 0) { printf("%s",dev_error_str(error)); exit(1); } sprintf(helloworld, "Hello World"); ch_ptr = NULL; status = dev_putget_asyn(hw,DevHello, &helloworld,D_STRING_TYPE, &ch_ptr,D_STRING_TYPE, (DevCallbackFunction*)void_callback, (void*)user_data, &asynch_id, &error); printf("\nDevHello dev_putget_asynch(%d) returned %d\n",asynch_id, status); if (status < 0) dev_printerror_no(SEND,NULL,error); /* * wait for answer from client (waits for a max of 25 s) */ status = dev_synch(&timeout_25s, &error); dev_free(hw,&error); exit(0); }
A common error when starting an application (e.g. helloworld) is to forget to specify the NETHOST environment variable.
In this case you will get an error similar to this :
Thu Nov 6 13:56:42 1997 environmental variable NETHOST not definedThe solution is to set the environment variable to the name of a host where a TACO control system Manager is running e.g. ``setenv NETHOST libra'' for csh or ``export NETHOST=libra'' for ksh or bash.
An alternative to specifying the NETHOST environment variable is to qualify the device name with the facility field which is the same as the NETHOST e.g. //libra/exp/hello/world.
If the Manager is not running you will get the following error :
Thu Nov 6 14:03:26 1997 no network manager availableIf you don't know which host is your NETHOST then ask your TCO system administrator/guru. If you are supposed to be the guru then start the Manager. If you don't know how then send an email to the TACO help-line taco@esrf.fr
If your application dies with the following message :
./helloworld: can't load library 'libdsapi.so'You must add the DSAPI library directory for your platform to the shared library path searched by your system.
For Solaris and Linux use :
set $LD_LIBRARY_PATH:$DSHOME/lib/$OS for csh and tcsh,
export $LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$DSHOME/lib/$OS for ksh and bash.
For HP-UX use :
set $SHLIB_PATH:$DSHOME/lib/$OS for csh and tcsh,
export $SHLIB_PATH=$SHLIB_PATH:$DSHOME/lib/$OS for ksh and bash.
Where $DSHOME is and environment variable pointing to the TACO home directory and $OS the operating system flavour.
Shared libraries are not supported on OS9 and Windows/NT (yet).
The TACO makefiles are multi-platform and make use of the conditional statements supported by GNU make (also known as gmake). gmake supports statements of the kind ifdef $(symbol), else and endif. Most TACO conditional makefiles use the same symbols. These are :
A simple example Makefile for the helloworld program could look like this :
# # # Makefile for helloworld - a simple DSAPI client # # # TACO home directory # DSHOME = $(LOCAL_DSHOME) # # library home directory - platform dependant # ifdef __hpux10 LIBHOME = $(DSHOME)/lib/hpux10.2 endif # __hpux10 ifdef _solaris LIBHOME = $(DSHOME)/lib/solaris endif # _solaris ifdef linux LIBHOME = $(DSHOME)/lib/linux endif # linux ifdef _UCC LIBHOME = $(DSHOME)/lib/os9 endif # _UCC ifdef vw68k LIBHOME = $(DSHOME)/lib/vw68k endif # vw68k ifdef vwx86 LIBHOME = $(DSHOME)/lib/vwx86 endif # vwx86 # # include files home directory # INCLDIRS = -I$(DSHOME)/include \ -I$(DSHOME)/include/private # # compiler flags - platform dependant # ifdef __hpux10 CC = /bin/cc CFLAGS = -Aa -g -DEBUG -Dunix -D_HPUX_SOURCE -D__hpux10 -DBSD=199704 \ -c $(INCLDIRS) endif # __hpux10 ifdef _solaris CC = /opt/SUNWspro/SC4.0/bin/cc CFLAGS = -Xa -g -Dsolaris -DEBUG -c $(INCLDIRS) endif # _solaris ifdef linux CC = gcc CFLAGS = $(INCLDIRS) -Dlinux -Dunix -ansi -DEBUG -g -c endif # linux ifdef _UCC CC = xcc CFLAGS = -mode=c89 -g -D EBUG -to osk -tp 020 -x il -e as=. $(INCLDIRS) endif # _UCC ifdef vw68k CC = cc68k CFLAGS = -Dvxworks -Dunix -DCPU=MC68020 -ansi -m68030 \ -msoft-float -DEBUG -e $(INCLDIRS) -g endif # vw68k ifdef vwx86 CC = cc386 CFLAGS = -v -c -Dvxworks -Dunix -DCPU=I80386 -ansi \ -DEBUG $(INCLDIRS) -g endif # vwx86 # # library flags # ifdef __hpux10 LFLAGS = -L$(LIBHOME) -ldsapi -ldsxdr -ldbapi -lm endif # __hpux10 ifdef _solaris LFLAGS = -L$(LIBHOME) -ldsapi -ldsxdr -ldbapi -lnsl -lsocket -lm endif # _solaris ifdef linux LFLAGS = -L$(LIBHOME) -ldsapi -ldsxdr -ldbapi -lm endif # linux ifdef _UCC LFLAGS = -L$(LIBHOME) -l dsapi -l dsxdr -l dbapi -l rpclib -l netdb_small \ -l socklib.l -l sys_clib.l -l unix.l endif # _UCC # #------------------------main-target-to-make------------------------------ # all : helloworld helloworld : helloworld.c $(CC) $(CFLAGS) helloworld.c -o helloworld $(LFLAGS)
NOTE: don't forget to start all rules with a tabulation mark !
Although even this simple example looks complicated keeping all platform dependancies in one file can prove to be a time saver when developing on multiple platforms.
The rules for memory allocation in DSAPI can be summarised as follows :
If you understand the above rules and follow them you should not have any problems. The problems come from not understanding and following these rules. The XDR types supported by DSAPI are covered in the section on ``XDR Types''.
To illustrate the above rules here are some examples :
devserver ps; long status, error; float readvalue; . . . status = dev_putget(ps, DevReadCurrent, NULL, D_VOID_TYPE, &readvalue, D_FLOAT_TYPE, &error); printf("current %6.3f\n",readvalue); . . .This is a simple example of using a simple C type to receive output from the server. Simply pass the pointer to the simple type to DSAPI.
NOTE : DSAPI cannot allocated memory for simple types because it expects a pointer to a value and not a pointer to a pointer to a value and it therefore has no way of distinguishing between a pointer to the value ZERO and a pointer to NULL (if you know what I mean ...)
devserver ps; long status, error, i; float readvalues[MAX_READVALUES]; DevFloatVarArray float_vararr; . . . float_vararr.length = MAX_READVALUES; float_vararr.sequence = readvalues; status = dev_putget(ps, DevReadAll, NULL, D_VOID_TYPE, &float_vararr, D_VAR_FLOATARR, &error); printf("read %d value\n\n",float_vararr.sequence); for (i=0; i<float_vararr.sequence; i++) { printf(" current[%d] %6.3f\n", i, readvalues[i]); } . . .In this example the client receives a variable length array of floats. The client has allocated memory for the array of floats itself. It is the responsibility of the client to ensure that sufficient memory is allocated for the return argments and that the server does not send more values than the client expects.
devserver ps; long status, error, i; DevFloatVarArray float_vararr; . . . float_vararr.length = 0; float_vararr.sequence = NULL; status = dev_putget(ps, DevReadAll, NULL, D_VOID_TYPE, &float_vararr, D_VAR_FLOATARR, &error); printf("read %d value\n\n",float_vararr.sequence); for (i=0; i<float_vararr.sequence; i++) { printf(" current[%d] %6.3f\n", i, float_vararr.sequence[i]); } dev_xdrfree(D_VAR_FLOATARR, &float_vararr, &error); . . .In this example the client sets the sequence to NULL and lets DSAPI allocate memory for the output arguments. The client has to free the allocated memory.
The default timeout for synchronous calls is 3 seconds. The default timeout for asynchronous calls is 25 seconds.
The client can modify the timeout per device using the dev_rpctimeout() call (cf. the C library reference). This can be necessary if the request is know to take longer than the default timeout to execute.
If a client gets lots of timeouts there could be a network problem i.e. lots of network traffic. This can fixed by simply changing from UDP to TCP protocol (see next section).
typedef struct { u_int length; DevCmdInfo *sequence; } DevVarCmdArray; typedef struct { long cmd; /* command */ char cmd_name [20]; /* command name as ASCII string */ char *in_name; /* description of input arguments */ long in_type; /* type of input arguments */ char *out_name; /* description of output arguments */ long out_type; /* type of output arguments */ } DevCmdInfo; long dev_cmd_query (ds, varcmdarr, error) devserver ds; /* client handle */ DevVarCmdArray *varcmdarr; /* results of query */ long *error; /* error */Dev_cmd_query() returns a sequence of DevCmdInfo structures containing all available commands, their names, their input and output data types, and type descriptions for one device. Commands and data types are read from the command list in the device server. Command names are read from the CMDS table of the resource data base. Data type descriptions for input and output arguments for a command function have to be specified in the resource database in the CLASS table as:
CLASS/class_name/cmd_name/IN_TYPE: "Current in mA" CLASS/class_name/cmd_name/OUT_TYPE: "Power in MW" class_name : Name of the device class. Retrieved from the device server. cmd_name : Name of the command. Retrieved from the CMDS table in the resource data base.
long dev_free (ds,error) devserver ds; /* client handle */ long *error; /* error */Dev_free() closes the connection to a device associated with the passed client handle.
long dev_import (dev_name,access,ds_ptr,error) DevString dev_name; /* device name */ long access; /* requested access level */ devserver *ds_ptr; /* returned pointer to the client handle */ long *error; /* error */Opens a connection to a device and returns a client handle for the connection. Dev_import can distinguish between local and remote devices.
If the control system is running with security on then the access parameter determines what level of access permission the client wants on the device. The following levels are supported :
The default access is WRITE_ACCESS and correpsonds to access=0. If the TACO control system is running with security the client has to have the necessary permission in the security database for the (UID,GID,HOST,NETWORK) quadrupole.
For more information on security refer to ``Access Control and Security for the ESRF Control System'' by J.Meyer (DSN/102).
typedef struct { char device_name[80]; char device_class[32]; char device_type[32]; char server_name[80]; char server_host[32]; } DevInfo; long dev_inform (clnt_handles, num_devices, dev_info, error) devserver *clnt_handles; /* list of client handles */ long num_devices; /* number of client handles */ DevInfo **dev_info; /* returned list of information structures */ long *error; /* error */Dev_Inform() returns to the user a structure containing device information for every specified device client handle. The information structure contains:
long dev_put (ds,cmd,argin,argin_type,error) devserver ds; /* client handle */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* type of input arguments */ long *error; /* error */Dev_put() executes a command on the device associated with the passed client handle, without returning any output data. The device might be remote or local. Input data types must correspond to the types specified for this command in the device server's command list. Otherwise an error code will be returned. The output data type in the device server's command list must be set to D_VOID_TYPE. All arguments have to be passed as pointers.
long dev_put_asyn (ds,cmd,argin,argin_type,error) devserver ds; /* client handle */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* type of input arguments */ long *error; /* error */The function dev_put_asyn() is similar to dev_put(). The only difference is, that dev_put_asyn() sends a request to execute a command to a device server and returns immediately when the command was received. The only errors which can be returned by dev_put_asyn() are errors during the sending of the command. A correct return status only indicates that the command execution was started.
long dev_putget (ds,cmd,argin,argin_type,argout,argout_type,error) devserver ds; /* client handle */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* type of input arguments */ DevArgument argout; /* pointer to output arguments */ DevType argout_type; /* type of output arguments */ long *error; /* error */Dev_putget() executes a command synchronously on the device associated with the passed client handle. The device might be remote or local. Input and output data types must correspond to the types specified for this command in the device server's command list. Otherwise an error code will be returned. All arguments have to be passed as pointers.
typedef struct { u_int length; char *sequence; } DevOpaque; long dev_putget_raw (ds,cmd,argin,argin_type,argout,argout_type,error) devserver ds; /* client handle */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* type of input arguments */ DevOpaque *argout; /* pointer to opaque data */ DevType argout_type; /* type of output arguments, returned by the command */ long *error; /* error */Dev_putget_raw() executes a command on the device associated with the passed client handle and returns the outgoing arguments as a block of opaque data in XDR format. All arguments have to be passed as pointers. Memory for the opaque block will be allocated by the RPC if the sequence pointer is initialised to NULL. The allocated memory can be freed with dev_xdrfree() and the type identifier D_OPAQUE_TYPE.
long dev_rpc_protocol (ds, protocol, error) devserver ds; /* client handle */ int protocol; /* transport protocol */ long *error; /* error */By calling dev_rpc_protocol() with one of the two defined protocol parameters D_UDP and D_TCP (API.h), the transport protocol for an open RPC connection will be set to the chosen protocol. Before switching the protocol, an RPC connection to a device server has to be opened by a dev_import() call.
All devices implemented in the same server and imported by the client use the same RPC connection. Changing the protocol of a RPC connection with dev_rpc_protocol means changing the protocol for all devices of the same server.
long dev_rpc_timeout (ds, request, dev_timeout, error) devserver ds; /* client handle */ int request; /* CLSET_TIMEOUT or CLGET_TIMEOUT */ struct timeval *dev_timeout; /* timeout value */ long *error; /* error */Sets or reads the timeout for a RPC connection with UDP protocol. A request to set the timeout has to be asked with CLSET_TIMEOUT as request parameter and the timeout specified by the timeval structure dev_timeout. The timeout will be set without any retry. A request to read the timeout has to be asked with CLGET_TIMEOUT, and the current timeout will be returned in dev_timeout.
All devices implemented in the same server and imported by the client use the same RPC connection. Changing the timeout of a RPC connection with dev_rpc_timeout means changing the timeout value for all devices of the same server.
long dev_xdrfree (type, objptr, error) DevType type; /* type of arguments */ DevArgument objptr; /* pointer to arguments */ long *error; /* error */Dev_xdrfree frees the memory for device server data allocated by XDR. An example for the use of dev_xdrfree() is the freeing of a D_VAR_FLOATARR data type. Using dev_xdrfree() you don't have to care about the length of the internal sequence of float values. Just pass a pointer to a D_VAR_FLOATARR structure and the allocated memory for the sequence will be freed, according to the length specified in the structure.
long dev_asynch_timeout ( devserver ds, long request, struct timeval *tout, long *error)Call to set/get the timeout for an asynchronous call to the device ds. Get/Set operation is determined by request = CLSET_TIMEOUT or CLGET_TIMEOUT. The timeout is returned/specified in tout. If an error occurs the call returns DS_NOTOK and an appropiate error code in error.
long dev_pending ( devserver ds)Call to return the number of asynchronous requests still pending replies for device ds. If ds = NULL then return the total number of pending calls.
struct _DevCallbackData { long asynch_id; /* id of asynchronous call */ DevArgument argout; /* pointer to output argument */ DevType argout_type; /* argout type */ long status; /* status of command execution */ long error; /* error code after command execution */ struct timeval time; /* time at server when command was executed */ } DevCallbackData; void callback (devserver ds, void *user_data, DevCallbackData cb_data); long dev_putget_asyn (ds,cmd,argin,argin_type,argout,argout_type, callback, user_data, asynch_id, error) devserver ds; /* client handle */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* type of input arguments */ DevArgument argout; /* pointer to output arguments */ DevType argout_type; /* type of output arguments */ DevCallbackFunction *callback; /* pointer to callback function */ void *user_data; /* pointer to user data to pass to callback */ long *asynch_id; /* asynchronous id returned by call */ long *error; /* error */Dev_putget_asyn() executes a command asynchronously on the device associated with the passed client handle. The device must be remote and compiled with V6. Input and output data types must correspond to the types specified for this command in the device server's command list. Otherwise an error code will be returned. All arguments have to be passed as pointers.
The client continues immediately and does not wait for the server to execute the request. The callback function has to be specified otherwise an error will be returned. The callback function is triggered by making a call to dev_synch(). The client can pass data to the callback function via user_data. The callback function receives the device server handle, user data and a DevCallbackData structure as input. The function returns a (unique) id in asynch_id for each call.
long dev_synch (struct timeval *timeout, long *error);This calls checks to see if any asynchronous replies are pending. If so it triggers the associated callback routines. The call will wait for a maximum of timeout time before returning if no replies are received otherwise it returns immediately after unpacking all received replies. A timeout of zero means check to see if any replies are pending otherwise returing immediately.
long dev_cmd (ds, cmd, argin, argin_type, argout, argout_type, error) DevServer ds; /* object pointer */ long cmd; /* command */ DevArgument argin; /* pointer to input arguments */ long argin_type; /* type of input arguments */ DevArgument argout; /* pointer to output arguments */ long argout_type; /* type of output arguments */ long *error; /* error */Dev_cmd executes a command on a given object locally in a device server. Memory freeing must be done with free() and not with dev_xdrfree().
With the extended functionality of dev_putget and
dev_put the function should be used only to access
objects which are not exported.
To access internal exported devices the unified interface must be
used, to avoid access and security problems in the coming releases.
long ds__create (name, ds_class, ds_ptr, error) char *name; /* device name */ DevServerClass ds_class; /* class of the object */ DevServer *ds_ptr; /* returned pointer to the object */ long *error; /* error */Ds__create() creates a new device server object of the class ds_class and will return a pointer on the object. Before creating the object (DevMethodCreate : obj_create(3x)) the class and all its superclasses are checked to see if they have been initialised. If not, then the DevMethodClassInitialise (class_init(3x)) is called for each uninitialised class.
long ds__destroy (ds, error) DevServer ds; /* object pointer */ long *error; /* error */Ds__destroy() searches for a destroy method (DevMethodDestroy) in the object class. If no destroy method is implemented in the object class, its superclasses are searched. Arriving at the end of the class tree, the destroy method of the general device server class will be executed.
The general destroy method will free the object correctly only, if
no memory allocation was done for object fields outside the
DevServerPart structure of the object. The device name, as a field of
DevServerPart will be freed correctly bye the general device server class
destroy method.
Also exported objects can be destroyed. They will be deleted from the
list of exported devices and all client accesses will be stopped.
long dev_export (name, ds, error) char *name; /* device name */ DevServer ds; /* object pointer */ long *error; /* error */Dev_export makes devices visible for device server clients. All necessary connection information for a dev_import() call will be stored in a database table. Moreover the exported devices are added to the device server's global list of exported devices. Dev_export is installed as a method in the DeviceServerClass and accessible by the name DevMethodDevExport.
DevMethodFunction ds__method_finder (ds, method) DevServer ds; /* */ DevMethod method; /* */Ds__method_finder() searches for a method in the class hierarchy of the object ds and returns a pointer to the method function. If the method was not found in the object`s class, the search continues in all its superclasses up to the general device server class.
If the method is not implemented the method finder takes DRASTIC action and exits. This has been included in the specification to guarantee that on returning from the method finder the method can be directly executed.
long ds__method_search (ds_class, method, function_ptr) DevServerClass ds_class; /* class pointer */ DevMethod method; /* method to search for */ DevMethodFunction *function_ptr; /* returned pointer to the method function */Ds__method_search() searches for a method in the class specified. It returns the pointer to the method function if the requested method was found in the class. If no such method was specified the status DS_NOTOK is returned.
long ds__svcrun (error) long *error; /* error */Ds__svcrun() supports the checking of pending RPC requests to the device server on all open sockets. If requests are available on file descriptors (sockets), the next pending request for every descriptor will be executed and ds__svcrun() will return afterwards. If no commands are pending on any descriptor ds__svcrun() should return after 10ms.
void dev_printerror_no (mode, comment, dev_errno) DevShort mode; /* indicates, how to handle the error message buffer*/ char *comment; /* comment on error */ long dev_errno; /* error */If a message service is imported, all error messages are sent to an error file, on the NETHOST, called :
NETHOST:/DSHOME/api/error/hostname_program-number NETHOST = device server system host. DSHOME = device server system directory on NETHOST. hostname = name of the host where the service is installed. prog_number = program number of the registered service.If no message service is imported, all error messages are sent to stderr and printed on the terminal.
The mode parameter indicates, how to handle the error message buffer. Single messages can only be 256 characters long. To printout longer messages, short strings can be buffered and printed later as a text.
char *dev_error_str (dev_errno) long dev_errno; /* error */Dev_error_str() returns the error string for a given error number. It first checks to see if the error is negative. If so it returns an standard error message (negative errors are not supported). Then it checks if the error is one of the kernel errors (e.g. NETHOST not defined, RPC timeout etc.) and returns a corresponding error message. Then it checks to see if a dynamic error message was returned by the last dev_put_get(), dev_put() or dev_putget_asyn() call, if so it returns this error message. If none of the above are true it searches the TACO database for the (static) error string. If an appropriate error string cannot be found in the data base, dev_error_str() returns a string, indicating the failure. dev_error_str() allocates memory for the returned error string everytime using malloc(), it is the client's responsibility to free this memory using free()11.1.
void dev_error_push (char *error_string);Dev_error_push is a server side call for generating dynamic error strings. If called by the server while executing a dev_putget() it will make a copy of the error string and transmit it back to the client. The client can recover the error string by calling dev_error_str() immediately after the return of the dev_putget() call in question. Note if a new call to dev_putget() is made the error string returned by the previous call(s) is lost. Dev_error_push() can be called multiple times to stack errors if necessary e.g. to return errors from multiple nested calls.
Dev_error_push() is available only from DSAPI version V8.18 and onwards.
void dev_printdebug (debug_bits, fmt, [a0], [a1], ....) long debug_bits; /* debug flags */ char *fmt; /* A printf(3S) like format string */ double a0, a1, ...; /* variables to be printed */Dev_printdebug sends the debug information if the specified debug_bits are set. Possible debug_bits (debug flags) are:
#define DBG_TRACE 0x1 #define DBG_ERROR 0x2 #define DBG_INTERRUPT 0x4 #define DBG_TIME 0x8 #define DBG_WAIT 0x10 #define DBG_EXCEPT 0x20 #define DBG_SYNC 0x40 #define DBG_HARDWARE 0x80 #define DBG_STARTUP 0x100 #define DBG_DEV_SVR_CLASS 0x200 #define DBG_API 0x400 #define DBG_COMMANDS 0x800 #define DBG_METHODS 0x1000 #define DBG_STARTUP 0x100 #define DBG_DEV_SVR_CLASS 0x200 #define DBG_API 0x400 #define DBG_COMMANDS 0x800 #define DBG_METHODS 0x1000 #define DBG_SEC 0x2000 #define DBG_ASYNCH 0x4000If a message service is imported, debug messages are sent to a named pipe, on the NETHOST, called :
NETHOST:/DSHOME/api/pipe/hostname_program-number NETHOST = device server system host. DSHOME = device server system directory on NETHOST. hostname = name of the host where the service is installed. prog_number = program number of the registered service.If no message service is imported, debug messages are sent to stdout and printed on the terminal.
typedef void DevVoid
typedef char DevChar
typedef char DevBoolean
typedef u_short DevUShort
typedef short DevShort
typedef u_long DevULong
typedef long DevLong
typedef float DevFloat
typedef double DevDouble
typedef char* DevString
typedef struct { long state; float value; } DevIntFloat;
typedef struct { float set; float read; } DevFloatReadPoint;
typedef struct { short state; float set; float; } DevStateFloatReadPoint;
typedef struct { long set; long read; } DevLongReadPoint;
typedef struct { double set; double read; } DevDoubleReadPoint;
struct { u_int length; <Type> *sequence} Dev<Type>VarArr;where <Type> is the required type.
The following variable length arrays are implemented as part of the DSAPI kernel types :
typedef struct { u_int length; char *sequence; } DevVarCharArray;
typedef struct { u_int length; DevString *sequence; } DevVarStringArray;
typedef struct { u_int length; u_short *sequence; } DevVarUShortArray;
typedef struct { u_int length; short *sequence; } DevVarShortArray;
typedef struct { u_int length; u_long *sequence; } DevVarULongArray;
typedef struct { u_int length; long *sequence; } DevVarLongArray;
typedef struct { u_int length; float *sequence; } DevVarFloatArray;
typedef struct { u_int length; double *sequence; } DevVarDoubleArray;
typedef struct { u_int length; DevFloatReadPoint *sequence; } DevVarFloatReadPointArray;
typedef struct { u_int length; DevStateFloatReadPoint *sequence; } DevVarStateFloatReadPointArray;
typedef struct { u_int length; DevLongReadPoint *sequence; } DevVarLongReadPointArray;
long dev_put_asyn (ds, cmd, argin, argin_type, error) devserver ds; /* client handle to the device */ long cmd; /* command to execute */ DevArgument argin; /* pointer to input arguments */ DevType argin_type; /* input argument data type */ long *error; /* error */
The general destroy method will free the object correctly only, if no memory allocation was done for object fields outside the DevServerPart structure of the object. The device name, as a field of DevServerPart will be freed correctly bye the general device server class destroy method.
Also exported objects can be destroyed. They will be deleted from the list of exported devices and all client accesses will be stopped.
long ds__destroy (ds, error) DevServer ds; /* Pointer to the object */ long *error; /* error */
Attention:
To destroy an exported object, ds__destroy() must be used. Executing
only the destroy method will not delete the device from the list
of exported devices. With the next client access a nice core will
be generated.
Attention:
This unified interface for device access works on all exported
devices. Objects which are not exported, can be accessed only be
dev_cmd().
To access process internal devices the unified interface must be
used to avoid access and security problems in the coming releases.
typedef struct _DevServerSec { long security_key; long access_right; long single_user_flag; } DevServerSec; typedef struct _DevServerAccess { DevServer ds; char export_name[80]; long export_status; long export_counter; long single_user_flag; long max_no_of_clients; DevServerSec *client_access; } DevServerDevices; DevServerDevices *devices /* Exported devices; in DevServer.c */
are no longer static arrays. The are allocated dynamically in data blocks. The BLOCK_SIZE is defined in ApiP.h and set to 5 structures per data block. To avoid the growth of a device server, all client connections should be freed correctly.
| 31 | 30 20 | 19 12 | 11 0 | ------------------------------------------------------------- | | | | | | | |- Position in the | | | list of exported | | | devices. | | | | | |- Position in the | | list of client | | connections to the | | device. | | | |- Export counter | |- Local access flag
The export counter field becomes interesting only if you destroy
an exported object and reexport another or the same object again.
In the case of a destroyed object, the export counter is increased
and all client connections on the old value are no longer valid.
A newly exported device might take the place in the list of exported
devices afterwards.
The Local access flag is set if the dev_import() detects a local
device.
The split up of the device ID limits a device server to the following
values:
Maximum number of exported devices = 4096 Maximum number of client connections per device = 256`_=
The database itself is the ndbm package which is part of the UNIX operating system. It is a file oriented database.
TACO is a distributed control system. This is also true for the static database. The C library get/store data from/into the database through a database server across the network with RPC's. This is hidden to the user and implemented in the C library functions.
To identify every device server instance, a device server is started with a personal name which is different for each instance. For example, a device server for PerkinElmer vacuum pump called Perkin will be started with the personal name ID16 when it will drive pump installed on ESRF beam line ID16 and will be started with the personal name ID11 when it will drive pumps on the ESRF beam line ID11. The device list must be entered with the following format :
device server process name/personal name/device: device names listdevice is a key word allowing the software to know that it is a device list. Example:
BlValves/ID10/device: ID10/rv/1, ID10/rv/2 \ ID10/rv/3In this case, the device server process name is BlValves, the personal name is ID10 and it drives three devices. The device server must be started on the command line as BlValves ID10.
In the device list, each device name must be separated by a comma. If the list continue on the next line, use the character at the end of the line. All devices driven by the same device server must be defined in only one device list.
A device name must not have more than 23 characters with a family and member name limited to 19 characters. A device server process name is limited to 23 characters and the personal name to 11 characters.
device name/resource name: resource valueExample
sy/ps-b/1/fbus_channel: 2 sy/ps-b/1/upper_limit: 456.5 sy/ps-b/1/fbus_desc: fb0 sy/ps-b/1/error_str: "G64 crate out of order" sy/ps-b/1/linear_coeff: 8.123, 9.18, 10.78 \ 7.32, 101.78, 27.2Resource name must not exceed 23 characters. Resource value are stored in the database as ASCII characters and converted to the requested type when they are returned to the caller. The available types are :
It is also possible to define resources for non physical devices and to use them to configure any software. A resource definition can look like
class/tutu/titi/tata: "When will we eat?"and be retrieved by a C program. In this case, the second and third fields length is limited to 19 characters.
To delete resources from a resource file, init the resource value with the character %.
ID10/att/1/upper_limit: %will erase the resource upper_limit for the device ID10/att/1 from the database.
The SEC domain is reserved for the security aspect of the device server model. All the update, insert, delete from this domain are protected by a password.
The SYS domain is a generic domain for resources and devices which are part of the beam line control system itself (data collector resources...)
The CMDS and ERROR domain are used to store error messages and commands strings.
Files used by the NDBM software to keep data (two files per domain) are stored in a directory pointed to by the DBM_DIR environment variable software also needed by the database server.
Greta (Graphical Resource Editor for TAco) is the graphical interface to the TACO static database. This tool allows the user to retrieve, add, delete or update resources, to add, delete update device list for a device server, to save/load data to/from a file, to get device, server or database informations. For greta, all the informations stored into the database are splitted into three parts which are :
The Informations part of the device window contains device information like device server host, device server PID, device class... This sub-window is not editable. The Resources sub-window displays all the resources defined for the selected device and is editable. It is possible to update, delete, add device resource(s) in this sub-window. The five window main buttons are :
It is possible to open up to 10 different device windows. The device name is displayed in the window title.
The Informations part of the device window contains server informations like devices number defined for this server, device name... This sub-window is not editable. The "In charge device list" sub-window displays the list of device(s) defined for this server. This list follows the syntax described in the device list chapter. This sub-window is editable and the device list can be modified. The Resources sub-window displays all the resources belonging to each server device and is editable. It is possible to update, delete, add device resource(s) in this sub-window. The five window main buttons are :
It is possible to open up to 10 different server windows. The server name is displayed in the window title.
The Resources sub-window displays all the resources selected This sub-window is editable. It is possible to update, delete, add device resource(s) in this sub-window. The two window main buttons are :
It is possible to open up to 10 different resources windows.
Global-Informations : Display in greta main window general database informations. These informations are the number of devices defined in the database, the number of exported devices for each device's domain, the pseudo-devices number and the number of resources for each domain.
Help-On version : Display a window with the greta software release number
File-Print : Print the greta main window
File-Exit : Exit the application
A resource file is divided in two parts which are:
db_fillup <data_source>This command creates the database into memory and load it with resource files contents or with a database backup file according to the data_source parameter. This command directly access the ndbm files (not via the server) and therefore needs the DBM_DIR and DBTABLES environment variables. To hide these environment variables, this command is alittle script which set these environment variable and then, call the real command with the argument given by the user. The setting of these environment variables is done by a file called dbm_env. Example :
db_fillup 0
db_infoThis command displays the total number of devices and resources defined in the database as well as the number of devices and resources for each domain. Example :
$db_info DEVICE STATISTICS 90 devices are defined in database 84 of the defined devices are actually exported: 0 for the CLASS domain 6 for the SYS domain 0 for the ERROR domain 0 for the CMDS domain 0 for the SEC domain 78 for the ID16 domain 12 pseudo devices are defined in database RESOURCE STATISTICS 4126 resources are defined in database: 42 resources for the CLASS domain 28 resources for the SYS domain 348 resources for the ERROR domain 651 resources for the CMDS domain 0 resources for the SEC domain 3057 resources for the ID16 domain
db_read <domain name>This function displays all the data recorded in the database for a specific domain. This command directly access the ndbm files (not via the server) and therefore needs the DBM_DIR and DBTABLES environment variables. To hide these environment variables, this command is alittle script which set these environment variable and then, call the real command with the argument given by the user. The setting of these environment variables is done by a file called dbm_env. Example :
$db_read class CLASS: relayserver|id16|unittype|1|: icv196 CLASS: dc|1|host|1|: inel1 CLASS: dc|1|max_call|1|: 1000 CLASS: dc|1|36_default|1|: inel1 CLASS: dc|inel1|dev_number|1|: 100 CLASS: dc|inel1|cellar_number|1|: 50 CLASS: dc|inel1|path|1|: /users/b/dserver/system CLASS: dc|inel1|login|1|: dserver CLASS: dc|server_nb|inel1_rd|1|: 2
db_update <file>This command allows a user to load into the database all the resources and devices list defined a resource file. It will insert new resources or update already existing ones. It will also updates or insert device information. Example :
db_update FluoScreen_ID16.res
db_devres <device_name>db_devres displays all the resources belonging to a device. Example :
$ db_devres id16/att/1 block1 : ID16/att1_b/1 number_of_blocks : 3 block3 : ID16/att1_b/3 unitnumber : 1 block2 : ID16/att1_b/2 fluorscreen : NO attenuatornum : 1
db_devinfo <device_name>db_devinfo displays device (or pseudo device) information. For device, these information are the host name where the device server in charge of the device is running, the device server process identifier and the device server name. For pseudo device, it is just the PID and the host of the process which created the pseudo device. Example (for a real device) :
$ db_devinfo id16/att/1 Device id16/att/1 belongs to class : attenuatorClass It is monitored by the server : attenuator/id16 version 1 The device server process name is : attenuator This process is running on the computer : id161 with process ID : 117Example (for a pseudo device) :
$ db_devinfo id16/bidon/1 Device id16/bidon/1 is a pseudo device It is created by a process with PID : 234 running on host : inel1
db_servinfo <full device server name>This command displays the device list for a specific device server. The device server is specified by its full device server name which is the device server process name/personal name. For device server with several embedded classes, device belonging to each class wil be displayed. Example :
$ db_servinfo attenuator/id16 Device number 1 : id16/att/1 exported from host id161 The device server is part of the process : attenuator with PID : 45
db_devdel [-r] <device_name>This command delete a device (or a pseudo device) and all its resources from the database. The -r option prevents the command to also remove all the device resources. Example :
$ db_devdel id12/att/1
db_resdel <device name/resource name>This command deletes a resource from the database. Example :
$ db_resdel fe/id/10/io_word
db_servdel [-r] <full device server name>This command deletes all the device(s) belonging to a device server from the database. It also deletes all the resources belonging to these devices. The -r option prevents the command to delete resources. Example :
$ db_servdel attenuator/id16
db_servunreg <full device server name>This command unregisters all the device(s) belonging to a device server from the database. After this command, all the devices are not exported anymore. Example :
$ db_servunreg attenuator/id16
dbm_sec_passwdIt is possible to protect security data (in the SEC domain) with a password. This password will be asked for each insert/update into the SEC domain. dbm_sec_passwd is the command which allows to define or change the password.
dbm_sec_objinfo <obj_name>dbm_sec_objinfo displays security data for a given object. A object can be a domain, a family or a device.
dbm_sec_userinfo [-u user_name] [-g group_name]sec_userinfo returns all accesses specified for a user and (or) for a group.
int db_getresource (dev_name, res, res_num, error) char *dev_name; /* The device name */ Db_resource res; /* Array of res. name, type and pointer to store resource value */ unsigned int res_num; /* Resource number */ long *error; /* Error */This function retrieve resources from the database, convert them to the desired type and store them at the right place.
int db_putresource (dev_name, res, res_num, error) char *dev_name; /* The device name */ db_resource *res; /* Array of res. name, type and pointer to resource value */ unsigned int res_num; /* Resource number */ long *error; /* Error */This function update already defined resource(s) or add new resource(s) if it (they) does not exist. Resource files are not updated by this function. It is not possible to update/insert resource belonging to the SEC domain.
int db_delresource (dev_name, res_name, res_num, error) char *dev_name; /* The device name */ char **res_name; /* Resource name(s) to be deleted. */ unsigned int res_num; /* Resource number */ long *error; /* Error */db_delresource allows a user to remove resources from the database. The resource file where the resource was initially defined is not updated. It is not possible to delete resource(s) from the SEC domain with this function.
int db_getdevexp (filter, tab, dev_num, error) char *filter; /* The filter to select exported devices */ char ***tab; /* Exported devices name */ unsigned int *dev_num; /* Exported devices number */ long *error; /* Error */This function allows a user to get the name of exported (and then ready to accept command) devices. With the filter parameter, it is possible to limit the devices name returned by the function. This function is not available for OS-9 client.
int db_freedevexp (ptr) char **ptr; /* Exported devices name array*/The previous function can return a lot of device names and allocate memory to store them. This call is a local call and frees all the memory allocated by the db_getdevexp function.
int db_getdevlist (ds_full_name, dev_tab, dev_num, error) char *ds_full_name; /* Full device server name (device server process name/personal name) */ char ***dev_tab; /* Device name(s) array */ unsigned int *dev_num; /* Device number */ long *error; /* Error */db_getdevlist returns to the caller the devices list for the device server with the full device server name ds_full_name.
int db_dev_import (name, tab, dev_num, error) char **name; /* Device(s) name to be imported */ Db_devinf_imp *tab; /* RPC device(s) parameters array */ unsigned int dev_num; /* Device number */ long *error; /* Error */This function returns all the necessary parameters to build RPC connection between a client and the device server in charge of a device. It allows to retrieve these RPC's information for several devices at the same time.
int db_dev_export (devexp, dev_num, error) Db_devinf *tab; /* RPC device(s) parameters array */ unsigned int *dev_num; /* Device number */ long *error; /* Error */This function stores into the database the network parameters for a device or a group of devices. The network parameters are all the information needed by RPC to build a connection between a client and the device server in charge of a device.
long db_deviceinfo (dev_name, devinfo, error) char *dev_name; /* Device name */ db_devinfo_call *devinfo; /* Device informations */ long *error; /* Error */This function returns to the caller a structure with many device informations. These informations are the name of the server in charge of the device, the host where it is running, the device server program number, the device class...
long db_deviceres (dev_nb, dev_name_list, res_nb, res_list, error) long dev_nb /* Number of device */ char **dev_name_list; /* Device name list */ long res_nb; /* Number of resource(s) */ char ***res_list; /* Resource(s) list */ long *error; /* Error */This function returns to the caller the list of all resources for a list of devices. The resources are returned as string(s) with the following syntax : "device name/resource name : resource value".
long db_devicedelete (dev_name, error) char *dev_name; /* Device name */ long *error; /* Error */This function deletes a device from the list of device registered in the database.
long db_devicedeleteres (dev_nb, dev_name_list, error) long dev_nb; /* Number of device */ char **dev_name_list; /* Device name list */ db_error *error; /* Error */This function deletes all the resources belonging to a list of devices from the database.
long db_getpoller (dev_name, poll, error) char *dev_name; /* Device name */ db_poller *poll; /* Device poller info */ db_error *error; /* Error */This function returns to the caller information about the device poller in charge of a device. A poller is a process in charge of "polling" the device in order to store device command result into the TACO data collector. The poller informations are the poller name, the host where it is running,....
int db_svc_unreg (ds_full_name, error) char *ds_full_name; /* Full device server name (dev. server process name/personal name) */ long *error; /* Error */db_svc_unreg mark all the devices driven by the device server with a full name ds_full_name as not exported devices.
int db_svc_check (ds_full_name, h_name, p_num, v_num, error) char **ds_full_name; /* Full device server name (dev. server process name/personal name) */ char *h_name; /* Device server host name */ unsigned int *p_num; /* Device server program number */ unsigned int *v_num; /* Device server version number */ long *error; /* Error */This function returns host name, program number and version number of the first device found in the database for the device server with the full name ds_full_name.
long db_servinfo (ds_name, pers_name, s_info, error) char *ds_name; /* Device server name */ char *pers_name; /* Device server personal name */ db_svcinfo_call *s_info; /* Server information */ long *error; /* Error */This function returns miscellaneous informations for a device server started with a personal name. These informations are the number and name of device served by the server, the device server process name....
long db_servdelete (ds_name, pers_name, delres_flag, error) char *ds_name; /* Device server name */ char *pers_name; /* Device server personal name */ long delres_flag; /* Delete device(s) resource flag */ long *error; /* Error */This function deletes a device server from the database and if needed, all the server device resources.
long db_servunreg (ds_name, pers_name, error) char *ds_name; /* Device server name */ char *pers_name; /* Device server personal name */ long *error; /* Error */This function unregisters (mark device(s) as not exported) for all the device(s) served by the device server ds_name started with the personal name pers_name.
long db_getdevdomainlist(domain_nb, domain_list, error) long *domain_nb; /* The number of domain */ char ***domain_list; /* Domain name list */ long *error; /* Error */This function returns to the caller a list of domain used for all devices defined in the database.
long db_getdevfamilylist(domain, family_nb, family_list, error) char *domain; /* The domain name */ long *family_nb; /* The number of families */ char ***family_list; /* Family name list */ long *error; /* Error */This function returns to the caller a list of families for all devices defined in the database with the first field set to a given domain name.
long db_getdevmemberlist(domain, family, member_nb, member_list, error) char *domain; /* The domain name */ char *family; /* The famiy name */ long *member_nb; /* The number of members */ char ***member_list; /* Member name list */ long *error; /* Error */This function returns to the caller a list of members for all devices defined in the database with the first field name set to a given domain and the second field name set to a given family.
long db_getresdomainlist(domain_nb, domain_list, error) long *domain_nb; /* The number of domain */ char ***domain_list; /* Domain name list */ long *error; /* Error */This function returns to the caller a list of domain used for all resources defined in the database.
long db_getresfamilylist(domain, family_nb, family_list, error) char *domain; /* The domain name */ long *family_nb; /* The number of families */ char ***family_list; /* Family name list */ long *error; /* Error */This function returns to the caller a list of families for all resources defined in the database with the first field name set to a given domain name.
long db_getresmemberlist(domain, family, member_nb, member_list, error) char *domain; /* The domain name */ char *family; /* The famiy name */ long *member_nb; /* The number of members */ char ***member_list; /* Member name list */ long *error; /* Error */This function returns to the caller a list of members for all resources defined in the database with the first field name set to a given domain and the second field name set to a given family.
long db_getresresolist(domain, family, member, resource_nb, resource_list, error) char *domain; /* The domain name */ char *family; /* The famiy name */ char *member; /* The member name */ long *resource_nb; /* The number of members */ char ***resource_list; /* Resource name list */ long *error; /* Error */This function returns to the caller a list of resource name for all resources defined in the database for a device with a specified domain family and member field name.
long db_getresresoval(domain, family, member, resource, resval_nb, resource_list, error) char *domain; /* The domain name */ char *family; /* The famiy name */ char *member; /* The member name */ char *resource; /* The resource name */ long *resval_nb; /* The number of resource values */ char ***resource_list; /* Resource value list */ long *error; /* Error */This function returns to the caller a list of resource values for all the resource with a domain, family, member and name specified in the first four function parameters. Member and resource field name can be set to wild card (*).
long db_getdsserverlist(server_nb, server_list, error) long *server_nb; /* The number of device server */ char ***server_list; /* Server name list */ long *error; /* Error */This function returns to the caller a list of device server executable name.
long db_getdspersnamelist(server, persname_nb, persname_list, error) char *server; /* The device server executable name */ long *persname_nb; /* The number of personal name */ char ***persname_list; /* Personal name list */ long *error; /* Error */This function returns to the caller a list of device server personal name list for device server with a given executable name.
long db_gethostlist(host_nb, host_list, error) long *host_nb; /* The number of host name */ char ***host_list; /* Host name list */ long *error; /* Error */This function returns to the caller a list of hosts name where device server should run.
int db_psdev_register (psdev, num_psdev, error) db_psdev_info *psdev; /* Pseudo device parameters array */ long num_psdev; /* Pseudo devices number */ db_error *error; /* Error */This function is used to register pseudo devices into the database. This feature has been implemented only for control system debug purpose. It helps the debugger to know which process has created pseudo devices and on which computer they are running.
int db_psdev_unregister (psdev_list, num_psdev, error) char **psdev_list; /* Pseudo device(s) names list */ long num_psdev; /* Pseudo devices number */ db_error *error; /* Error */This function is used to unregister pseudo devices from the database.
long db_analyse_data (in_type, buffer, nb_devdef, devdef, nb_resdef, resdef, error_line, error) long in_type /* Buffer type (buffer or file) */ char *buffer; /* Buffer */ long *nb_devdef; /* Number of device definition list */ char ***devdef; /* Device definition list */ long *nb_resdef; /* Number of resource definition list */ char ***resdef; /* Database definition list */ long *error_line; /* Buffer line number with error */ long *error; /* Error */This function analyses a buffer (file or buffer) assuming that this buffer is used to update the database and returns device definition list and resource definition list.
long db_upddev ( nb_devdef, devdef, deferr_nb, error) long nb_devdef; /* Number of device definition list */ char **devdef; /* Device definition list */ long *deferr_nb; /* Device def. list number with error */ long *error; /* Error */This function updates the database with the new device definition defined in the device definition list.
long db_updres ( nb_resdef, resdef, deferr_nb, error) long nb_resdef; /* Number of resource definition */ char **resdef; /* Resource definition list */ long *deferr_nb; /* Resource def. number with error */ long *error; /* Error */This function updates the database with the new resource definition contained in the resource definition list.
long db_stat (info, error) db_stat_call *info; /* Database information */ long *error; /* Error */This functions returns database global informations as the number of exported devices defined in the database, the number of resources defined for each device domain...
long db_secpass (pass, error) char **pass; /* Database security password */ long *error; /* Error */The static database is also used to store security resources. A very simple system protects security resources from being updated by a user if the administrator choose to protect them. This function returns database protection data to the caller allowing an application to ask its user for security resources password.
int db_cmd_query (cmd_name, cmd_code, error) char *cmd_name; /* Command name */ unsigned int *cmd_code; /* Command code */ long *error; /* Error */The static database is also used to store (as resources) command name associated to command code (in the CMDS domain). db_cmd_query returns the command code associated to a command name.
int db_svc_close ( error) long *error; /* Error */This function asks the database server to close all the files needed to store database data (the ndbm files) allowing another process to open these files. When this function is called, no further call to database server will work until the db_svc_reopen function will be executed.
int db_svc_close ( error) long *error; /* Error */This function asks the database server to reopen database files.
The db_dev_import enables a user to retrieve necessary parameters to build RPC connections between clients and server for several devices with the same call. The TACO control system defined by the first device of the list will be used. `_=
The present implementation offers a simple model for user events which will permit device server programmers to add their own events (user events) to their code thereby providing adding value to their device servers. The present implementation is ideal for device servers which have a small number of clients. A full implementation with sophisticated system and user events which provides efficient mechanisms for distributing events to large numbers of clients will be implemented in TANGO (next generation TACO). The present implementation in TACO is simply an avant-gout of TANGO events and allows TACO programmers to gain experience using events.
This chapter presents the user event api, examples of how to program them and a discussion on performance and problems which can arise.
Events are short messages which are sent to clients asynchronously. The origin of the messages is a device server. Clients only receive messages if they have solicited them. Events are classified according to type. Event types are specific to the device server and should be defined as unique long integers. The most obvious way to do so is to use the device class unique base as offset and number events starting from 1 e.g. :
#define D_EVENT_AGPS_STATE DevAgpsBase + 1
#define D_EVENT_OMS_STATE_CHANGE DevOmsBase + 1
long dev_event_listen (devserver ds, long event_type, DevArgument argout, DevType argout_type DevCallbackFunction *callback, void *user_data, long *event_id_ptr, long *error) devserver ds - device from which client wants to receive events long event_type - type of event to receive DevArgument argout - pointer to argout data (if any) which will be sent with event DevType argout_type - argout type DevCallbackFunction *callback - pointer to callback function void *user_data - pointer to user data to pass to callback function long *event_id_ptr - pointer to event id (returned by dev_event_listen()) long *error - pointer to error code (if any)
long dev_event_unlisten (devserver ds, long event_type, long event_id, long *error) devserver ds - device from which to unregister client's interest in event long event_type - event type to unregister long event_id - event id (returned by dev_event_listen()) long *error - pointer to error code (if any)long dev_event_fire
long dev_synch (struct timeval *timeout, long *error) struct timeval *timeout - pointer to maximum time to wait while polling long *error - pointer to error code (if any)
long dev_event_fire (DevServer ds, long event_type, DevArgument argout,DevType argout_type, long event_status, long event_error) long event_type - event type to dispatch DevArgument argout - pointer to argout to dispatch with event DevType argout_type - argout type long event_status - status of event to dispatch to client long event_error - error code of event to dispatch to client (if status != DS_OK)
long dev_event_fire (Device *device, long event_type, DevArgument argout,DevType argout_type, long event_status, long event_error) long event_type - event type to dispatch DevArgument argout - pointer to argout to dispatch with event DevType argout_type - argout type long event_status - status of event to dispatch to client long event_error - error code of event to dispatch to client (if status != DS_OK)
void * events_thread(void * arg) { long event = 1; long counter=0; struct timespec t100ms; fprintf(stderr, "\nfire_events(): starting thread %s\n", (char *) arg); for (;;) { dev_event_fire(ds, event,&counter,D_LONG_TYPE,DS_OK,0); counter++; /* * sleep for 90 ms */ t100ms.tv_sec = 0; t100ms.tv_nsec = 90000000; nanosleep(&t100ms, NULL); } return NULL; } int event_thread_start() { int retcode; pthread_t th_a, th_b; void * retval; #if defined(linux) || defined(solaris) retcode = pthread_create(&th_a, NULL, fire_events, "a"); #else retcode = pthread_create(&th_a, pthread_attr_default, (pthread_startroutine_t)fire_events, (pthread_addr_t)"a"); #endif /* linux || solaris */ if (retcode != 0) fprintf(stderr, "create a failed %d\n", retcode);
The function event_thread_start() has to be called at an appropiate point in the device server e.g. during class_initialise() or object_create().
Using the example code above a number of tests were done on different platforms. The results were all roughly the same i.e. the server could generate events at regular time intervals of 100 millseconds wih a jitter of less than 10 microseconds. The jitter goes up as a function of the number of clients e.g. jitter of 25 microseconds for 10 clients on Linux/m68k. Here is an example output log from a client (Linux/x86 + Pentium) which accepts the events from a device server running on a tacobox (Linux/x86 + Pentium) and prints out their times :
counter = 3362 , server time = {924772119 s,342170 us} delta time = 99974 us counter = 3363 , server time = {924772119 s,442169 us} delta time = 99999 us counter = 3364 , server time = {924772119 s,542169 us} delta time = 100000 us counter = 3365 , server time = {924772119 s,642169 us} delta time = 100000 us counter = 3366 , server time = {924772119 s,742169 us} delta time = 100000 us counter = 3367 , server time = {924772119 s,842169 us} delta time = 100000 us counter = 3368 , server time = {924772119 s,942169 us} delta time = 100000 us counter = 3369 , server time = {924772120 s,042173 us} delta time = 100004 us counter = 3370 , server time = {924772120 s,142169 us} delta time = 99996 us counter = 3371 , server time = {924772120 s,242169 us} delta time = 100000 us counter = 3372 , server time = {924772120 s,342169 us} delta time = 100000 us counter = 3373 , server time = {924772120 s,442169 us} delta time = 100000 us counter = 3374 , server time = {924772120 s,542169 us} delta time = 100000 us
Known problems so far are that when the server or client die then HP-UX and Solaris servers and clients have difficult to detect this due to the way sockets are handled. The next release will fix this by implementing an event heartbeat which will reactivate the event channel. Failure to do so will result in the event timing out and the client being removed from the list of registered clients in the server.
DOMAIN/FAMILY/MEMBER/SIGNAL
The signal name is an extension to the device name used in the ESRF control system. To create a signal object a name with four fields must be used. This corresponds to signal naming as it is used in the history database and in general data display applications.
A special problem is the relation between read and set values. To identify all signals which can be set clearly the following naming convention must be respected. A set-point signal name must be preceded by the identifier "set-".
Example: SR/RF-FOC/TRA3-1/set-Voltage
A set-point signal can be modified and its actual value can be read.
In the case of a readable set-point value and a separate read value (as on most of the power sup- plies) the read values must keep the same signal name without the preceding identifier "set-".
Example: SR/RF-FOC/TRA3-1/Voltage
With this convention all signals which can be modified can be easily identified. Also the relation between separate read and set signals can be automatically established.
The properties of a signal object are:
To avoid the polling of several commands in the data collector, the state of a device should be also treated as a signal and should be returned as the signal "DOMAIN/FAMILY/MEMBER/State" by this command.
Command list entry:
DevReadSigValues, read_signal_values, D_VOID_TYPE, D_VAR_FLOATARR, READ_ACCESS
Command function definition:
long read_signal_values (xxx ds, DevVoid *argin, DevVarFloat Array *argout, long *error) Description: Returns the signal values of a device. Arg(s) In: None Arg(s) Out: DevVarFloatArray signal_values - Array of signal values. long *error - Pointer to error code, in case routine fails.
The properties of all signals of a class are returned as a string array. The first string (element [0]) must indicate the number of properties per signal, to have the flexibility to add new properties. The number of elements in the string array will be:
length = number of properties * number of signals + 1
The properties of the signals must be added to the string array by using the result of the method DevMethodReadProperties on the signal or multi signal object (see: the user guides of the two classes).
Command list entry:
DevReadSigConfig, read_signal_config, D_VOID_TYPE, D_VAR_STRINGARR, READ_ACCESS
Command function definition:
long read_signal_config (xxx ds, DevVoid *argin, DevVarStringArray *argout, long *error) Description: Returns the signal properties of all signals of a device. Arg(s) In: None Arg(s) Out: DevVarStringArray signal_values - Array of signal properties. long *error - Pointer to error code, in case routine fails.
The method DevMethodSignalsReset must be used on the signal or multi signal object (see: the user guides of the two classes)
Command list entry:
DevUpdatedSigConfig, update_signal_config, D_VOID_TYPE, D_VOID_TYPE, WRITE_ACCESS
Command function definition:
long update_signal_config (xxx ds, DevVoid *argin, DevVoid *argout, long *error) Description: Reinitialises all signal properties of all signals of a device with the actual resource values. Arg(s) In: None Arg(s) Out: None
Command list entry:
DevSetSigValue, set_signal_value, D_STRINGDOUBLE_TYPE, D_VOID_TYPE, WRITE_ACCESS
Command function definition:
long set_signal_value (xxx ds, DevStringDouble *argin, DevVoid *argout, long *error) Description: Receives a new value for a signal. Verifies that the value doesn`t exceed the specified range for the signal. Applies the new set-point. Arg(s) In: DevStringDouble *argin - Structure containing the name of the signal to modify as a string and the value to be applied as double. Arg(s) Out: None
To use a multi signal object it must be created and initialised in the object_initialise() method:
#include <MDSSignalP.h> #include <MDSSignal.h> /* * Create the signal objects specified for this class */ if (ds__create (ds->devserver.name, mDSSignalClass, &ds->focus.msignal_obj, error) == DS_NOTOK) { return(DS_NOTOK); } if (ds__method_finder (ds->focus.msignal_obj, DevMethodInitialise) (ds->focus.msignal_obj, focusClass->devserver_class.class_name, error) == DS_NOTOK) { return(DS_NOTOK); }
Afterwards two commands can be implemented using the multi signal object:
===================================================== Function: static long read_signal_config() Description: Read the properties of all signals specified for the focus power supply. Arg(s) In: Focus ds - pointer to object void *argin - no input arguments Arg(s) Out: DevVarStringArray *argout - Array of signal properties long *error - pointer to error code, in case routine fails ===================================================== static long read_signal_config (Focus ds, DevVoid *argin, DevVarStringArray *argout, long *error) { *error = 0; if (ds__method_finder (ds->focus.msignal_obj, DevMethodReadProperties) (ds->focus.msignal_obj, argout, error) == DS_NOTOK) { return(DS_NOTOK); } return (DS_OK); } ===================================================== Function: static long update_signal_config() Description: Reinitialises all specified signal properties with their actual resource values.. Arg(s) In: Focus ds - pointer to object void *argin - no input arguments Arg(s) Out: void *argout - no outgoing arguments long *error - pointer to error code, in case routine fails ==================================================== static long update_signal_config (Focus ds, DevVoid *argin, DevVoid *argout, long *error) { *error=0; if (ds__method_finder (ds->focus.msignal_obj, DevMethodSignalsReset) (ds->focus.msignal_obj, error) == DS_NOTOK) { return(DS_NOTOK); } return(DS_OK); }
The third command just has to return an array of values which must be ordered as the signal properties!
==================================================== Function: static long read_signal_values() Description: Read the measurement and setpoint values for this device. [0] : current setpoint [1] : voltage [2] : current Arg(s) In: Focus ds - pointer to object void *argin - no input arguments Arg(s) Out: DevVarFloatArray *argout - Array of signal values.. long *error - pointer to error code, in case routine fails ===================================================== static long read_signal_values (Focus ds, DevVoid *argin, DevVarFloatArray *argout, long *error) { static float values[3]; *error = 0; ................. -> Read the signal values here! ................. argout->length = 3; argout->sequence = &values[0]; return (DS_OK); }
The fourth command must treat all available set-points, which are identified by their name.
==================================================== Function: static long set_signal_value() Description: Receives a new value for a signal. Verifies that the value doesn`t exceed the specified range for the signal. Applies the new set-point. Arg(s) In: Focus ds - pointer to object DevStringDouble *argin - Structure containing the name of the signal to modify as a string and the value to be applied as double. Arg(s) Out: void *argout - no output arguments. long *error - pointer to error code, in case routine fails ===================================================== static long set_signal_value (Focus ds, DevStringDouble *argin, void *argout, long *error) { long limit_state; char *sig_name; *error = 0; /* * Check whether the signal name is a valid set-point signal and * whether its values are in the specified range. */ if (ds__method_finder (ds->focus.msignal_obj, DevMethodCheckLimits) (ds->focus.msignal_obj, argin, &limit_state, error) ==DS_NOTOK) { return(DS_NOTOK); } if ( limit_state != DEVRUN ) { *error = DevErr_ValueOutOfBounds; return (DS_NOTOK); } /* * Find the set-point signal amongst all available set-points and * apply the new set value. */ sig_name = strrchr (argin->name, `/`); sig_name++; if ( strcmp (sig_name, "set-Voltage") == 0 ) { ................. -> Set the value here! ................. } if ( strcmp (sig_name, "set-Current") == 0 ) { ................. -> Set the value here! ................. } return (DS_OK); }
The multi signal object is also used to handle alarms on signals which change the state of a device. The method used in the DevState command is DevMethodCheckAlarms and the method used in the DevStatus command is DevMethodReadAlarms. See the Multi Signal Class Users Guide for more information.
A second way to extract the signal names and properties of a device was developed. They are read directly from the resource database without a connection to the device. This interface is used in applications like fsigmon, devsel, hdb_config and the hdb_filler which can read data only from the data collector without having access to a device server running on a VME crate.
To use this functionality your client must be linked with the shared library: libdssig.sl
The functions were not integrated to the TACO API-library, because it uses internally the signal and multi signal classes. This would cross reference the API-library with the class library. Linking problems and Makefile changes would be the result.
Available functions are:
long dev_get_sig_config (char *device_name, DevVarStringArray *sig_config, long *error) Description: Extract the signal configuration for a device from the resource database. The result is the same as calling the command DevGetSigConfig on the device. The returned data must not be freed. Data will be freed with the next call to the function. Arg(s) In: char *device_name - Name of the device. Arg(s) Out: DevVarStringArray *sig_config - Array containing the configuration of all signals known for this device. long *error - pointer to error code, in case routine fails.
long dev_get_sig_config_from_name (char *signal_name, DevVarStringArray *sig_config, long *error) Description: Extract the signal configuration for one signal of a device from the resource database. The returned data must not be freed. Data will be freed with the next call to the function. Arg(s) In: char *device_name - Name of the device. char *signal_name - Name of the signal. Arg(s) Out: DevVarStringArray *sig_config - Array containing the configuration of the signal for this device. long *error - pointer to error code, in case routine fails.
long dev_get_sig_list (char *device_name, DevVarStringArray *sig_list, long *error) Description: Extract all signal names defined for a device. Arg(s) In: char *device_name - Name of the device. Arg(s) Out: DevVarStringArray *argout - Array containing the list of signals defined for the device. long *error - pointer to error code, in case routine fails.
long dev_get_sig_set_list (char *device_name, DevVarStringArray *argout, long *error) Description: Extract all signal names for set-points defined for a device. Signal names for set-points are pre-ceeded by the by the identifier "set-". Arg(s) In: char *device_name - Name of the device. Arg(s) Out: DevVarStringArray *sig_list - Array containing the list of signals for set-points defined for the device. long *error - pointer to error code, in case routine fails.
long dev_get_sig_setread (char *signal_name,DevLongString *set_signal, DevLongString *read_signal, long *error) Description: Returns for a given signal of a device the corresponding set-point signal and read-point signal names together with their index in the signal list of the device. The signal name entered can be either the set-point signal or the read-point signal name. If a set-point doesn`t exist for a entered signal name, a NULL pointer is returned for the signal name and the index is initialised to "-1". The same is true for a set-point signal which has no separate read-signal defined. Signal names for read-points and set-points are the same, only the set-point signal name is preceded by the identifier "set-". Arg(s) In: char *device_name - Name of the device. char *signal_name - Name of the signal. Arg(s) Out: DevLongString *set_signal - The name and the index, in the signal list, of the set-point signal. DevLongString *read_signal - The name and the index, in the signal list, of the read-point signal. long *error - pointer to error code, in case routine fails.
With the described commands, signals can be displayed in a generic way on the client side.
An example shows how DevReadSigConfig and DevReadSigValues can be used to display signals in a device server menu. The data type in this case is known and dev_cmd_query() is not used.
devserver device; DevVarStringArray sig_config; DevVarFloatArray param_array; long nu_of_properties; long nu_of_signals; long i, k; case (3) : /* * Read the device signal values. */ param_array.length = 0; param_array.sequence = NULL; if (dev_putget (device, DevReadSigValues, NULL, D_VOID_TYPE, ¶m_array, D_VAR_FLOATARR, &error) < 0) { dev_printerror_no (SEND, "DevReadSigValues", error); break; } /* * Read the signal properies to display the values. */ sig_config.length = 0; sig_config.sequence = NULL; if (dev_putget (device, DevGetSigConfig, NULL, D_VOID_TYPE, &sig_config, D_VAR_STRINGARR, &error) < 0) { dev_printerror_no (SEND, "DevGetSigConfig", error); break; } /* * Find the label format and unit for the signal values. */ nu_of_properties = atol (sig_config.sequence[0]); nu_of_signals = (sig_config.length -1) / nu_of_properties; printf ("Device parameters:\n"); for (i=0; i<nu_of_signals; i++) { sprintf (format, "%24s [%2s] : %s\n", sig_config.sequence[(i*nu_of_properties) + 2], sig_config.sequence[(i*nu_of_properties) + 3], sig_config.sequence[(i*nu_of_properties) + 4]); printf (format, param_array.sequence[i]); } /* * Free the allocated arrays. */ if ( dev_xdrfree (D_VAR_FLOATARR, ¶m_array, &error) < 0 ) { dev_printerror_no (SEND, "dev_xdrfree", error); } if ( dev_xdrfree (D_VAR_STRINGARR, &sig_config, &error) < 0 ) { dev_printerror_no (SEND, "dev_xdrfree", error); } break;
An entry point to the HDB signal library was developed to allow signal configuration in HDB with the same names as they are known in a device class. Using dev_get_sig_config() in the HDB signal library and storing the result of the command DevReadSigValues in the data collector, all signals configured for a device class (in the device server) are dynamically available in HDB with the same names and descriptions.
But, today the HDB signal library still needs for dynamic loading one module for each device class. It is just a question of copy and paste to install such a module for a device class using the signal interface, but it implies recompilation of the HDB signal library. Studies are going on to change this to avoid recompilation and reinstallation of the HDB signal library in the future.
Here is an example module for the HDB signal library. This can be copied, but the function names must be changed to the class name the new module will be used for.
#include <API.h> #include <siggen.h> /* * function prototypes */ long RF_FOCUS_load_type (long *error); long RF_FOCUS_signal_list_init (char *device_name, SigDefEntry **signal_list_ptr, long *n_signal, long *error); extern long signal_list_init (char *device_name, SigDefEntry **signal_list_ptr, long *n_signal, long *error); /* * The load type function */ long RF_FOCUS_load_type (long *error) { return (DS_OK); } /* * Dynamic signal initialisation function. * Uses signals defined on the device server level. */ long RF_FOCUS_signal_list_init (char *device_name, SigDefEntry **signal_list_ptr, long *n_signal, long *error) { /* * calls the general signal init function, which is * used for all classes which implement signals on * the device server level. */ if ( signal_list_init (device_name, signal_list_ptr, n_signal, error) == DS_NOTOK ) { return (DS_NOTOK); } return (DS_OK); }
The device server signal interface was developed for the SRRF project and was adapted mainly to the project needs. But, I see it as a useful extension to other device server classes. The advantage of using signals is that you can immediately profit from generic plotting and display programs like fsigmon and xtuning. Contact meyer@esrf.fr or pons@esrf.fr for more information on these programs.
In order to get started you need access to the following tools :
The following shared library calls have been implemented to interface TACO clients to LabView :
TACO device servers in Labview work by implementing a device server loop in a part of the G program which polls the network periodically (using the lv_ds_cmd_get() call) to see if there are any client requests. Once a request is detected the LabView program has to execute it and then return the answer to the client (using the lv_ds_cmd_put()) call. Clients and servers communicate using the TACO RPC protocol on the network. The TACO devices have to be defined beforehand in the TACO database. The following shared library calls have been implemented to write TACO device servers in G :
An additional command exists to set the debugging flag in the library in order to display debugging information in a graphical window. The command is :
All TACO kernel types and motor types are supported. The following types are supported as input and output :
The following types are supported as input only :
The following arguments are supported in output only :
4 Examples
There are examples of calling all the functions as well as examples of a TACO LabView device server (device_server.vi) and client (device_client.vi) are available. Study them to find out how to write your own clients and servers in LabView.
Some of the known problems with the TACO LabView interface are :
This is the sixth release of the LabView TACO interface. A number of improvements been made e.g. adding support for the data base, dynamic device management, increasing limit on open files. In the future we plan to offer a VI library for TACO which will reduce the programming effort on clients even more. If any readers have ideas for other improvements they should send their comments to the author (goetz@esrf.fr) or even better add them to the source code themselves and then send the code to the author !
The TACO client interface in Python is based on an object model. The commands to a device are dynamically added to the list of methods for the device. Here is an example of using the TACO client interface in Python :
file TacoDevice.py x=Device("MCD/maxe032_1/1") print x x.CommandList() x.tcp() x.udp() print x.timeout() x.timeout(2) a = x.DevReadEncPos(2) aa=array([0,1,2,3,4,5,6,7],Float32) x.DevReadMulVel(0,1,2,3,out=aa) bb=DevReadMulVel(0,1,2,3,outtype='numeric') dev_putresource("MCD/maxe032_1/1","toto","5") dev_getresource("MCD/maxe032_1_1/1","axe_ident0") dev_delresource("MCD/maxe032_1/1","toto")
In addition to the dev.command() interface the following calls are defined in the interface :
List of taco types handled by the C interface :
D_VOID_TYPE D_BOOLEAN_TYPE D_USHORT_TYPE D_SHORT_TYPE D_ULONG_TYPE D_LONG_TYPE D_FLOAT_TYPE D_DOUBLE_TYPE D_STRING_TYPE D_INT_FLOAT_TYPE D_FLOAT_READPOINT D_LONG_READPOINT D_DOUBLE_READPOINT D_MOTOR_LONG D_MOTOR_FLOAT D_STATE_FLOAT_READPOINT D_MULMOVE_TYPE D_VAR_CHARARR D_VAR_STRINGARR D_VAR_USHORTARR D_VAR_SHORTARR D_VAR_ULONGARR D_VAR_LONGARR D_VAR_FLOATARR D_VAR_DOUBLEARR D_VAR_FRPARR D_VAR_SFRPARR D_VAR_LRPARR D_OPAQUE_TYPE
For numeric types, the correspondance C to Python numeric is :
D_VAR_CHARARR Int8 D_VAR_USHORTARR Int16 D_VAR_SHORTARR Int16 D_VAR_ULONGARR Int32 D_VAR_LONGARR Int32 D_VAR_FLOATARR Float32 D_VAR_DOUBLEARR Float64
TacoServer import * class MyServer (TacoServer): "This is a test class" # Common variables for a class my_cmd_list = { DevState : [D_VOID_TYPE, D_SHORT_TYPE , 'state'], DevStatus: [D_VOID_TYPE, D_STRING_TYPE, 'status'], DevOn: [D_VOID_TYPE, D_VOID_TYPE , 'on'], DevOff: [D_VOID_TYPE, D_VOID_TYPE , 'off'], DevSetValue: [D_FLOAT_TYPE, D_VOID_TYPE , 'set'], DevSetParam: [D_VAR_FLOATARR, D_VOID_TYPE , 'set_array'], DevReadValue: [D_VOID_TYPE, D_FLOAT_TYPE, 'read'], DevReadSigValues: [D_VOID_TYPE, D_VAR_FLOATARR,'read_signals'], DevGetDevs: [D_VOID_TYPE, D_VAR_STRINGARR,'read_names'], DevSetDevs: [D_VAR_STRINGARR, D_VOID_TYPE,'set_names'] } class_name = "TestClass" value = 123.4 names = ('no', 'input') array = (1,2,3) # rem_device = Dev('id/python/test4') def __init__ (self, name): TacoServer.__init__ (self, name, command_list=self.my_cmd_list) res = dev_getresource (self.dev_name, "value") if res != None: self.value = float(res); return def state (self): # print 'remote device status:' # print self.rem_device.DevStatus() return self.dev_state def status (self): if self.dev_state == DEVUNKNOWN: self.dev_status = "The device is in an unknown state" elif self.dev_state == DEVON: self.dev_status = "The device is switched ON" elif self.dev_state == DEVOFF: self.dev_status = "The device is switched OFF" return self.dev_status def on (self): self.dev_state = DEVON def off (self): self.dev_state = DEVOFF def read (self): return self.value def set (self, x): print x if x > 100: Server.error.taco_error = DevErr_ValueOutOfBounds raise Server.error elif x < 0: x / 0 self.value = x return def read_signals (self): return self.array def read_names (self): return self.names def set_names (self, in_names): # # A copy to a new tuple is needed here!!! # self.names = in_names # will result in a memory fault when # executing read_names! # self.names = () for i in in_names: self.names = self.names + (i, ) print self.names return def set_array (self, x): # # A copy to a new tuple is needed here!!! # self.array = x # will result in a memory fault when # executing read_signals! # self.array = () for i in x: self.array = self.array + (i, ) print self.array return
And a script to create the server and start it :
# # Example script how to start a # Taco device server written in Python # import TacoServer import MyServer def start(): # # Create two device objects # x=MyServer.MyServer ('id/python/test1') y=MyServer.MyServer ('id/python/test2') # # Put the two objects to be exported # on the network in a tuple # dev=(x,y) # # Export to the network and start the # device server thread # # With a device server definition in the resource # database as: # Python/test/device: id/python/test1 \ # id/python/test2 # # TacoServer.server_startup (dev, process_name='Python', server_name='test')
from TacoServer import * class YourServer (TacoServer): "This is another test class" # Common variables for a class cmd_list = { DevState : [D_VOID_TYPE, D_SHORT_TYPE , 'state'], DevStatus: [D_VOID_TYPE, D_STRING_TYPE, 'status'], DevOpen: [D_VOID_TYPE, D_VOID_TYPE , 'open'], DevClose: [D_VOID_TYPE, D_VOID_TYPE , 'close'], DevReadSigValues: [D_VOID_TYPE, D_VAR_FLOATARR,'read_signals']} class_name = "YourTestClass" value = 123.4 def __init__ (self, name): TacoServer.__init__ (self, name) return def state (self): return self.dev_state def status (self): if self.dev_state == DEVUNKNOWN: self.dev_status = "The device is in an unknown state" elif self.dev_state == DEVOPEN: self.dev_status = "The device is Open" elif self.dev_state == DEVCLOSE: self.dev_status = "The device is Closed" return self.dev_status def open (self): self.dev_state = DEVOPEN def close (self): self.dev_state = DEVCLOSE def read_signals (self): signals = (self.dev_state, self.value) return signalsFor more information please contact the authors directly - domingue@esrf.fr and meyer@esrf.fr.
In TACO
an object can be a physical piece
of hardware, an ensemble of hardware, a logical device or a combination
of all these [1]. Objects (devices) are created,
exported and stored
in a process called a device server. Every device is exported with
a unique three field name consisting of DOMAIN/FAMILY/MEMBER
and understands a set of commands which are specific for a class
of objects in the device server.
Every exported object can be accessed via the Remote Procedure Call
(RPC) interface of the device server.
A device server client uses the Application Programmers Interface (API)
to access devices. The API is based on the file paradigm which consists
of opening the file, reading/writing to the file and then
closing the file. In the device server API paradigm these actions are
importing, accessing and freeing the device connection [1].
One problem of TACO was the open
access to devices from all over the network and by all users
on the network. Access restrictions were only possible by system
administration means, like restricted network access.
It was not possible to protect sensitive actions on devices
because, once a device was imported, all commands could be executed.
Also no possibility was given to block a device in a kind of
single user mode to do some action which required exclusive access
for a user (e.g. tuning or calibration of hardware).
To solve the above mentioned problems, a database supported security system was needed. Sufficient control over users and groups of users, which are allowed to access devices in the control system, had to be given. In order not to be dependent on machines where the control system is running, access control for networks and hosts had to be added. A list of hierarchical rights was established to specify access modes to devices. Combining a minimal access right with a command of a device, allows a protection for critical actions. A single user mode was added to give clients the possibility to be sure, that a sequence of commands on a device is not interrupted by other clients.
The solution described has been modelled on the Amoeba distributed operating system [3] capability lists and the UNIX access control lists. Development effort has gone into making the system as flexible as possible, with reconfigurable access rights at runtime and fast access verification for received RPC calls in a device server.
The access control system uses the following hierarchy to find the maximal access right, for a requesting client, in the database. The device can only be imported, if the requested access is lower or equal the maximal access right.
Client authentication happens only once during the import of the first device. For all other new connections only the device access must be verified. That requires one or two database requests. A security key is created on the client side after the import off a device. By verifying this key all parameters for the open client connection to a device can guaranteed unchanged. Nothing can be modified on the connection. Parameters necessary to check the device and command access are send to the server with every access. The parameters are checked on the server side. Sending parameters and verifying for every server access slow down the system, but is better adapted to a connectionless system and runs more reliable. Figure 3 and figure 4 show how the security key is created and how parameters are transferred.
To make database access as general as possible, the resource database was reused for security data. A specially protected table (SEC domain) was added to avoid any overwriting of data by unauthorised persons. With this solution all available database access functions of the control system could be reused. This might be not the fastest solution. One can imagine to suppress one or two database accesses by creating a new security database and security service. But a major advantage of the current solution was the very easy maintenance of a well defined interface.
The main part of the security system is part of the API library, added to the import, access and free functions. Figure 5 shows the security aspects added to the API library.
One problem remains and can only be solved by the device server programmer
himself. For example:
What does a single user mode mean for a device
which itself accesses two underlying devices in other servers?
Do these low level devices also have to be set in single user mode
or would this disturb other clients using the same low level devices?
This kind of access control over hierarchical levels can not be given
automatically. Needs might be different from case to case and requirements
are only known to the device server programmer.
The access control system can only give the tools to handle complex
access hierarchies.
The main problem for TACO security is the OS9
operating system which, in the currently used version, still requires
super-user rights to execute RPCs.
Effort still has to go into a so-called device black box. A record
should be kept of the last n commands executed on a device. This record can be
dumped or stored in a database for offline analysis. It enables diagnostics
to be carried out in the event of device failure or crash.
Security for a control system is used if the Network Manger was started with the security option:
If a device server exited and comes back to action, all clients which
had open connections will be reconnected automatically with the
device accesses they had before. During the reconnection the security
database is read again and changes are applied.
To achieve proper access control in a device server, the functions
dev_import(), dev_putget(), dev_put() and dev_free() must
be used for internal communication as described in DSN101.
A single user connection is always a TCP connection.
A died single user or administrator client will be detected on
the next access to the server and the single user lock will be
freed.
It is not possible to change the RPC protocol for a connection
if a single user mode is active.
When freeing a single user mode, the protocol on the
connection will be set back to the initial protocol.
Tools are now available to handle security resources easily.
static DevCommandListEntry commands_list[] = { {DevState, dev_read_state, D_VOID_TYPE, D_LONG_TYPE, READ_ACCESS}, {DevStatus, dev_read_status, D_VOID_TYPE, D_STRING_TYPE, READ_ACCESS}, {DevOpen, dev_open_valve, D_VOID_TYPE, D_VOID_TYPE, WRITE_ACCESS}, {DevClose, dev_close_valve, D_VOID_TYPE, D_VOID_TYPE, WRITE_ACCESS}, {DevSetCalib, dev_set_calib, D_VAR_LONGARR, D_VOID_TYPE, SU_ACCESS}, };Dangerous commands can be protected and only be executed by a client with super user rights or an administrator.
# # default access right, if no user or group entry can be # found. # SYS/MINIMAL/ACC_RIGHT/default: READ_ACCESS, 160.103.5, \ 160.103.2.132 # # user resources for the SY domain # SYS/USER/ACC_RIGHT/sy: meyer, READ_ACCESS, \ taurel, WRITE_ACCESS # # user resources for device families in the SY domain # SYS/USER/ACC_RIGHT/sy|v-rv: meyer, SU_ACCESS, \ os9, WRITE_ACCESS # # user resources for devices in the SY domain # SYS/USER/ACC_RIGHT/sy|v-rv|s9: meyer, ADMIN_ACCESS SYS/USER/ACC_RIGHT/sy|v-rv|s2: meyer, ADMIN_ACCESS # ##################################################################### # # # group resources for the SY domain # SYS/GROUP/ACC_RIGHT/sy: dserver, WRITE_ACCESS, \ os9, READ_ACCESS # # group resources for device families in the SY domain # SYS/GROUP/ACC_RIGHT/sy|v-rv: vacuum, SU_ACCESS # # group resources for devices in the SY domain # SYS/GROUP/ACC_RIGHT/sy|v-rv|s1: dserver, ADMIN_ACCESS # ##################################################################### # # user identification information # SYS/USER/IDENT/meyer: 215, 160.103.5.54, \ 160.103.2.132 SYS/USER/IDENT/taurel: 261, 160.103.2, \ 160.103.5.68 # # group identification information # SYS/GROUP/IDENT/dserver: 101, 160.103 SYS/GROUP/IDENT/vacuum: 310, 160.103.4.29 SYS/GROUP/IDENT/os9: 0, 160.103.4.218 #The resources must be stored in the SEC table of the resource database. The SEC table on libra is not protected. Everybody can try and set up some resources. To avoid the total chaos when redefining the default access or some global access on a whole domain, please put your resource files in the directory:
Specifying access control and security resources for OS9 clients, use as predefined user and group name os9 with the uid = 0 and the gid = 0. Other names are not possible, because any OS9 user must have the uid = 0 and super user rights on a crate to run a device server. The name was changed from root to os9 to avoid conflicts with the UNIX user root.
#include DevSec.h char *dev_name = "SY/V-RV/S1"; long readwrite = WRITE_ACCESS; devserver pv; long error = 0; /* * import the device */ if ( dev_import (dev_name, readwrite, &pv, &error) == DS_NOTOK ) { return (DS_NOTOK); }For Example, the requested WRITE_ACCESS was verified in the security database and granted. The client can execute all commands on the device which are specifyed with READ_ACCESS or WRITE_ACCESS in the command list of the device server. A command specified with SU_ACCESS cannot be executed.
Remember:
typedef struct _DevSecListEntry { char *access_name; long access_right; } DevSecListEntry; static DevSecListEntry DevSec_List[] = { {"NO_ACCESS", NO_ACCESS}, {"READ_ACCESS", READ_ACCESS}, {"WRITE_ACCESS", WRITE_ACCESS}, {"SI_WRITE_ACCESS", SI_WRITE_ACCESS}, {"SU_ACCESS", SU_ACCESS}, {"SI_SU_ACCESS", SI_SU_ACCESS}, {"ADMIN_ACCESS", ADMIN_ACCESS}, }; #define SEC_LIST_LENGTH (sizeof(DevSec_List)/sizeof(DevSecListEntry))
During the port of TACO to Linux it was decided to move to a more standard method for conditional Makefiles and adopt the GNU make tool. GNU make (sometimes called gmake) offers a wide range of facilities including conditional statements, it has been ported to a wide variety of platforms and is well-documented.
This chapter describes the standard way to write GNU Makefiles for building TACO source code in general and device servers in particular.
Once this philisophy is accepted there is still the choice to be made between a so-called master Makefile from which platform dependant Makefiles can be generated (using a tool like imake) or a single Makefile with conditional statements (as supported by GNU make for example) for handling platform dependancies at make time. The latter approach is the one adopted for TACO and described in this chapter.
ifdef linux CC = gcc endif
In addition there are a host of string substition and analysis functions e.g. subst, strip, findstring, filter, sort, as well as built-in expansion functions e.g. dir, suffix, basename, join, wildcard which can be used to define arg1 and arg2. Refer to chapter 8 of the manual.
These scripts can be found in /users/d/dserver/make/bin on the file server(s). gmake is also available as binary for all supported platforms and can be found in /users/d/dserver/make/bin/$OS where $OS stands for the operating system e.g. s700, solaris, sun4 (gmake is the standard make on Linux).
For those sites running TACO who support only one platforms it would be advisable to simple define the appropriate variables for that platform in the Makefile and then call gmake without any arguments.
# # RcsID = " $Header: /libra/users/d/dserver/doc/notes/DSN122/RCS/DSN122.tex,v 1.1 1997/01/15 06:18:54 goetz Exp goetz $ "; # #********************************************************************* # # File: Makefile # # Project: <PROJECT> # # Description: GNU Makefile for Template device server # # Author(s): <AUTHOR> # # Original: <DATE> # # $Log: DSN122.tex,v $ # Revision 1.1 1997/01/15 06:18:54 goetz # Initial revision # # # Copyright (c) 1996 by European Synchrotron Radiation Facility, # Grenoble, France # #********************************************************************** # GNU Makefile Generated by the Automatic Class Generation Tool, <REVISION> # <GENERATIONDATE>. # #--------------------------------------------------------------------- # This Makefile works with the GNU make (sometimes called gmake) # It makes use of the GNU make conditional statements to support # multiple platforms. To use this makefile for a particular platform # call GNU make with the appropriate symbol for that platform # defined e.g. "gmake __hp9000s700=1 unix=1 all". The following symbols # are used to identify the following platforms : # # __hp9000s700 = HPUX 9000 series 700 # _solaris = Solaris # sun = SunOS # _UCC = OS9 Fastrak Ultra-C Compiler # unix = various unix flavours (Solaris, HPUX, Lynx, Linux) # lynx = LynxOS # Linux = Linux # #-------------------------------------------------------------------- # # The variables DSHOME is passed to the Makefile # as input argument or via the environment. # # For UltraC use the settings for the environment variables: # MWOS = /usr/local/MWOS # PATH = $PATH:$MWOS/UNIX/bin/hp97k # CDEF = $MWOS/OS9/SRC/DEFS # CDEFESRF = /usr/local/os9/dd/DEFS # CLIB = $MWOS/OS9/LIB # CLIBESRF = /usr/local/os9/dd/LIB # #-------------------------------------------------------------------- # ifdef _UCC LIB_HOME = $(DSHOME)/lib/os9/ucc OBJS_HOME = $(DSHOME)/lib/os9/ucc/objs INSTALL_HOME = $(DSHOME)/bin/os9/ucc endif ifdef lynx LIB_HOME = $(DSHOME)/lib/lynxos INSTALL_HOME = $(DSHOME)/bin/lynxos endif ifdef __hp9000s700 LIB_HOME = $(DSHOME)/lib/s700 INSTALL_HOME = $(DSHOME)/bin/s700 endif ifdef sun LIB_HOME = $(DSHOME)/lib/sun4 INSTALL_HOME = $(DSHOME)/bin/sun4 endif ifdef _solaris LIB_HOME = $(DSHOME)/lib/solaris INSTALL_HOME = $(DSHOME)/bin/solaris endif ifdef linux LIB_HOME = $(DSHOME)/lib/linux INSTALL_HOME = $(DSHOME)/bin/linux endif #--------------------------------------------------------------------- # All include file and standard library pathes # # make sure to get always the new include files # under ../include # INCLDIRS = -I ../include \ -I $(DSHOME)/include \ -I $(DSHOME)/include/private #--------------------------------------------------------------------- # All necessary compiler flags for UNIX and OS9 # ifdef _UCC # The C Compiler for OS9 CC = /usr/local/MWOS/UNIX/bin/hp97k/xcc # Libraries LIBDIRS = -L $(LIB_HOME) -L $(CLIB) LFLAGS = $(LIBDIRS) \ -l dsclass \ -l dsapi \ -l dsxdr \ -l dbapi \ -l dcapi \ -l rpclib.l \ -l netdb_small.l \ -l socklib.l \ -l sys_clib.l \ -l unix.l ICODE_LFLAGS = $(LIBDIRS) \ -Wi,-l=$(LIB_HOME)/libdsapi.il \ -Wi,-l=$(LIB_HOME)/libdsxdr.il \ -Wi,-l=$(LIB_HOME)/libdbapi.il \ -Wi,-l=$(LIB_HOME)/libdcapi.il \ -l dsapi \ -l rpclib.l \ -l netdb.l \ -l socklib.l \ -l sys_clib.l # Compiler Flags with ANSI standart for OS9 CFLAGS = -mode=c89 -i -to osk -tp 020 $(INCLDIRS) ICODE_CFLAGS = -mode=c89 -i -j -O 7 -to osk -tp 020 $(INCLDIRS) NAME = -o $@ endif ifdef unix # The C Compilers for UNIX ifdef sun CC = /usr/lang/acc endif ifdef _solaris CC = /opt/SUNWspro/SC4.0/bin/cc endif ifdef lynx CC = gcc endif ifdef __hpux CC = /bin/cc endif ifdef linux CC = gcc endif # Libraries LIBDIRS = -L $(LIB_HOME) ifdef _solaris LFLAGS = $(LIBDIRS) -ldsclass -ldsapi -ldbapi -ldsxdr -ldcapi -lnsl -lsocket else LFLAGS = $(LIBDIRS) -ldsclass -ldsapi -ldbapi -ldsxdr -ldcapi -lm endif NAME = -o endif #unix # Compiler flags with ANSI standart for UNIX ifdef __hpux CFLAGS = -Aa -D_HPUX_SOURCE $(INCLDIRS) endif ifdef sun CFLAGS = -Aa $(INCLDIRS) endif ifdef _solaris CFLAGS = -Xa $(INCLDIRS) endif ifdef lynx CFLAGS = -ansi -Dlynx -Dunix -X $(INCLDIRS) endif ifdef linux CFLAGS = -ansi -Dlinux -Dunix $(INCLDIRS) endif #--------------------------------------------------------------------- # RCS options to lock and check out a version. # Or to check in a new version. # # RCS lock options RCSLOCK = co -l -r$(VERSION) # RCS check out options RCSCO = co -r$(VERSION) # RCS check in options RCSCI = ci -u -f -s"Rel" -r$(VERSION) -m"$(LOCKMSG)" #--------------------------------------------------------------------- # Class library # The object file representing the class has # to be added to the class library. # CLASS_LIB = libdsclass.a CLASS_OBJS = Template.o # #--------------------------------------------------------------------- # All Files needed for the Server and the client # # all include files INCL = TemplateP.h \ Template.h # source files SRC = Template.c \ startup.c \ ps_menu.c # object files SVC_OBJS = Template.o \ startup.o SVC_ICODE = Template.ic \ startup.ic CLN_OBJS = ps_menu.o #--------------------------------------------------------------------- # What has to be made # # Names of executables in the home directory SERVER = Templateds CLIENT = template_menu # Names of executables # and include files in the installation directories SVC_INST = $(SERVER) CLN_INST = $(CLIENT) INCL_INST = Template.h INCLP_INST = TemplateP.h #--------------------------------------------------------------------- # build server and client # ifdef _UCC # Rule for making OS-9 relocatable files .SUFFIXES: .ic .o .c .c.ic: $(CC) $(CFLAGS) -efe $< .c.o: $(CC) $(CFLAGS) -c $< all: $(SERVER) $(CLIENT) $(SERVER): $(SVC_OBJS) $(CC) $(CFLAGS) $(NAME) $(SVC_OBJS) $(LFLAGS) $(CLIENT): $(CLN_OBJS) $(CC) $(CFLAGS) $(NAME) $(CLN_OBJS) $(LFLAGS) icode: $(SVC_ICODE) echo Linking with icode libraries! $(CC) $(ICODE_CFLAGS) -o $(SERVER) $(SVC_ICODE) $(ICODE_LFLAGS) endif ifdef unix all: $(SERVER) $(CLIENT) $(SERVER): $(SVC_OBJS) $(CC) $(CFLAGS) $(NAME) $@ $(SVC_OBJS) $(LFLAGS) $(CLIENT): $(CLN_OBJS) $(CC) $(CFLAGS) $(NAME) $@ $(CLN_OBJS) $(LFLAGS) endif # Add object file representing the class # to the class library. # $(CLASS_LIB): $(CLASS_OBJS) ifdef _UCC # For os9 all object files are kept are # kept in a special directory, because # the library has to be built by a cat # of all object files. # cp $(CLASS_OBJS) $(OBJS_HOME) libgen -c $(OBJS_HOME)/?*.o -o=$(OBJS_HOME)/$(CLASS_LIB) cp $(OBJS_HOME)/$(CLASS_LIB) $(LIB_HOME) rm -rf $(OBJS_HOME)/$(CLASS_LIB) endif ifdef unix ar rv $(LIB_HOME)/$(CLASS_LIB) $(CLASS_OBJS) endif # # install executables # ifdef _UCC install: $(SERVER) $(CLIENT) $(CLASS_LIB) cp $(SERVER) $(INSTALL_HOME)/$(SVC_INST) cp $(CLIENT) $(INSTALL_HOME)/$(CLN_INST) endif ifdef unix install: $(SERVER) $(CLIENT) cp $(SERVER) $(INSTALL_HOME)/$(SVC_INST) cp $(CLIENT) $(INSTALL_HOME)/$(CLN_INST) endif # # install include files # rm -f $(DSHOME)/include/$(INCL_INST) cp ../include/$(INCL_INST) $(DSHOME)/include chmod 664 $(DSHOME)/include/$(INCL_INST) rm -f $(DSHOME)/include/private/$(INCLP_INST) cp ../include/$(INCLP_INST) $(DSHOME)/include/private chmod 664 $(DSHOME)/include/private/$(INCLP_INST) clean: -rm -f $(SVC_OBJS) -rm -f $(CLN_OBJS) -rm -f $(SVC_ICODE) -rm -f *.i clobber: clean -rm -f $(SERVER) -rm -f $(CLIENT) lock: $(RCSLOCK) $(SRC) cd ../include; $(RCSLOCK) $(INCL); cd ../src co: $(RCSCO) $(SRC) cd ../include; $(RCSCO) $(INCL); cd ../src ci: $(RCSCI) $(SRC) cd ../include; $(RCSCI) $(INCL); cd ../src
newds/test/device: id/new/1The resource file can contain other resources which are device specific. The resource file must be stored in the resource base directory (e.g. /users/d/dserver/dbase/res on libra for the test control system used at the ESRF).
# # test device for the Newds device server # newds/test/device: id/new/1 # # private commands # cmds/4/6/1: "DevNewCmd1"This is all explained in the section on "Adding Private Commands".
Test of a running device server called PneumValves started with the personal name sr_c02.
$testcs -d pneumvalves/sr_c02 DS pneumvalves/sr_c02 : UDP version 1 ==> OK DS pneumvalves/sr_c02 : TCP version 1 ==> OK DS pneumvalves/sr_c02 : UDP version 4 ==> OK DS pneumvalves/sr_c02 : TCP version 4 ==> OK $
If the device server is badly killed (with a kill -9 under UNIX or if the device server has crashed).
$testcs -d pneumvalves/sr_c02 DS pneumvalves/sr_c02 : UDP version 1 ==> NOK, leaving test DS process PID found in database : 17185 $
If the device server is nicely killed.
$testcs -d pneumvalves/sr_c02 DS pneumvalves/sr_c02 defined in database on host libra but not started $
If the device server is unregistered from the database (dbset_servunreg or dbm_servunreg command) or has never been started.
$testcs -d pneumvalves/sr_c02 Device server is not running (PN in db = 0) $
If the device server is deleted from the database (dbset_servdel or dbm_servdel command
$testcs -d pneumvalves/sr_c02 Device server not defined in database $
$ testcs -k -v Manager : UDP version 1 ==> OK Manager : UDP version 4 ==> OK Database server : UDP version 1 ==> OK Database server : UDP version 2 ==> OK Database server : UDP version 3 ==> OK Database server : TCP version 1 ==> OK Database server : TCP version 2 ==> OK Database server : TCP version 3 ==> OK Data collector read server 1 on gemini : TCP version 1 ==> OK Data collector read server 1 on gemini : UDP version 1 ==> OK Data collector read server 2 on gemini : TCP version 1 ==> OK Data collector read server 2 on gemini : UDP version 1 ==> OK Data collector read server 3 on gemini : TCP version 1 ==> OK Data collector read server 3 on gemini : UDP version 1 ==> OK Data collector read server 4 on gemini : TCP version 1 ==> OK Data collector read server 4 on gemini : UDP version 1 ==> OK Data collector read server 5 on gemini : TCP version 1 ==> OK Data collector read server 5 on gemini : UDP version 1 ==> OK Data collector write server 1 on gemini : TCP version 1 ==> OK Data collector write server 1 on gemini : UDP version 1 ==> OK Data collector write server 2 on gemini : TCP version 1 ==> OK Data collector write server 2 on gemini : UDP version 1 ==> OK Data collector write server 3 on gemini : TCP version 1 ==> OK Data collector write server 3 on gemini : UDP version 1 ==> OK Data collector write server 4 on gemini : TCP version 1 ==> OK Data collector write server 4 on gemini : UDP version 1 ==> OK Data collector read server 1 on aries : TCP version 1 ==> OK Data collector read server 1 on aries : UDP version 1 ==> OK Data collector read server 2 on aries : TCP version 1 ==> OK Data collector read server 2 on aries : UDP version 1 ==> OK Data collector read server 3 on aries : TCP version 1 ==> OK Data collector read server 3 on aries : UDP version 1 ==> OK Data collector read server 4 on aries : TCP version 1 ==> OK Data collector read server 4 on aries : UDP version 1 ==> OK Data collector read server 5 on aries : TCP version 1 ==> OK Data collector read server 5 on aries : UDP version 1 ==> OK Data collector write server 1 on aries : TCP version 1 ==> OK Data collector write server 1 on aries : UDP version 1 ==> OK Data collector write server 2 on aries : TCP version 1 ==> OK Data collector write server 2 on aries : UDP version 1 ==> OK Data collector write server 3 on aries : TCP version 1 ==> OK Data collector write server 3 on aries : UDP version 1 ==> OK Data collector write server 4 on aries : TCP version 1 ==> OK Data collector write server 4 on aries : UDP version 1 ==> OK $
$ testcs -h vme006 -v Test host : vme006 DS plc/sy_s678 and pneumvalves/sy_s678 : UDP version 1 ==> OK DS plc/sy_s678 and pneumvalves/sy_s678 : TCP version 1 ==> OK DS plc/sy_s678 and pneumvalves/sy_s678 : UDP version 4 ==> OK DS plc/sy_s678 and pneumvalves/sy_s678 : TCP version 4 ==> OK DS ripc/sy_s678 and ripc-channel/sy-s678 : UDP version 1 ==> OK DS ripc/sy_s678 and ripc-channel/sy-s678 : TCP version 1 ==> OK DS arun/sy_s678 and pg_arun/sy_s678 : UDP version 1 ==> OK DS arun/sy_s678 and pg_arun/sy_s678 : TCP version 1 ==> OK DS arun/sy_s678 and pg_arun/sy_s678 : UDP version 4 ==> OK DS arun/sy_s678 and pg_arun/sy_s678 : TCP version 4 ==> OK DS magvaccoolingilds/sy and cellmagil/sy : UDP version 1 ==> OK DS magvaccoolingilds/sy and cellmagil/sy : TCP version 1 ==> OK DS thctrl/sy and srthc/sy : UDP version 1 ==> OK DS thctrl/sy and srthc/sy : TCP version 1 ==> OK DS thctrl/sy and srthc/sy : UDP version 4 ==> OK DS thctrl/sy and srthc/sy : TCP version 4 ==> OK $On this output, you can remark that device server with several embedded classes are tested as one server (plc/sy_s678 and pneumvalves/sy_s678 are part of the same device server process). It is also possible to detect old device server which are registered in the RPC layers with version 1 only (ripc/sy_s678 and magvaccoolingilds servers).
$testcs -a Testing control system kernel components Getting information from the whole control system On large control system, this may needs time ! Getting information for : id101 Getting information for : id102 Getting information for : id106 Getting information for : tina Control system with 34 server process(s) distributed on 4 host(s) Testing device server(s) running on id101 Testing device server(s) running on id102 DS gpib/dummy and mcamb/id10 : UDP version1 ==> NOK !!!!!! DS process PID found in database : 66 DS wxbpm/mcd defined in database on host id102 but not started Testing device server(s) running on id106 Testing device server(s) running on tina DS ud_daemon/ud_atte defined in database on host tina but not started $This exmaple does not use the verbose mode of testcs. From the output, you can conclude that
Example of using dev_error_push() :
long MyClass::my_cmd(MyClass my_device, void *vargin, void *vargout, long *error); { static char error_str[256]; long argin; argin = *(long*)vargin; if (argin > my_device.maximum) { sprintf("MyClass::my_cmd(): argin = %d exceeds maximum value allowed (max=%d)\n" argin, my_device.maximum); dev_error_push(error_str); *error = DevErr_CommandFailed; return(DS_NOTOK); } . . . }
| 31 26 | 25 18 | 17 12 | 11 0 | ------------------------------------------------------------- | | | | | | | |- Error Number | | | | | |- Error category | | | |- Device Server Identification | |- Team Number
#ifndef _DserverTeams_h #define _DserverTeams_h /* * Definitions to code and decode the error and command numbers. */ #define DS_TEAM_SHIFT 26 #define DS_IDENT_SHIFT 18 #define DS_TEAM_MASK 0x3f #define DS_IDENT_MASK 0xff /************** Device server development Teams definitions **************/ #define CntrlTeamNumber (1 << DS_TEAM_SHIFT) /* CS - Machine Control */ #define DasTeamNumber (2 << DS_TEAM_SHIFT) /* CS - Data Acquisition */ #define ProgTeamNumber (3 << DS_TEAM_SHIFT) /* Experiments -Programming */ #define CrgTeamNumber (4 << DS_TEAM_SHIFT) /* External - CRG */ #define BlcTeamNumber (5 << DS_TEAM_SHIFT) /* CS - Beam Line Control */ #endif /* _DserverTeams_h */
#ifndef _DasDsNumbers_h #define _DasDsNumbers_h #include <DserverTeams.h> /* ESRF-VDL */ #define DevVdlBase DasTeamNumber + (1 << DS_IDENT_SHIFT) /* ELTEC-IC40 */ #define DevIpcBase DasTeamNumber + (2 << DS_IDENT_SHIFT) /* NOVELEC-MCCE */ #define DevMcceBase DasTeamNumber + (3 << DS_IDENT_SHIFT) /* ESRF - SKELETON */ #define DevSkelBase DasTeamNumber + (4 << DS_IDENT_SHIFT) /* LECROY 1151 - COUNTER*/ #define DevCntBase DasTeamNumber + (5 << DS_IDENT_SHIFT) /* ESRF - TDC CI022 */ #define DevTdcBase DasTeamNumber + (6 << DS_IDENT_SHIFT) /* CAEN V462 - GATEGEN */ #define DevGategenBase DasTeamNumber + (7 << DS_IDENT_SHIFT) /* ADAS ICV101 - ADC */ #define DevAdcicv101Base DasTeamNumber + (8 << DS_IDENT_SHIFT) /* EC740 TFG */ #define DevTfgBase DasTeamNumber + (9 << DS_IDENT_SHIFT) /* EC738 MCS */ #define DevMcsBase DasTeamNumber + (10 << DS_IDENT_SHIFT) /* VVHIST */ #define DevHcBase DasTeamNumber + (11 << DS_IDENT_SHIFT) /* HM - MM6326 */ #define DevHmBase DasTeamNumber + (12 << DS_IDENT_SHIFT) /* Current Transformer */ #define DevCtBase DasTeamNumber + (13 << DS_IDENT_SHIFT) #endif /* _DasDsNumbers_h */
| 31 26 | 25 18 | 17 0 | --------------------------------------------------- | | | | | |- Command Number | | | | | | | |- Device Server Identification | |- Team NumberThe distribution of Team Number and Device Server Identification is the same as described in the last section.
Define the error code:
Example:
#define DevErr_SetHighLimit DevMcceBase + 15 ERROR/2/3/15: "Unable to set polarization high limit" #define DevSetHighLimit DevMcceBase + 15 CMDS/2/3/15: "DevSetHighLimit"
All general errors and commands as they are defined in the include files DevErrors.h and DevCmds.h are loaded in the database as resources with the Team_Number = 0 and the DS_Identification = 0. Only the error messages for API and database errors are kept in a global error list.
In all versions of the API-library, starting with version 3.20, the functions dev_printerror_no(), dev_error_str() and dev_cmd_query() use error and command resource definitions. To relink older software should not cause problems, as long as these functions are used and the global lists are not directly accessed.
A description of the two error functions can be found in the man page dev_error.3x.
To recompile your old software, which might use other XDR data types as the ones mentioned in the above list, you have two possibilities.
| 31 26 | 25 18 | 17 0 | --------------------------------------------------- | | | | | |- Data Type Number | | | | | | | |- Device Server Identification | |- Team NumberThe distribution of Team Number and Device Server Identification is the same as described in section 2.
Example (ct_xdr.h):
#include <DasDsNumbers.h> /* * definitions for current transformer data type */ struct DevCtIntLifeTime { float DeltaIntensity; /* delta-intensity for this measure */ float LifeTime; /* value of the life-time */ long DateTicks; /* date in ticks since midnight */ long DeltaTused; /* delta-T used for calculations */ }; typedef struct DevCtIntLifeTime DevCtIntLifeTime; /* The declaration for the xdr function */ bool_t xdr_DevCtIntLifeTime (); /* The declaration for the xdr length calculation function */ long xdr_length_DevCtIntLifeTime (); struct DevVarCtIntLifeTimeArray { u_int length; DevCtIntLifeTime *sequence; }; typedef struct DevVarCtIntLifeTimeArray DevVarCtIntLifeTimeArray; /* The declaration for the xdr function */ bool_t xdr_DevVarCtIntLifeTimeArray (); /* The declaration for the xdr length calculation function */ long xdr_length_DevVarCtIntLifeTimeArray (); /* The definition of the data type number */ #define D_CT_LIFETIME DevCtBase + 1 /* The definition of the load macro */ #define LOAD_CT_LIFETIME(A) xdr_load_type ( D_CT_LIFETIME, \ xdr_DevVarCtIntLifeTimeArray, \ sizeof(DevVarCtIntLifeTimeArray), \ xdr_length_DevVarCtIntLifeTimeArray, \ A )
The .c file contains the XDR functions and the XDR length
calculation functions for the data type.
More information on
how to write a XDR function can be found in the HP, SUN or OS9
documentation of NFS/RPC. In addition to the standard XDR
functions, all translation functions of the defined general purpose
data types can be reused.
The XDR length calculation functions are structured in the same
way as the XDR functions. The length of each structure field has
to be summed up to find the length of the structure in XDR format.
Reusable XDR length calculation functions are
available for all defined general purpose data types.
Example (ct_xdr.c):
#include <dev_xdr.h> #include <ct_xdr.h> bool_t xdr_DevCtIntLifeTime (xdrs, objp) XDR *xdrs; DevCtIntLifeTime *objp; { if (!xdr_float(xdrs, &objp->DeltaIntensity)) { return (FALSE); } if (!xdr_float(xdrs, &objp->LifeTime)) { return (FALSE); } if (!xdr_long(xdrs, &objp->DateTicks)) { return (FALSE); } if (!xdr_long(xdrs, &objp->DeltaTused)) { return (FALSE); } return (TRUE); } long xdr_length_DevCtIntLifeTime(objp) DevCtIntLifeTime *objp; { long length = 0; length = length + xdr_length_DevFloat (&objp->DeltaIntensity); length = length + xdr_length_DevFloat (&objp->LifeTime); length = length + xdr_length_DevLong (&objp->DateTicks); length = length + xdr_length_DevLong (&objp->DeltaTused); return (length); } bool_t xdr_DevVarCtIntLifeTimeArray(xdrs, objp) XDR *xdrs; DevVarCtIntLifeTimeArray *objp; { if (!xdr_array(xdrs, (char **)&objp->sequence, (u_int *)&objp->length, ~0, sizeof(DevCtIntLifeTime), xdr_DevCtIntLifeTime)) { return (FALSE); } return (TRUE); } long xdr_length_DevVarCtIntLifeTimeArray (objp) DevVarCtIntLifeTimeArray *objp; { long length = 0; /* * four bytes for the number of array elements */ length = length + xdr_length_DevLong (&objp->length); /* * now calculate the length of the array */ length = length + (objp->length * xdr_length_DevCtIntLifeTime(&objp->sequence[0]) ); return (length); }
Example:
long *error; .............. if ( LOAD_CT_LIFETIME(error) == DS_NOTOK ) { return (DS_NOTOK); } ..............
Second, the XDR functions of the data type must be linked to server and client. This should be done locally first to test the data transfer. Afterwards the new XDR data type can be used completely local for server and client, or can be integrated to the XDR library. To make the data type visible to other clients who want to use the service.
Bit Field | Bits | Possible Numbers |
Team Number | 6 | 0 - 63 |
DS Identification | 8 | 0 - 255 |
Error Category | 6 | 0 - 63 |
Error Number | 12 | 0 - 4095 |
Command Number | 18 | 0 - 262143 |
XDR Data Type Number | 18 | 0 - 262143 |
Important files and pathes are:
Despite private definitions, the wheel should not be reinvented. Errors and commands should be reused as long as an appropriate definition can be found in the general files DevErrors.h and DevCmds.h.
Also, first try to reuse already existing XDR data types before creating new ones. In 80% of all cases the general purpose data types are sufficient.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: * a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. * b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. * c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: * a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, * b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, * c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS
This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.43)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html -split 2 TACO
The translation was initiated by Andy Goetz on 2002-03-01