ANSI C Based OPC UA Client/Server SDK  1.8.2.394
Oversampling Example

Most examples in the SDK are using the OpcUa_Timer functionality of the UaStack to implement sampling.

This however is not necessary. E.g. a data provider can work completely event based without polling or it can use a better timer implementation. A problem when using OpcUa_Timer is that it is not very accurate and no fast sampling rates are possible due to the fact that it is derived from the Berkeley socket select timeout.

Thus, we use POSIX timers in this example which have a resolution of nanoseconds. This example was tested to work on Linux, QNX and Windows.

Files used in this lesson:

Introduction

In this example we create a provider which allows to sample data at 1 Khz (1 ms sampling interval) using POSIX timers. To reduce network load we do not publish the values at the same rate, but allow to queue the sampled data in MonitoredItem queues and publish information at a lower publishing frequency without losing any data. This is called oversampling in OPC UA, hence the title of this example.

A client could e.g. configure these settings:

  • Sampling interval: 1ms
  • Publishing interval: 50ms
  • QueueSize: 50
  • DiscardPolicy: DiscardOldest

The following picture from the chapter OPC UA Subscription Concept shows an overview of the OPC UA sampling mechanism. You should read this chapter before continuing with this example.

subscription_model.png
OPC UA Sampling Mechanism

Note that sampling of the data in this example is done in an own thread. Thus, this example requires threading. The OPC UA SDK itself works single-threaded, but it is possible to do sampling of underlying data in a separate thread and call UaServer_NewItemValue from this thread. This requires UaServer_NewItemValue to be thread-safe which is the case if the SDK is compiled with SYNCHRONIZATION support enabled.

Creating POSIX Timers

Creating a POSIX timer consists of the following steps:

  1. Creating the Timer Object
  2. Arming the Timer

This can be done by using the two functions timer_create and timer_settime.

Creating the Timer Object

The function timer_create can be used to create a timer.

int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);

The clockid argument specifies the clock that the new timer uses to measure time. It can be chosen from one of the following values:

CLOCK_REALTIME
A settable system-wide real-time clock.
CLOCK_MONOTONIC
A nonsettable monotonically increasing clock that measures time from some unspecified point in the past that does not change after system startup.

The parameter sevp configures how the caller should be notified. There are basically four options:

SIGEV_NONE
Don’t asynchronously notify when the timer expires. Progress of the timer can be monitored using timer_gettime(2).
SIGEV_SIGNAL
Upon timer expiration, generate the signal sigev_signo for the process. See sigevent(7) for general details. The si_code field of the siginfo_t structure will be set to SI_TIMER. At any point in time, at most one signal is queued to the process for a given timer; see timer_getoverrun(2) for more details.
SIGEV_THREAD
Upon timer expiration, invoke sigev_notify_function as if it were the start function of a new thread. See sigevent(7) for details.
SIGEV_THREAD_ID (Linux-specific):
As for SIGEV_SIGNAL, but the signal is targeted at the thread whose ID is given in sigev_notify_thread_id, which must be a thread in the same process as the caller. The sigev_notify_thread_id field specifies a kernel thread ID, that is, the value returned by clone(2) or gettid(2). This flag is intended only for use by threading libraries.

On success, timer_create returns 0, and the ID of the new timer is placed in *timerid.

Arming the Timer

int timer_settime(timer_t timerid, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);

The function timer_settime arms or disarms the timer identified by timerid. The new_value argument is pointer to an itimerspec structure that specifies the new initial value and the new interval for the timer. The itimerspec structure is defined as follows:

struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};
struct itimerspec {
struct timespec it_interval; /* Timer interval */
struct timespec it_value; /* Initial expiration */
};

Each of the substructures of the itimerspec structure is a timespec structure that allows a time value to be specified in seconds and nanoseconds. These time values are measured according to the clock that was specified when the timer was created by timer_create(2)

