Extend timestamp filter, giving access to the record timestamp
This commit is contained in:
committed by
Michael Davidsaver
parent
5eff3803a8
commit
c042b08ab0
@@ -216,22 +216,96 @@ registrar(tsInitialize)
|
||||
|
||||
=head3 TimeStamp Filter C<"ts">
|
||||
|
||||
This filter replaces the timestamp in the data fetched through the channel with
|
||||
the time the value was fetched (or an update was sent). The record's timestamp
|
||||
This filter is used for two purposes:
|
||||
|
||||
=over
|
||||
|
||||
=item * to retrieve the timestamp of the record as a value in several different
|
||||
formats;
|
||||
|
||||
=item * to retrieve the record value as normal, but replace the timestamp with
|
||||
the time the value was fetched.
|
||||
|
||||
=back
|
||||
|
||||
=head4 Parameters
|
||||
|
||||
=head4 No parameters (an empty pair of braces)
|
||||
|
||||
Retrieve the record value as normal, but replace the timestamp with the time the
|
||||
value was fetched (or an update was sent). This is useful for clients that can't
|
||||
handle timestamps that are far in the past. Normally, the record's timestamp
|
||||
indicates when the record last processed, which could have been days or even
|
||||
weeks ago for some records, or set to the EPICS epoch if the record has never
|
||||
processed.
|
||||
|
||||
=head4 Parameters
|
||||
=head4 Numeric type C<"num">
|
||||
|
||||
None, use an empty pair of braces.
|
||||
The following values are accepted for this parameter:
|
||||
|
||||
=head4 Example
|
||||
=over
|
||||
|
||||
Hal$ caget -a 'test:channel.{"ts":{}}'
|
||||
test:channel.{"ts":{}} 2012-08-28 22:10:31.192547 0 UDF INVALID
|
||||
Hal$ caget -a 'test:channel'
|
||||
test:channel <undefined> 0 UDF INVALID
|
||||
=item * C<"dbl"> requests the timestamp as C<epicsFloat64> representing the
|
||||
non-integral number of seconds since epoch. This format is convenient,
|
||||
but loses precision, depending on which epoch is used.
|
||||
|
||||
=item * C<"sec"> requests the number of seconds since epoch as C<epicsUInt32>.
|
||||
|
||||
=item * C<"nsec"> requests the number of nanoseconds since epoch as
|
||||
C<epicsUInt32>.
|
||||
|
||||
=item * C<"ts"> requests the entire timestamp. It is provided as a two-element
|
||||
array of C<epicsUInt32> representing seconds and nanoseconds.
|
||||
|
||||
=back
|
||||
|
||||
Note that C<epicsUInt32> cannot be transferred over Channel Access; in that
|
||||
case, the value will be converted to C<epicsFloat64>.
|
||||
|
||||
=head4 String type C<"str">
|
||||
|
||||
The following values are accepted for this parameter:
|
||||
|
||||
=over
|
||||
|
||||
=item * C<"epics"> requests the timestamp as a string in the format used by
|
||||
tools such as C<caget>.
|
||||
|
||||
=item * C<"iso"> requests the timestamp as a string in the ISO8601 format.
|
||||
|
||||
=back
|
||||
|
||||
=head4 Epoch adjustment C<"epoch">
|
||||
|
||||
The following values are accepted for this parameter:
|
||||
|
||||
=over
|
||||
|
||||
=item * C<"epics"> keeps the EPICS epoch (1990-01-01) and is the default if the
|
||||
C<"epoch"> parameter is not specified.
|
||||
|
||||
=item * C<"unix"> converts the timestamp to the UNIX/POSIX epoch (1970-01-01).
|
||||
|
||||
=back
|
||||
|
||||
=head4 Examples
|
||||
|
||||
Hal$ caget -a 'test:invalid_ts.{"ts":{}}'
|
||||
test:invalid_ts.{"ts":{}} 2012-08-28 22:10:31.192547 0 UDF INVALID
|
||||
Hal$ caget -a 'test:invalid_ts'
|
||||
test:invalid_ts <undefined> 0 UDF INVALID
|
||||
Hal$ caget -a test:channel
|
||||
test:channel 2021-03-11 18:23:48.265386 42
|
||||
Hal$ caget 'test:channel.{"ts": {"str": "epics"}}'
|
||||
test:channel.{"ts": {"str": "epics"}} 2021-03-11 18:23:48.265386
|
||||
Hal$ caget 'test:channel.{"ts": {"str": "iso"}}'
|
||||
test:channel.{"ts": {"str": "iso"}} 2021-03-11T18:23:48.265386+0100
|
||||
Hal$ caget -f9 'test:channel.{"ts": {"num": "dbl"}}'
|
||||
test:channel.{"ts": {"num": "dbl"}} 984331428.265386105
|
||||
Hal$ caget -f1 'test:channel.{"ts": {"num": "ts"}}'
|
||||
test:channel.{"ts": {"num": "ts"}} 2 984331428.0 265386163.0
|
||||
Hal$ caget -f1 'test:channel.{"ts": {"num": "ts", "epoch": "unix"}}'
|
||||
test:channel.{"ts": {"num": "ts", "epoch": "unix"}} 2 1615483428.0 265386163.0
|
||||
|
||||
=cut
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*************************************************************************\
|
||||
* Copyright (c) 2021 Cosylab d.d
|
||||
* Copyright (c) 2010 Brookhaven National Laboratory.
|
||||
* Copyright (c) 2010 Helmholtz-Zentrum Berlin
|
||||
* fuer Materialien und Energie GmbH.
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
/*
|
||||
* Author: Ralph Lange <Ralph.Lange@bessy.de>
|
||||
* Author: Jure Varlec <jure.varlec@cosylab.com>
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -16,20 +18,122 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "chfPlugin.h"
|
||||
#include "db_field_log.h"
|
||||
#include "dbExtractArray.h"
|
||||
#include "dbLock.h"
|
||||
#include "db_field_log.h"
|
||||
#include "epicsExport.h"
|
||||
#include "freeList.h"
|
||||
#include "cantProceed.h"
|
||||
|
||||
/*
|
||||
* The size of the data is different for each channel, and can even
|
||||
* change at runtime, so a freeList doesn't make much sense here.
|
||||
*/
|
||||
/* Allocation size for freelists */
|
||||
#define ALLOC_NUM_ELEMENTS 32
|
||||
|
||||
|
||||
/* Filter settings */
|
||||
|
||||
enum tsMode {
|
||||
tsModeInvalid = 0,
|
||||
tsModeGenerate = 1,
|
||||
tsModeDouble = 2,
|
||||
tsModeSec = 3,
|
||||
tsModeNsec = 4,
|
||||
tsModeArray = 5,
|
||||
tsModeString = 6,
|
||||
};
|
||||
|
||||
static const chfPluginEnumType ts_numeric_enum[] = {
|
||||
{"dbl", 2}, {"sec", 3}, {"nsec", 4}, {"ts", 5}};
|
||||
|
||||
enum tsEpoch {
|
||||
tsEpochEpics = 0,
|
||||
tsEpochUnix = 1,
|
||||
};
|
||||
|
||||
static const chfPluginEnumType ts_epoch_enum[] = {{"epics", 0}, {"unix", 1}};
|
||||
|
||||
enum tsString {
|
||||
tsStringInvalid = 0,
|
||||
tsStringEpics = 1,
|
||||
tsStringIso = 2,
|
||||
};
|
||||
|
||||
static const chfPluginEnumType ts_string_enum[] = {{"epics", 1}, {"iso", 2}};
|
||||
|
||||
typedef struct tsPrivate {
|
||||
enum tsMode mode;
|
||||
enum tsEpoch epoch;
|
||||
enum tsString str;
|
||||
} tsPrivate;
|
||||
|
||||
static const chfPluginArgDef ts_args[] = {
|
||||
chfEnum(tsPrivate, mode, "num", 0, 0, ts_numeric_enum),
|
||||
chfEnum(tsPrivate, epoch, "epoch", 0, 0, ts_epoch_enum),
|
||||
chfEnum(tsPrivate, str, "str", 0, 0, ts_string_enum),
|
||||
chfPluginArgEnd
|
||||
};
|
||||
|
||||
static int parse_finished(void *pvt) {
|
||||
tsPrivate *settings = (tsPrivate *)pvt;
|
||||
if (settings->str != tsStringInvalid) {
|
||||
settings->mode = tsModeString;
|
||||
#if defined _MSC_VER && _MSC_VER <= 1700
|
||||
// VS 2012 crashes in ISO mode, doesn't support timezones
|
||||
if (settings->str == tsStringIso) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
} else if (settings->mode == tsModeInvalid) {
|
||||
settings->mode = tsModeGenerate;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* Allocation of filter settings */
|
||||
static void *private_free_list;
|
||||
|
||||
static void * allocPvt() {
|
||||
return freeListCalloc(private_free_list);
|
||||
}
|
||||
|
||||
static void freePvt(void *pvt) {
|
||||
freeListFree(private_free_list, pvt);
|
||||
}
|
||||
|
||||
|
||||
/* Allocation of two-element arrays for second+nanosecond pairs */
|
||||
static void *ts_array_free_list;
|
||||
|
||||
static void *allocTsArray() {
|
||||
return freeListCalloc(ts_array_free_list);
|
||||
}
|
||||
|
||||
static void freeTsArray(db_field_log *pfl) {
|
||||
freeListFree(ts_array_free_list, pfl->u.r.field);
|
||||
}
|
||||
|
||||
/* Allocation of strings */
|
||||
static void *string_free_list;
|
||||
|
||||
static void *allocString() {
|
||||
return freeListCalloc(string_free_list);
|
||||
}
|
||||
|
||||
static void freeString(db_field_log *pfl) {
|
||||
freeListFree(string_free_list, pfl->u.r.field);
|
||||
}
|
||||
|
||||
|
||||
/* The dtor for waveform data for the case when we have to copy it. */
|
||||
static void freeArray(db_field_log *pfl) {
|
||||
/*
|
||||
* The size of the data is different for each channel, and can even
|
||||
* change at runtime, so a freeList doesn't make much sense here.
|
||||
*/
|
||||
free(pfl->u.r.field);
|
||||
}
|
||||
|
||||
static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) {
|
||||
static db_field_log* generate(void* pvt, dbChannel *chan, db_field_log *pfl) {
|
||||
epicsTimeStamp now;
|
||||
epicsTimeGetCurrent(&now);
|
||||
|
||||
@@ -44,7 +148,7 @@ static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) {
|
||||
dbScanLock(dbChannelRecord(chan));
|
||||
dbChannelGetArrayInfo(chan, &pSource, &nSource, &offset);
|
||||
dbExtractArray(pSource, pTarget, pfl->field_size,
|
||||
nSource, pfl->no_elements, offset, 1);
|
||||
nSource, pfl->no_elements, offset, 1);
|
||||
pfl->u.r.field = pTarget;
|
||||
pfl->dtor = freeArray;
|
||||
pfl->u.r.pvt = pvt;
|
||||
@@ -56,34 +160,207 @@ static db_field_log* filter(void* pvt, dbChannel *chan, db_field_log *pfl) {
|
||||
return pfl;
|
||||
}
|
||||
|
||||
static void channelRegisterPre(dbChannel *chan, void *pvt,
|
||||
chPostEventFunc **cb_out, void **arg_out, db_field_log *probe)
|
||||
{
|
||||
static db_field_log *replace_fl_value(tsPrivate const *pvt,
|
||||
db_field_log *pfl,
|
||||
void (*func)(tsPrivate const *,
|
||||
db_field_log *)) {
|
||||
/* Get rid of the old value */
|
||||
if (pfl->type == dbfl_type_ref && pfl->u.r.dtor) {
|
||||
pfl->u.r.dtor(pfl);
|
||||
}
|
||||
pfl->no_elements = 1;
|
||||
pfl->type = dbfl_type_val;
|
||||
|
||||
func(pvt, pfl);
|
||||
return pfl;
|
||||
}
|
||||
|
||||
static void ts_to_array(tsPrivate const *settings,
|
||||
epicsTimeStamp const *ts,
|
||||
epicsUInt32 arr[2]) {
|
||||
arr[0] = ts->secPastEpoch;
|
||||
arr[1] = ts->nsec;
|
||||
if (settings->epoch == tsEpochUnix) {
|
||||
/* Cannot use epicsTimeToWhatever because Whatever uses signed ints */
|
||||
arr[0] += POSIX_TIME_AT_EPICS_EPOCH;
|
||||
}
|
||||
}
|
||||
|
||||
static void ts_seconds(tsPrivate const *settings, db_field_log *pfl) {
|
||||
epicsUInt32 arr[2];
|
||||
ts_to_array(settings, &pfl->time, arr);
|
||||
pfl->field_type = DBF_ULONG;
|
||||
pfl->field_size = sizeof(epicsUInt32);
|
||||
pfl->u.v.field.dbf_ulong = arr[0];
|
||||
}
|
||||
|
||||
static void ts_nanos(tsPrivate const *settings, db_field_log *pfl) {
|
||||
epicsUInt32 arr[2];
|
||||
ts_to_array(settings, &pfl->time, arr);
|
||||
pfl->field_type = DBF_ULONG;
|
||||
pfl->field_size = sizeof(epicsUInt32);
|
||||
pfl->u.v.field.dbf_ulong = arr[1];
|
||||
}
|
||||
|
||||
static void ts_double(tsPrivate const *settings, db_field_log *pfl) {
|
||||
epicsUInt32 arr[2];
|
||||
ts_to_array(settings, &pfl->time, arr);
|
||||
pfl->field_type = DBF_DOUBLE;
|
||||
pfl->field_size = sizeof(epicsFloat64);
|
||||
pfl->u.v.field.dbf_double = arr[0] + arr[1] * 1e-9;
|
||||
}
|
||||
|
||||
static void ts_array(tsPrivate const *settings, db_field_log *pfl) {
|
||||
pfl->field_type = DBF_ULONG;
|
||||
pfl->field_size = sizeof(epicsUInt32);
|
||||
pfl->no_elements = 2;
|
||||
pfl->type = dbfl_type_ref;
|
||||
pfl->u.r.pvt = NULL;
|
||||
pfl->u.r.field = allocTsArray();
|
||||
pfl->u.r.dtor = freeTsArray;
|
||||
ts_to_array(settings, &pfl->time, (epicsUInt32*)pfl->u.r.field);
|
||||
}
|
||||
|
||||
static void ts_string(tsPrivate const *settings, db_field_log *pfl) {
|
||||
char const *fmt;
|
||||
char *field;
|
||||
size_t n;
|
||||
|
||||
pfl->field_type = DBF_STRING;
|
||||
pfl->field_size = MAX_STRING_SIZE;
|
||||
pfl->type = dbfl_type_ref;
|
||||
pfl->u.r.pvt = NULL;
|
||||
pfl->u.r.field = allocString();
|
||||
pfl->u.r.dtor = freeString;
|
||||
|
||||
switch (settings->str) {
|
||||
case tsStringEpics:
|
||||
fmt = "%Y-%m-%d %H:%M:%S.%06f";
|
||||
break;
|
||||
case tsStringIso:
|
||||
fmt = "%Y-%m-%dT%H:%M:%S.%06f%z";
|
||||
break;
|
||||
case tsStringInvalid:
|
||||
default:
|
||||
fmt = ""; // Silence compiler warning.
|
||||
cantProceed("Logic error: invalid state encountered in ts filter");
|
||||
}
|
||||
|
||||
field = (char *)pfl->u.r.field;
|
||||
n = epicsTimeToStrftime(field, MAX_STRING_SIZE, fmt, &pfl->time);
|
||||
if (!n) {
|
||||
field[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static db_field_log *filter(void *pvt, dbChannel *chan, db_field_log *pfl) {
|
||||
tsPrivate *settings = (tsPrivate *)pvt;
|
||||
|
||||
switch (settings->mode) {
|
||||
case tsModeDouble:
|
||||
return replace_fl_value(pvt, pfl, ts_double);
|
||||
case tsModeSec:
|
||||
return replace_fl_value(pvt, pfl, ts_seconds);
|
||||
case tsModeNsec:
|
||||
return replace_fl_value(pvt, pfl, ts_nanos);
|
||||
case tsModeArray:
|
||||
return replace_fl_value(pvt, pfl, ts_array);
|
||||
case tsModeString:
|
||||
return replace_fl_value(pvt, pfl, ts_string);
|
||||
case tsModeGenerate:
|
||||
case tsModeInvalid:
|
||||
cantProceed("Logic error: invalid state encountered in ts filter");
|
||||
}
|
||||
|
||||
return pfl;
|
||||
}
|
||||
|
||||
|
||||
/* Only the "generate" mode is registered for the pre-queue chain as it creates
|
||||
it's own timestamp which should be as close to the event as possible */
|
||||
static void channelRegisterPre(dbChannel * chan, void *pvt,
|
||||
chPostEventFunc **cb_out, void **arg_out,
|
||||
db_field_log *probe) {
|
||||
tsPrivate *settings = (tsPrivate *)pvt;
|
||||
*cb_out = settings->mode == tsModeGenerate ? generate : NULL;
|
||||
}
|
||||
|
||||
/* For other modes, the post-chain is fine as they only manipulate existing
|
||||
timestamps */
|
||||
static void channelRegisterPost(dbChannel *chan, void *pvt,
|
||||
chPostEventFunc **cb_out, void **arg_out,
|
||||
db_field_log *probe) {
|
||||
tsPrivate *settings = (tsPrivate *)pvt;
|
||||
|
||||
if (settings->mode == tsModeGenerate || settings->mode == tsModeInvalid) {
|
||||
*cb_out = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
*cb_out = filter;
|
||||
*arg_out = pvt;
|
||||
|
||||
/* Get rid of the value of the probe because we will be changing the
|
||||
datatype */
|
||||
if (probe->type == dbfl_type_ref && probe->u.r.dtor) {
|
||||
probe->u.r.dtor(probe);
|
||||
}
|
||||
probe->no_elements = 1;
|
||||
probe->type = dbfl_type_val;
|
||||
|
||||
switch (settings->mode) {
|
||||
case tsModeArray:
|
||||
probe->no_elements = 2;
|
||||
/* fallthrough */
|
||||
case tsModeSec:
|
||||
case tsModeNsec:
|
||||
probe->field_type = DBF_ULONG;
|
||||
probe->field_size = sizeof(epicsUInt32);
|
||||
break;
|
||||
case tsModeDouble:
|
||||
probe->field_type = DBF_DOUBLE;
|
||||
probe->field_size = sizeof(epicsFloat64);
|
||||
break;
|
||||
case tsModeString:
|
||||
probe->field_type = DBF_STRING;
|
||||
probe->field_size = MAX_STRING_SIZE;
|
||||
break;
|
||||
case tsModeGenerate:
|
||||
case tsModeInvalid:
|
||||
cantProceed("Logic error: invalid state encountered in ts filter");
|
||||
}
|
||||
}
|
||||
|
||||
static void channel_report(dbChannel *chan, void *pvt, int level, const unsigned short indent)
|
||||
{
|
||||
printf("%*sTimestamp (ts)\n", indent, "");
|
||||
tsPrivate *settings = (tsPrivate *)pvt;
|
||||
printf("%*sTimestamp (ts): mode: %d, epoch: %d, str: %d\n",
|
||||
indent, "", settings->mode, settings->epoch, settings->str);
|
||||
}
|
||||
|
||||
static chfPluginIf pif = {
|
||||
NULL, /* allocPvt, */
|
||||
NULL, /* freePvt, */
|
||||
allocPvt,
|
||||
freePvt,
|
||||
|
||||
NULL, /* parse_error, */
|
||||
NULL, /* parse_ok, */
|
||||
NULL, /* parse_error, */
|
||||
parse_finished,
|
||||
|
||||
NULL, /* channel_open, */
|
||||
channelRegisterPre,
|
||||
NULL, /* channelRegisterPost, */
|
||||
channelRegisterPost,
|
||||
channel_report,
|
||||
NULL /* channel_close */
|
||||
};
|
||||
|
||||
static void tsInitialize(void)
|
||||
{
|
||||
chfPluginRegister("ts", &pif, NULL);
|
||||
freeListInitPvt(&private_free_list, sizeof(tsPrivate),
|
||||
ALLOC_NUM_ELEMENTS);
|
||||
freeListInitPvt(&ts_array_free_list, 2 * sizeof(epicsUInt32),
|
||||
ALLOC_NUM_ELEMENTS);
|
||||
freeListInitPvt(&string_free_list, MAX_STRING_SIZE,
|
||||
ALLOC_NUM_ELEMENTS);
|
||||
chfPluginRegister("ts", &pif, ts_args);
|
||||
}
|
||||
|
||||
epicsExportRegistrar(tsInitialize);
|
||||
|
||||
Reference in New Issue
Block a user