The easiest way to get started writing TINE servers is to use the Server Wizard to create the skeleton to be used to provide the desired functionality. Likewise, 'buffered' servers are also very easy to put together and use, as they hide numerous details that the novice server writer either doesn't want to see or is prone to forget. Buffered servers also offer a reduced set of functionality, however.
It is nonetheless straightforward to write your own TINE server, either from scratch or beginning from a clone of a working project. To this end, we present here a brief tutorial on writing a simple TINE server from scratch.
The example below is a simple server code module which contains one equipment function and supports one property Sine. It simulates data IO by running a routine in a background task which does nothing more than update a buffer containing a sine curve (filled in upon initialization).
First some necessary #includes, #defines, and global variables:
We've assigned a global buffer sinbuf[] for keeping our sine values, and we've #defined a macro random (based on the standard library routine rand()) which we will use to simulate a bit of noise.
And for convenience, we've defined a property id for the property Sine.
The following routine (udpateSine()) fills in the array buffer passed as an argument with values representing a sine curve ranging from 0.0 to 512.0.
Below, the initialization routine sininit() does nothing (we could as well pass a NULL in the equipment module registration). The background routine Sinbkg() simulates data IO.
It updates the sine buffer by simple calling updateSine(), which generate a new sine curve with different random noise each time it is called.
Below is the equipment module, with 1 allowable property: Sine. This routine allows a caller to ask for formats other than the native float format. A check against the property being called can be made with a “strcmp()”, or as shown below, by using a a check against the hash code assigned during property registration. This is a much more efficient way of determining the requested property where there a many, many registered properties. You should assign a known property ID in a call to RegisterPropertyEx() or in the 'exports.csv' file. Then a simple call to GetPropertyId() will return to ID of the requested property which can then be used in a case switch.
The example code shown here uses putValuesFromFloat() to covert the native format (float) of the data buffer into the format requested by the caller. The 'devName' parameter is not checked against, as is the input data set 'din'. The 'access' flag is used to return an error code if WRITE access is demanded.
Below is a simple initialization routine, postSystemInit(). This registers the one equipment module and sets the minimum polling rate to 200 milliseconds (which is also the default value). This lets a client get updates at 5 Hz. Note the arguments for RegisterEquipmentModule(). The first argument NULL informs the kernel that the exported equipment module name should be obtained from an “exports.csv” startup file.
The second argument “SINEQM” says that the local 6-character equipment name is “SINEQM” (see discussion on “What’s in a Name”). The third argument 0 informs the kernel that the number of devices serviced by this equipment module is likewise to be obtained from the “exports.csv”. (Note that 0 is by definition an illegal number of devices). The fourth argument sineqm is the address of the equipment function shown above. Similarly, the fifth and sixth arguments (sininit and sinbkg) are the addresses of the initialization and background routines shown above. The seventh argument 200 says that the background task should be entered every 200 milliseconds. The last argument NULL says that there is no exit routine associated with this equipment module.
Finally, the main() routine. This registers an initialization post routine, namely the routine we named postSystemInit() in Step 4. Then it calls SystemInit(), followed by an infinite for-loop, which calls SystemCycle().
Note that SystemCycle() will in general block at the system polling rate (guaranteed not to be longer than MinPollingRate), so that the CPU is not 100 busy due to the infinite loop. The one exception is MSDOS, which does not block. Any other regular activity can be included inside this for-loop, keeping in mind that for proper performance, no other action should block the CPU for any longer than the MinPollingRate. Also note that in multi-threaded builds (which link against the multi-threaded tine libraries), an independent cycler thread can be started by calling SystemStartCycleTimer() instead of inclosing SystemCycle() within a for-loop. In such cases, main() should of course be prevented from returning until the application is no longer needed.
You can try compiling and linking the above code. You will need to have a repository containing the TINE kernel library and the TINE include files. Please see the section on 'Recipes' for your platform as to how to generate the library and suggestions as to where to keep the include files and library.
If the above code is compiled and linked with a TINE kernel library, it needs to be accompanied by the appropriate set of startup files in order to be visible to TINE clients. Keep in mind that the startup files are all CSV files (Comma Separated Value files). Such files are human-readable ascii files, which are understood by every spreadsheet application. They are also editable by your favorite editor, and that could be a problem if you forget that a comma “,” has a special meaning in a CSV file.
So when including say a list of persons responsible for a front-end server, avoid writing something like Tom, Joe, and Sally with your favorite editor. Either use a spreadsheet editor, or remember to enclose it in quotes: “Tom, Joe, and Sally”, or avoid commas altogether: Tom + Joe + Sally.
An example 'fecid.csv' file relevant to the above code might be:
And an example 'exports.csv' file relevant to the above code might be:
If the cshosts.csv startup file is also present, then the server will register itself with the name server and be immediately accessible to all TINE clients.
An alternative is to make use of a single fec.xml file containing all of the FEC and exported property information. See the section on Configuration Files for more details.
This example is intentionally simple and you can surmise how it can be extended to, for instance, offer another property to get and set the Noise (you will have to check the access parameter against CA_WRITE and obtain the value sent to the server in the din parameter).
You can also offer properties to set and read the amplitude, frequency, phase, etc. of the Sine Wave.
And you may have noticed that as written, the background task can be changing the values in the sine curve at the same time that the equipment module handler is returning the sine curve back to a caller, at least if the server is compiled as a multi-threaded server (which most are). The easiest way out of this dilemma is to call SetSystemUseDataProtection(TRUE) in the em postSystemInit() routine. This will ensure that the backround routine and the equipment module handler cannot be invoked at the same time. You are also free to make use of your own synchronization objects and double buffering techniques, etc.
Life is even easier if make use of the buffered server layer. You have a bit less control over what transpires, but in the same vein, many fewer aspects to consider. The buffered server layer is generally used in a multi-threaded mode, so you won't need to explicitly code our own cycling. You also won't need to provide an equipment module handler. In case you need to support WRITE calls (commands which change settings) you must supply a property handler. But as a minimal example, consider a buffered server attaching to the same database as above.
The entire server logic as shown below:
Here, the main() routine will Attach the server to the database via matching the local equipment module name. And by using the extended call, we provide a background task, again called updateSine() which generates a new sine curve with noise and pushes the data into the local server buffers.
A call to pushBufferedData can also schedule the property for immediate delivery by passing a TRUE as the last parameter, rather than a FALSE (as is done here).
The server remains alive until someone enters a key stroke.