Read With Absolute Time-Stamps

This guide assumes that you’ve read the background section about the common behavior of Readers or gone through the Read Basic Value And Domain Data how-to guide.

For brevity, in C++ we also assume that all the code is in namespace daq or it has been imported via using namespace daq; and omit any otherwise necessary header includes until the final complete listing.

In general, the openDAQ™ SDK supports any type of Signal to act as a Domain, but most often this will be an implicit Signal representing a time domain where the data will be in values called ticks. These ticks usually represent an integer counter from some starting-point called an Origin and can be converted to a time-stamp or a time-point using the information provided in the Signal’s Signal Descriptor. How this conversion is done is explained in Tick resolution and Origin.

As this is a common practice, if you want to display the Signal data or perform some calculation, it can be automated by a new kind of Reader called appropriately a Time Reader as long as the following preconditions hold:

  • The domain is "time"

  • The domain origin is specified as a string in an ISO 8601 standard format

  • The domain unit is "s" (seconds) [This restriction will be lifted in the future]

  • Tick resolution is defined

You use it by wrapping any other reader that operates on samples and can read domain data. Then you can issue the same read calls as you would to the base Reader only now the read domain data is in std::chrono::time_point values of the std::chrono::system_clock instead of ticks.

Reading time with Time Reader
  • Cpp

  • Python

auto reader = StreamReader(signal);

// Signal produces 5 samples

auto timeReader = TimeReader(reader);

SizeT count{5};
double values[5]{};
std::chrono::system_clock::time_point timeStamps[5]{};

// read with Time Reader
timeReader.readWithDomain(values, timeStamps, &count);

for (SizeT i = 0u; i < count; ++i)
{
    std::cout << timeStamps[i] << ": " << values[i] << std::endl;
}
reader = opendaq.StreamReader(signal)

# Signal produces 5 samples

time_reader = opendaq.TimeStreamReader(reader)

# read with Time Reader

values, timestamps = time_reader.read_with_timestamps(5)

print(list(zip(timestamps, values)))

The Time Reader installs a transform function for domain values on the wrapped Reader and removes it when it is destroyed. This means that performing reads on the original reader will still yield time-points as long as the Time Reader is alive.

Reading time with the wrapped reader
auto reader = StreamReader(signal);

// Signal produces 5 samples

auto timeReader = TimeReader(reader);

SizeT count{5};
double values[5]{};
std::chrono::system_clock::time_point timeStamps[5]{};

// read with the wrapped Reader
reader.readWithDomain(values, timeStamps, &count);

for (SizeT i = 0u; i < count; ++i)
{
    std::cout << timeStamps[i] << ": " << values[i] << std::endl;
}

Full listing

The following is a self-contained file with all the examples of reading domain as a system clock time-point. To properly illustrate the point and provide reproducibility, the data is manually generated, but the same should hold when connecting to a real device.

The output printed to the console in all below examples should be:

2022-09-27 00:02:03.0000000: 1
2022-09-27 00:02:03.0010000: 2
2022-09-27 00:02:03.0020000: 3
2022-09-27 00:02:03.0030000: 4
2022-09-27 00:02:03.0040000: 5

Times are in UTC.

Full listing
#include <coreobjects/unit_factory.h>
#include <opendaq/context_factory.h>
#include <opendaq/data_rule_factory.h>
#include <opendaq/packet_factory.h>
#include <opendaq/reader_factory.h>
#include <opendaq/scheduler_factory.h>
#include <opendaq/signal_factory.h>
#include <opendaq/time_reader.h>

#include <cassert>
#include <iostream>

using namespace daq;
using namespace date;

SignalConfigPtr setupExampleSignal();
SignalPtr setupExampleDomain(const SignalPtr& value);
DataPacketPtr createPacketForSignal(const SignalPtr& signal, SizeT numSamples, Int offset = 0);
daq::DataDescriptorPtr setupDescriptor(daq::SampleType type, const daq::DataRulePtr& rule = nullptr);
DataDescriptorBuilderPtr setupDescriptorBuilder(SampleType type, const daq::DataRulePtr& rule = nullptr);

/*
 * Example 1: Reading time with Time Reader
 */
