Time stamps and other data stamps are used to tag all data elements transfered throughout the control system, and are used for synchronization and correlation.
The standard timestamp in use in most control systems is Coordinated Universal Time (UTC),
Maintaining the timestamp in this format has the following advantages:
In C, C++ you might write:
... time_t timestamp; char *timestampstring; timestamp = time(); timestampstring = ctime(timestamp); printf("the current time is : %s",timestampstring); ...
which gives the following output:
"Mon Jan 10 12:08:01 2005"
In VB you might write:
Dim timestamp As Long
Dim timestampstring As String * 26
timestamp = utils.UNIXTIME
timestampstring = utils.ASCTIME(timestamp)
Label1.Caption = "Timestamp is " + timestampstring
which gives the following output:
"Mon Jan 10 12:08:01 2005"
In Java you might write:
long timestamp = System.currentTimeMillis(); String timestampstring = new Date(timestamp).toString(); System.out.println("the timestamp is : " + timestampstring);
which gives the following output:
"Mon Jan 10 12:08:01 CET 2005"
Log files should include the Time Zone in the date format, but the ansi standard calls do not do this (ctime(), asctime()), in contrast to java calls which does. Calls to feclog() will in any event make a full evaluation of the TINE timestamp to include both milliseconds and the TIME ZONE in effect.
This leads to an immediate "international settings" problem, as the TIME ZONE variables themselves are not standardized. By default, TINE uses the Central European Time signature as follows "CET" when standard time is in effect, and "CDT" when daylight time is in effect, i.e. a three letter abbreviation so as to preserve the time string length. For institutes NOT in Western Europe, these time zone strings can be set via environment variable as follows
All Data have a timestamp.
APIs:
double hwDataTimeStamp = 0; void srvBackgroundIOLoop(void) { // read hardware ... // set the data timestamp for these data: hwDataTimeStamp = getDataTimeStamp(); // etc. } ini eqm(char *devName,char * devProperty,DTYPE *dout,DTYPE *din, short access) { int devnr,prpid; // ... devnr = GetDeviceNumberEx(EQPMODNAME,devName,devProperty); if (devnr < 0) return illegal_equipment_number; prpid = GetPropertyId(EQPMODNAME,devProperty); switch (prpid) { //... case PRP_HW_ID: // return the data timestamp appropriate for these data: setDataTimeStamp(hwDataTimeStamp); if (dout->dArrayLength == 0 || dout->dArrayLength > NUMVALUES) return out_of_range; if (access&CA_WRITE && din == NULL) return illegal_read_write; if ((cc=putValuesFromFloat(dout,dbuf,NUMVALUES)) != 0) return cc; return 0; // ... } return illegal_property; }
Dim hwDataTimeStamp As Double Sub Timer1 ' read hardware ' ... ' set the data timestamp for these data: hwDataTimeStamp = Srv1.getDataTimeStamp(); ' etc. End Sub Private Sub Srv1_EqpFcn(ByVal devName As String, ByVal devProperty As String, ByVal outArrayLen As Long, ByVal inArrayLen As Long, ByVal devAccess As Integer) devNr = Srv1.EqpGetModuleNumberEx(devName, devProperty) ' ... Select Case devProperty Case "HW": ' return the data timestamp appropriate for these data: Srv1.setDataTimeStamp hwDataTimeStamp Srv1.EqpSendData dataArray Srv1.EqpSetCompletion 0, "" Exit Sub ' ... End Select End Sub
public class SineDevice extends TDevice { public static final int DATASIZE = 1024; public static final double PHASEDIFF = 2 * Math.PI / 100.0; private double frequency = 1; private double amplitude = 1; private double[] myData; private double phase = 0.0f; private double noise = 0.05; private double timestamp = 0; public double getTimestamp() { return timestamp; } public void setTimestamp(double timestamp) { this.timestamp = timestamp; } // .... other methods omitted .... public void update() { clearAlarm(512); incrementPhase(); for (int i = 0; i < 1024; i++) myData[i] = amplitude * Math.sin(phase + frequency * 6.28 * ((double) i / 1024.0)) + noise * Math.random(); if (amplitude > 100) setAlarm(512); // amplitude too high ! this.setTimestamp(TDataTime.getDataTimeStamp()); // < record the timestamp ! } } public class SineEquipmentModule extends TEquipmentModule { // .... other methods omitted .... private void registerProperties() { // all property information (except the handlers) from exports.csv file getExportInformationFromFile(); // attach the property handlers ... attachPropertyHandler("Sine",new TPropertyHandler() { protected int call(String devName, TDataType dout, TDataType din, TAccess devAccess) { return readSine(devName, dout); } }); // ... + other property handlers ... } private int readSine(String devName, TDataType dout) { SineDevice device = findDevice(devName); if (device == null) return TErrorList.device_not_connected; info[device.devNumber].numberCalls[0]++; // set the data timestamp to that when the data was taken! dout.setDataTimeStamp(device.getTimestamp()); return dout.putData(device.getSine()); } }
int linkId = -1; float sindata[100]; // asynchronous link: int startupRoutine(void) { DTYPE dout; // ... // start a data link here dout.dArrayLength = 100; dout.dFormat = CF_FLOAT; dout.data.fptr = sindata; linkId = AttachLink("/TEST/SINE/SINE_DEV_0","SINE",&dout,NULL,CA_READ,1000,showlink,CM_POLL); // etc. } void showlink(int id, int cc) { // get the data time stamp: double ts = getCurrentDataTimeStamp(id); printf("Data valid on : %s",ctime((time_t)ts)); } // synchronous call: int getSineCurve(void) { DTYPE dout; float lcldata[100]; double ts; int cc; dout.dArrayLength = 100; dout.dFormat = CF_FLOAT; dout.data.fptr = lcldata; cc = ExecLink("/TEST/SINE/SINE_DEV_0","SINE",&dout,NULL,CA_READ); ts = dout.dTimestamp; printf("Data valid on : %s",ctime((time_t)ts)); // etc. return cc; }
Dim dtimestamp As Double Dim timestampstring As String Dim linkId As Integer Dim sindata(99) As Single ' asynchronous link: Sub startupRoutine Acop1.DeviceContext = "TEST" Acop1.DeviceGroup = "SINE" Acop1.DeviceName = "SINE_DEV_0" Acop1.DeviceProperty = "SINE" linkId = Acop1.AttachLink(sindata) ' etc. End Sub Private Sub ACOP1_Receive(ByVal LinkIndex As Long, ByVal StatusCode As Long) ' get the data time stamp: dtimestamp = Acop1.GetDataTimestamp ' interpret the data time stamp: timestampstring = utils.ASCDTIME(dtimestamp) Label1.Caption = timestampstring End Sub
// ... float[100] val = new float[100]; TDataType dout = new TDataType(val); TDataType din = new TDataType(); TLink sin = new TLink("/TEST/SINE/SINEDEV_0","SINE",dout,din,TAccess.CA_READ); int cc = sin.execute(); System.out.println("data timestamp: " + new Date(dout.getDataTimeStamp()).toString()); // etc. ...
TINE data sets also carry two kinds of integer data stamps with then. The first is a "system" data stamps which is applied systematically. The second is a "user" data stamp supplied at the server side by the server at the time of the call. If never utilized, these data stamps will of course contain only the integer value '0'.
The system data stamp can be set explicitly by calling SetSystemDataStamp() (C code), or by calling the setSystemDataStamp() method of TDataType (java). In practice this value is typically set systematically, for instance by supplying a 'cycle number' from a "CYCLER" server (for those facilities with such a concept) which multicasts the cycle number as an external network global. The system data stamp for a particular data stamp can be obtained, if needed, by calling one of GetSystemDataStamp() or GetSystemDataStampFromCallbackId(), or by calling GetDataFromCallbackId() (C code), in which case a DTYPE object is filled in with all information pertaining to the incoming data set. or by calling the getSystemDataStamp() method of TDataType (java). Systematically applied data stamps (such as a cycle number or pulse number) are a good way to correlate data sets, especially as these extra data stamps can also be archived along with the data.
The device server can tag each data set with an integer value of its own choosing by setting the dStamp field in the DTYPE output object within an equipment modules dispatch handler (C code) or calling the setUserDataStamp() method of the TDataType object in java. A caller can retrieve this values by calling one of GetDataStamp(), GetDataStampFromCallbackId(), or GetDataFromCallbackId() (C code) or by calling the getUserDataStamp() method of TDataType (java).
The servers in the control system generally run in a distributed manner, meaning that they can run stand-alone are not tied to a central server. One the other hand they will make use of central services when they are available and in particular will synchronize themselves to a common time server.
It is assumed that on those operating systems where an NTP service is available (Windows NT/XP, UNIX) that the server is making use of that service and that all servers are sycnrhonizing their clocks to one single time server. Operating systems which do not offer an NTP service (DOS, Win16, VxWorks, NIOS) attempt to synchronize themselves the the central time server using an 'rdate' call built into the TINE library.
Under some circumstances, the central time server might not be available, and to this end the TINE control system offers an additional time server which multicasts the current TINE timestamp at 1 Hz. This 'global' value can be received at the server side and used to establish a data timestamp offset, which is to be appended to all data timestamps used internally. Note that this form of synchronization only synchronizes the data timestamps and does not adjust the system clock.
In addition, all servers will check for the existence of a server named "CYCLER" in its context. If such a server exists and produces a global integer quantity called "CycleNumber" then this value will be received and used to stamp all outgoing data sets, using the system data stamp field of the DTYPE object. In such a way, distributed data can be synchronized and correlated independent of the distributed clock timestamps.
1.5.8