If new_value->it_value specifies a nonzero value (i.e. either subfield is nonzero), then timer_settime() arms (starts) the timer, setting it to initially expire at the given time. (If the timer was already armed, then the previous settings are overwritten.) If new_value->it_value specifies a zero value (i.e. both subfields are zero), then the timer is disarmed.

The new_value->it_interval field specifies the period of the timer, in seconds and nanoseconds. If this field is nonzero, then each time that an armed timer expires, the timer is reloaded from the value specified in new_value->it_interval. If new_value->it_interval specifies a zero value, then the timer expires just once: at the time specified by it_value.

Sampling Thread

Polling (in this case simulating) the underlying data is done in an own thread. We want to wake this thread up periodically when the timer expires and let it sleep otherwise. For the notification mechanism SIGEV_THREAD sounds reasonable, but this would create a new thread for every notification. We don’t want the overhead of creating and deleting threads every millisecond so we go for the option SIGEV_SIGNAL which notifies our process using signals which are available on all UNIX like systems.

To wake up our thread using signals we first need to block the signal using sigprocmask, so no signal handler needs to be implemented. Instead we call the function sigwaitinfo in our thread to suspend execution until the timer expires. If the specified signal is pending, this function returns and we do our sampling.

As signal number we use SIGALRM and create our own define for it so that it is easily possible to change the used signal number when necessary.

#define SIGTIMER SIGALRM

Our sample provider creates a list of sampling rates in almost the same way as in all other examples. The difference is that the fastest rate is now 1 ms. In UaProvider_OverSampling_Subscription_Initialize() we create the POSIX timer thread and the POSIX timers necessary for sampling:

OpcUa_StatusCode UaProvider_OverSampling_Subscription_Initialize()
{
OpcUa_UInt32 i;
struct sigevent se;
struct itimerspec interval;
int ret;
sigset_t waitset;
OpcUa_InitializeStatus(OpcUa_Module_Server, "UaProvider_OverSampling_Subscription_Initialize");
#if OPCUA_USE_SYNCHRONISATION
uStatus = OpcUa_Mutex_Create(&g_mtxList);
OpcUa_GotoErrorIfBad(uStatus);
#endif
LOCK_LIST();
/* block signals processed by the timer thread */
sigemptyset( &waitset );
sigaddset( &waitset, SIGTIMER );
sigprocmask( SIG_BLOCK, &waitset, NULL );
pthread_attr_init(&g_timerthreadattr);
pthread_attr_setstacksize(&g_timerthreadattr, 1024*1024);
/* start timer thread */
pthread_create(&g_timerthreadid, &g_timerthreadattr, timer_thread, 0);
/* init vectors and start sampling timers */
for (i = 0; i < NUM_SAMPLINGRATES; i++)
{
UaBase_Vector_Initialize(&g_vecSamplingLists[i], 10, 10);
/* create sampling timer */
OpcUa_MemSet(&se, 0, sizeof(struct sigevent));
se.sigev_notify = SIGEV_SIGNAL; /* notifiy by own thread function */
se.sigev_signo = SIGTIMER;
se.sigev_value.sival_int = i; /* store sampling index */
ret = timer_create(CLOCK_MONOTONIC, &se, &g_hSamplingTimers[i]);
if (ret != 0)
{
perror("timer_create");
}
/* arm timer */
interval.it_interval.tv_sec = g_aiSamplingRates[i] / 1000;
interval.it_interval.tv_nsec = (g_aiSamplingRates[i] % 1000) * 1000000;
interval.it_value.tv_sec = interval.it_interval.tv_sec;
interval.it_value.tv_nsec = interval.it_interval.tv_nsec;
ret = timer_settime(g_hSamplingTimers[i], 0, &interval, 0);
if (ret != 0)
{
perror("timer_settime");
}
}
UNLOCK_LIST();
OpcUa_ReturnStatusCode;
OpcUa_BeginErrorHandling;
OpcUa_FinishErrorHandling;
}

The timer_thread blocks on the sigwaitinfo call and calls UaProvider_OverSampling_SampleData() when a samplig timer has expired.