void example1(const SignalConfigPtr& signal)
{
    auto reader = StreamReader(signal);

    // Signal produces 5 samples
    auto packet = createPacketForSignal(signal, 5);
    auto data = static_cast<double*>(packet.getData());
    data[0] = 1;
    data[1] = 2;
    data[2] = 3;
    data[3] = 4;
    data[4] = 5;

    signal.sendPacket(packet);

    auto timeReader = TimeReader(reader);

    SizeT count{5};
    double values[5]{};
    std::chrono::system_clock::time_point timeStamps[5]{};

    // read with Time Reader
    timeReader.readWithDomain(values, timeStamps, &count);
    assert(count == 5);

    for (SizeT i = 0u; i < count; ++i)
    {
        std::cout << timeStamps[i] << ": " << values[i] << std::endl;
        assert(values[i] == i + 1);
    }

    std::cout << std::endl;
}

/*
 * Example 2: Reading time with the wrapped reader
 */
void example2(const SignalConfigPtr& signal)
{
    auto reader = StreamReader(signal);

    // Signal produces 5 samples
    auto packet = createPacketForSignal(signal, 5);
    auto data = static_cast<double*>(packet.getData());
    data[0] = 1;
    data[1] = 2;
    data[2] = 3;
    data[3] = 4;
    data[4] = 5;
    signal.sendPacket(packet);

    auto timeReader = TimeReader(reader);

    SizeT count{5};
    double values[5]{};
    std::chrono::system_clock::time_point timeStamps[5]{};

    // read with the wrapped Reader
    reader.readWithDomain(values, timeStamps, &count);
    assert(count == 5);

    for (SizeT i = 0u; i < count; ++i)
    {
        std::cout << timeStamps[i] << ": " << values[i] << std::endl;
        assert(values[i] == i + 1);
    }
}

/*
 * ENTRY POINT
 */
int main(int /*argc*/, const char* /*argv*/ [])
{
    SignalConfigPtr signal = setupExampleSignal();
    signal.setDomainSignal(setupExampleDomain(signal));

    /*
      The output in both examples should be:

        2022-09-27 00:02:03.0000000: 1
        2022-09-27 00:02:03.0010000: 2
        2022-09-27 00:02:03.0020000: 3
        2022-09-27 00:02:03.0030000: 4
        2022-09-27 00:02:03.0040000: 5
     */

    example1(signal);
    example2(signal);

    return 0;
}

/*
 * Set up the Signal with Float64 data
 */
SignalConfigPtr setupExampleSignal()
{
    auto logger = Logger();
    auto context = Context(Scheduler(logger, 1), logger, nullptr);

    auto signal = Signal(context, nullptr, "example signal");
    signal.setDescriptor(setupDescriptor(SampleType::Float64));

    return signal;
}

SignalPtr setupExampleDomain(const SignalPtr& value)
{
    auto domainDataDescriptor = setupDescriptorBuilder(SampleTypeFromType<ClockTick>::SampleType, daq::LinearDataRule(1, 0))
                                .setOrigin("2022-09-27T00:02:03+00:00")
                                .setTickResolution(Ratio(1, 1000))
                                .setUnit(Unit("s", -1, "seconds", "time"))
                                .build();

    auto domain = Signal(value.getContext(), nullptr, "domain signal");
    domain.setDescriptor(domainDescriptor);

    return domain;
}

DataDescriptorBuilderPtr setupDescriptorBuilder(daq::SampleType type, const daq::DataRulePtr& rule)
{
    // Set up the data descriptor with the provided Sample-Type
    const auto dataDescriptor = daq::DataDescriptorBuilder().setSampleType(type);

    // For the Domain, we provide a Linear Rule to generate time-stamps
    if (rule.assigned())
        dataDescriptor.setRule(rule);

    return dataDescriptor;
}

DataDescriptorPtr setupDescriptor(daq::SampleType type, const daq::DataRulePtr& rule)
{
    return setupDescriptorBuilder(type, rule).build();
}

DataPacketPtr createPacketForSignal(const SignalPtr& signal, SizeT numSamples, Int offset)
{
    // Create a data packet where the values are generated via the +1 rule starting at 0
    auto domainPacket = daq::DataPacket(
        signal.getDomainSignal().getDescriptor(),
        numSamples,
        offset  // offset from 0 to start the sample generation at
    );

    return daq::DataPacketWithDomain(
        domainPacket,
        signal.getDescriptor(),
        numSamples
    );
}