void* timer_thread (void *arg)
{
UaBase_Vector *pvecList = OpcUa_Null;
sigset_t waitset;
int sig;
siginfo_t info;
int iIndex;
OpcUa_ReferenceParameter(arg);
/* block signals processed by the timer thread */
sigemptyset( &waitset );
sigaddset( &waitset, SIGTIMER );
sigprocmask( SIG_BLOCK, &waitset, NULL );
while (g_bExitTimerThread == 0)
{
sig = sigwaitinfo( &waitset, &info );
switch(sig)
{
case SIGTIMER:
iIndex = info.si_value.sival_int;
if (iIndex < 0 || iIndex >= NUM_SAMPLINGRATES) break;
pvecList = &g_vecSamplingLists[iIndex];
if (info.si_code == SI_TIMER)
{
#if TIMER_OVERRUN_TRACE
timer_t timerid;
timerid = g_hSamplingTimers[iIndex];
/* debug printf
printf("timer expired: %p\n", pvecList);
*/
if (timer_getoverrun(timerid) > 0)
{
/* if you see this output your system is not able to process the timers fast enough.
* It make your sampling routine faster or use a slower sampling rate.
* You should remove this printf from productive code.
*/
fprintf(stderr, "timer overrun\n");
}
#endif /* TIMER_OVERRUN_TRACE */
/* timer expired, sample data */
UaProvider_OverSampling_SampleData(pvecList);
}
break;
default:
/* debug info, this should not happen */
perror("sigwaitinfo");
break;
}
}
return 0;
}

POSIX Timer Accuracy

The POSIX timer API allows to configure timers at a resolution of 1 nanosecond. The accuracy of the timer depends on the operating system used and on the underlying hardware.

timespec datatype used by POSIX APIs:

struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};

Linux

On older Linux systems the accuracy of system calls that set timeouts was limited by the resolution of the software clock, a clock maintained by the kernel which measures time in jiffies. The size of a jiffy is determined by the value of the kernel constant HZ. In kernel 2.4.x HZ was 100, giving a jiffy value of 0.01 seconds. Since kernel 2.6.13 this was configurable and could be 100, 250, or 1000 Hz.

Since kernel 2.6.21 Linux supports high-resolution timers (HRTs). On a system that supports HRTs, the accuracy of sleep and timer system calls is no longer constrained by the jiffy, but instead can be as accurate as the hardware allows (microsecond accuracy is typical of modern hardware).

See http://man7.org/linux/man-pages/man7/time.7.html for more information.

Windows

On Windows timeout values in most (Win32) APIs are limited to 1 ms resolution. The accuracy is even worse. The default timer resolution is 64 Hz which allows an accuracy of 15.6 ms. There are functions like QueryPerformanceCounter which allow a more accurate measurement of time, unfortunately there is no Timer API which works with higher resolutions. One trick to work around that problem is to use the function timeBeginPeriod to increase the internal timer frequency. Using timeBeginPeriod(1) you can configure a period of 1 ms so that the timer runs at a frequency of 1000 Hz, but this comes with costs as well. The Windows timer interrupt is a global resource and ticks at one rate for the entire system. That means that a single program that raises the timer frequency affects the behavior of the entire system. Increasing this frequency also affects battery life when running on laptops.

Notes on Windows

Windows has no support for POSIX timers. To make this example working on Windows, this SDK version ships with a new third-party library pthread-win32 (not to be confused with pthreads-win32), which implements the POSIX timer API for Windows. Internally it uses the Windows API functions CreateTimerQueue, CreateTimerQueueTimer, etc., which are available since Windows XP and newer. (This API is not available on Windows CE). This library implements also a sort of fake-signals to make signal based notifications working on Windows.

You find pthread-win32 sources in the folder src/third-party/pthread-win32.

Note
This library is not part of the SDK, nor is it required. It is only used for this example to demonstrate sampling at high frequencies without using any platform specific API.