openDAQ™ Application Quick Start guide

The openDAQ™ SDK allows users to quickly and easily set up an application that can connect to an openDAQ™-compatible device, configure it and read its data. It allows for easy signal processing and routing of signals through blocks that process and manipulate signal data.

This guide illustrates the process of configuring and connecting to Devices, using simulated Devices and Signals, requiring no prior knowledge of the openDAQ™ framework.

Full final source files of the examples featured in this guide are available at the bottom of the page, as well as within the binary packages available at the openDAQ™ documentation and releases webpage.

This guide continues from the openDAQ™ project state at the end of the Setting up guide (C++/Python), making use of the provided openDAQ™ binaries and examples.

Simulating an openDAQ™ Device

For this guide, we will be using a Device simulator that outputs synthetic sine Signals at a chosen sample rate, with a chosen amplitude and frequency. It hosts an OPC UA server and a Native streaming server, allowing clients to inspect and configure the Device, and read its data.

The simulator can be downloaded in the form of a VirtualBox image, or built from source.

Using a virtual machine

The simulator can be used in the form of a VirtualBox .ova image. The image (opendaq-version_device_simulator.ova) can be found at the openDAQ™ documentation and releases webpage, and should be run with VirtualBox 7.0.6 or newer.

Once your see the log in prompt, the simulator is already started as a service in the background and there is no need to log in.

Detailed instructions on setting up the simulator can be found here.

Building a simulator from source

  • Cpp

  • Python

  • C#

To create an application that simulates an openDAQ™ Device, we build and run the cpp/quick_start/quick_start_simulator.cpp code example. To do so, navigate to the cpp/quick_start examples folder and run the following commands:

cmake -DCMAKE_BUILD_TYPE=Release -DOPENDAQ_CREATE_DEVICE_SIMULATOR=ON -Bbuild .
cd build
cmake --build .

We start the simulated device:

# Windows
cd Release
quick_start_simulator.exe
# Linux
./quick_start_simulator

To create an application that simulates an openDAQ™ Device, navigate to python/guide and run the simulator.py script:

py simulator.py

The following code example creates a C# application that simulates an openDAQ™ Device:

using Daq.Core.OpenDAQ;

var config = CoreObjectsFactory.CreatePropertyObject();
config.AddProperty(CoreObjectsFactory.CreateStringProperty("Name", "Reference device simulator", true));
config.AddProperty(CoreObjectsFactory.CreateStringProperty("LocalId", "RefDevSimulator", true));
config.AddProperty(CoreObjectsFactory.CreateStringProperty("SerialNumber", "sim01", true));

// Create an openDAQ(TM) instance builder to configure the instance

var instanceBuilder = OpenDAQFactory.InstanceBuilder();

// Add mDNS discovery service available for servers
instanceBuilder.AddDiscoveryService("mdns");

// Set reference device as a root
instanceBuilder.SetRootDevice("daqref://device1", config);

// Creating a new instance from builder
var instance = instanceBuilder.Build();

// Start the openDAQ OPC UA and native streaming servers
var servers = instance.AddStandardServers();

for (int i = 0; i < servers.Count; i++)
{
    servers[i].EnableDiscovery();
}

Console.Write("Press a key to exit the application ...");
Console.ReadKey(intercept: true);

openDAQ™ client application

Having set up a simulator Device that is running openDAQ™ servers, we now move on to implement the client application that connects to the simulated Device. The client-side application uses a Client module to connect to an openDAQ™ Server. When connected, the application is used to read the Device’s Signal data, read / modify its Properties, and perform structural changes such as adding / removing / re-routing Function Blocks or Devices - all of which will be explained throughout this guide.

There are several server/client modules available in the SDK. Throughout this guide will will be using the default "native" openDAQ protocol to connect to our devices.
  • Cpp

  • Python

  • C#

We start by editing our code with a basic application skeleton quick_start_empty.cpp found in the cpp/quick_start directory, starting with creating an openDAQ™ Instance:

#include <opendaq/opendaq.h>
#include <iostream>
#include <chrono>
#include <thread>

int main(int argc, const char* argv[])
{
    // Create a fresh openDAQ(TM) instance, which acts as the entry point into our application
    daq::InstancePtr instance = daq::Instance(MODULE_PATH);

    return 0;
}

We start by editing our code with a basic application skeleton in a new .py file, starting with creating an openDAQ™ instance:

import opendaq

instance = opendaq.Instance()

We start by editing our code with a basic application skeleton in a new .cs file, starting with creating a openDAQ™ instance:

using Daq.Core.OpenDAQ;

// Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
var instance = OpenDAQFactory.Instance();

The openDAQ™ Instance acts as our entry point to the application. It loads all available modules that allow for connecting to Devices, starting Servers, as well as doing data processing and calculations.

Modules are dynamic libraries that are loaded when creating an openDAQ™ instance. They look at a default or user-provided directory path, which points to our openDAQ™ binaries. They provide functions to connect to devices, start servers, and add function blocks that are used to process data and perform calculations.

Discovering devices

openDAQ™ Devices represent physical data acquisition hardware and allow for processing, generation, and manipulation of data. They can also be used to connect to other Devices, forming a device hierarchy.

The provided simulator represents a physical data acquisition Device. Such devices contain a list of Channels that correspond to the physical input / output connectors of the Device. A Channel outputs data received from sensors connected to the connectors as Signals, carrying data bundled in Packets. The simulator Device simulates two such Channels, both outputting sine wave Signals.

We can obtain a list of Devices that we can add / connect to via by getting a list of available Devices. openDAQ™ can ask all loaded Modules to return information about any Device it discovers. If multiple modules return information about the same device, it means that this device supports multiple protocols, and its discovery information will be grouped. In this guide, we use the "Native client module" to connect to our simulator that is running a "Native server" and a "Reference device module". The latter allows for the creation of simulated Devices that output sine waves. Those are used by the provided simulator to generate sample data.

The code snippet below searches for all available Devices, asking all Modules to produce a list of Device metadata including information on how to connect to said Devices in the form of connection strings.

  • Cpp

  • Python

  • C#

#include <opendaq/opendaq.h>
#include <iostream>
#include <chrono>
#include <thread>

int main(int argc, const char* argv[])
{
    // Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
    daq::InstancePtr instance = daq::Instance(MODULE_PATH);

    // Find and output the names and connection strings of all available devices
    daq::ListPtr<daq::IDeviceInfo> availableDevicesInfo = instance.getAvailableDevices();
    for (const auto& deviceInfo : availableDevicesInfo)
    {
        std::cout << "Device name: " << deviceInfo.getName() << ", Connection string: " << deviceInfo.getConnectionString() << std::endl;
        for (const auto & capability : deviceInfo.getServerCapabilities())
        {
            std::cout << " - Protocol name: " << capability.getProtocolName() << ", Connection string: " << capability.getConnectionString() << std::endl;
        }
    }

    return 0;
}
import opendaq

# Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
instance = opendaq.Instance()

# Find and output the names and connection strings of all available devices
for device_info in instance.available_devices:
    print('Device name: {}, Connection string: {}'.format(device_info.name, device_info.connection_string))
    for capability in device_info.server_capabilities:
        print(' - Protocol name: {}, Connection string: {}'.format(capability.protocol_name, capability.connection_string))
using Daq.Core.OpenDAQ;

// Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
var instance = OpenDAQFactory.Instance();

// Find and output the names and connection strings of all available devices
foreach (var deviceInfo in instance.AvailableDevices)
{
    Console.WriteLine($"Name: {deviceInfo.Name}, Connection string: {deviceInfo.ConnectionString}");
    foreach (var capability in deviceInfo.ServerCapabilities)
    {
        Console.WriteLine($" - Protocol name: {capability.ProtocolName}, Connection string: {capability.ConnectionString}");
    }
}

Running the above code initiates the discovery protocol of all modules loaded by openDAQ™. Most modules that work over the ethernet connection use a mDNS discovery client to find devices on the network. The output of the code snippet above should look something like this:

Device name: Reference device simulator, Connection string: daq://openDAQ_serialNumber
 - Protocol name: openDAQ Native Streaming, Connection string: daq.ns://ipAddress:7420/
 - Protocol name: openDAQ Native Configuration, Connection string: daq.nd://ipAddress:7420/
 - Protocol name: openDAQ OpcUa, Connection string: daq.opcua://ipAddress:4840/
Device name: Device 0, Connection string: daqref://device0
Device name: Device 1, Connection string: daqref://device1

Connection strings in openDAQ™ are used to connect to a device. They always appear in the format of "prefix://address". The prefix is used to differentiate between different modules that will be used for connection to the device:

  • "Simulator device" has a connection string that starts with daq://. Devices running an openDAQ™ server have a connection string of the format daq://Manufacturer_SerialNumber. We might discover multiple servers of the same device. They will be grouped under the same connection string, and their information made available in the "Sever capabilities" field as shown in the previous code snippet. When connecting via a connection string with the daq:// prefix, openDAQ™ will automatically choose the most optimal connection protocol.

  • "Reference device" has a connection string that starts with daqref://. Said prefix corresponds to the openDAQ™ simulator devices that can be created locally. They are used by our simulator virtual image/application.

Any device with an undefined manufacturer, serial number, or without an openDAQ™ server (with no "server capabilities") will not use the daq://Manufacturer_SerialNumber connection string format, but will use the one provided by an individual device/client implementation (Eg. daqref://)

Connecting to a remote device

In the previous section we obtained a list of available devices. We can use the discovery information to find and connect to our simulator - we filter the device information objects via name to find one that belongs to the simulator.

  • Cpp

  • Python

  • C#

#include <opendaq/opendaq.h>
#include <iostream>
#include <chrono>
#include <thread>

using namespace std::literals::chrono_literals;
using namespace date;

int main(int argc, const char* argv[])
{
    // Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
    daq::InstancePtr instance = daq::Instance(MODULE_PATH);

    // Find and connect to a simulator device
    const auto availableDevices = instance.getAvailableDevices();
    daq::DevicePtr device;
    for (const auto& deviceInfo : availableDevices)
    {
        if (deviceInfo.getName() == "Reference device simulator")
        {
            device = instance.addDevice(deviceInfo.getConnectionString());
            break;
        }
    }

    // Exit if no device is found
    if (!device.assigned())
        return 0;

    // Output the name of the added device
    std::cout << device.getInfo().getName() << std::endl;

    return 0;
}
import opendaq
import time

# Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
instance = opendaq.Instance()

# Find and connect to a simulator device
for device_info in instance.available_devices:
    if device_info.name == 'Reference device simulator':
        device = instance.add_device(device_info.connection_string)
        break
else:
    # Exit if no device is found
    exit(0)

# Output the name of the added device
print(device.info.name)
using Daq.Core.OpenDAQ;

// Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
var instance = OpenDAQFactory.Instance();

// Find and connect to a simulator device
Device device = null;
foreach (var deviceInfo in instance.AvailableDevices)
{
    if (deviceInfo.Name.Equals("Reference device simulator"))
    {
        device = instance.AddDevice(deviceInfo.ConnectionString);
        break;
    }
}

if (device == null)
{
    // Exit if no device is found
    return 0;
}

// Output the name of the added device
Console.WriteLine(device.Info.Name);

Adding a remote Device with its connection string connects to said Device. The Device can be used as if it were local. This means we can configure the device and read its data.

The Device we connect to is added as a child below the openDAQ™ Instance, or more accurately, below our Root Device.

Later examples in this guide will only extend the examples from the previous section. As such, the code from the previous examples will not be duplicated; only new additions will be displayed and explained.

The openDAQ™ Instance and Root Device

As mentioned above, the openDAQ™ Instance is our entry point to the openDAQ™ application. However, this is only a convenient abstraction. The Instance is from the application perspective a simple object that forwards almost all calls to its "Root Device". For example, when accessing sub-devices via te Instance, we are accessing the sub-devices of the Root Device.

  • Cpp

  • Python

  • C#

// The following two calls are equivalent
instance.getDevices();
instance.getRootDevice().getDevices();
# The following two calls are equivalent
instance.devices
instance.root_device.devices
// The following two calls are equivalent
instance.Devices;
instance.RootDevice.Devices;

The openDAQ™ Instance creates a default Root Device when constructed. The default Root Device gains access to all loaded Modules, thus allowing for the addition of Devices, and other openDAQ™ Components that are made available by the loaded Modules. The Root Device always appears at the top of the Device hierarchy.

Conveniently, our simulator overrides the default Root Device, by setting the Reference Device as the Root Device.

Reading Device data

The simplest way of reading values of an openDAQ™ device’s signal is to do a one-shot query of the last value sent through said signal. This can be achieved by simply calling the Signal’s function for retrieving the last value:

  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // Get the first signal of the first device's channel
    daq::ChannelPtr channel = device.getChannels()[0];
    daq::SignalPtr signal = channel.getSignals()[0];

    // Print out the last value of the signal
    std::cout << signal.getLastValue() << std::endl;

    return 0;
}
# ...

# Get the first signal of the first device's channel
channel = device.channels[0]
signal = channel.signals[0]

# Print out the last value of the signal
print(signal.last_value)
// ...

// Get the first signal of the first device's channel
var channel = device.GetChannels()[0];
var signal = channel.GetSignals()[0];

// Print out the last value of the signal
Console.WriteLine(signal.LastValue);

Packets and Readers

The SDK uses "Packets" to send data through Signals to all listeners. To act as a listener, a Connection with a Signal must be formed which is done by connecting it to an Input Port.

To ease reading data sent by Signals, openDAQ™ defines a set of Readers. Readers create an Input Port to which a given Signal is connected. They provide helper methods to ease reading any data that arrives through the formed Connection.

One such Reader is the Stream reader. It presents Packets that arrive through the Connection as a stream of data, abstracting away the concept of Packets from the user. In the example below we create such a Reader that interprets the data sent by the reference Device as a stream of double type values. We read up to 100 samples approximately every 25 ms.

  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // Output 40 samples using reader
    daq::StreamReaderPtr reader = daq::StreamReader<double, uint64_t>(signal);

    // Allocate buffer for reading double samples
    double samples[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        reader.read(samples, &count);
        if (count > 0)
            std::cout << samples[count - 1] << std::endl;
    }

    return 0;
}
# ...
reader = opendaq.StreamReader(signal, value_type=opendaq.SampleType.Float64)

# Output 40 samples using reader
for cnt in range (0, 40):
    time.sleep(0.025)
    # Read up to 100 samples and print the last one
    samples = reader.read(100)
    if len(samples) > 0:
        print(samples[-1])
// ...

// Output 40 samples using reader
var reader = OpenDAQFactory.CreateStreamReader(signal); //defaults to CreateStreamReader<double, long>

// Allocate buffer for reading double samples
double[] samples = new double[100];
for (int i = 0; i < 40; i++)
{
    Thread.Sleep(25);

    // Read up to 100 samples, storing the amount read into `count`
    nuint count = 100;
    reader.Read(samples, ref count);
    if (count > 0)
        Console.WriteLine(samples[count - 1]);
}

Reading time-stamps

Most often, to interpret Signal data, we want to determine the time at which the data was measured. To do so, Signals that carry measurement data contain a reference to another Signal - its domain Signal. The Domain Signal outputs domain data at the same rate as the measured signal. openDAQ™ allows for any application-specific domain type to be used (angle, frequency,…​), but most often the time domain is used. For example, our simulator Device outputs time Signal data in seconds.

To not lose timestamp accuracy, openDAQ™ provides a TickResolution parameter that is used to scale data from an integer tick to a value corresponding to the Signal’s physical unit. Our simulated Device does just that - it outputs time data as integers and provides a resolution ratio which scales the integers into double precision values in seconds. To scale the time data, the values of the domain Signal must be multiplied by the resolution.

Reading basic data and domain
  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // Get the resolution, origin, and unit
    daq::DataDescriptorPtr descriptor = signal.getDomainSignal().getDescriptor();
    daq::RatioPtr resolution = descriptor.getTickResolution();
    daq::StringPtr origin = descriptor.getOrigin();
    daq::StringPtr unitSymbol = descriptor.getUnit().getSymbol();

    std::cout << "Origin: " << origin << std::endl;

    // Allocate buffer for reading domain samples
    uint64_t domainSamples[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        reader.readWithDomain(samples, domainSamples, &count);
        if (count > 0)
        {
            // Scale the domain value to the Signal unit (seconds)
            daq::Float domainValue = (daq::Int) domainSamples[count - 1] * resolution;
            std::cout << "Value: " << samples[count - 1] << ", Domain: " << domainValue << unitSymbol << std::endl;
        }
    }

    return 0;
}
# ...

# Get the resolution, origin, and unit
descriptor = signal.domain_signal.descriptor
resolution = descriptor.tick_resolution
origin = descriptor.origin
unit_symbol = descriptor.unit.symbol

print('Origin:', origin)

for i in range (0, 40):
    time.sleep(0.025)

    # Read up to 100 samples
    samples, domain_samples = reader.read_with_domain(100)

    # Scale the domain values to the Signal unit (seconds)
    domain_values = domain_samples * float(resolution)
    if len(samples) > 0:
        print('Value:', samples[-1], ', Domain:', domain_values[-1], unit_symbol)
// ...

// Get the resolution, origin, and unit
var descriptor = signal.DomainSignal.Descriptor;
var resolution = descriptor.TickResolution;
var origin = descriptor.Origin;
var unitSymbol = descriptor.Unit.Symbol;

Console.WriteLine($"Origin: {origin}");

// Allocate buffer for reading domain samples

long[] domainSamples = new long[100];
for (int i = 0; i < 40; i++)
{
    Thread.Sleep(100);

    // Read up to 100 samples, storing the amount read into `count`
    nuint count = 100;
    reader.ReadWithDomain(samples, domainSamples, ref count);
    if (count > 0)
    {
        // Scale the domain value to the Signal unit (seconds)
        double domainValue = (double)domainSamples[count - 1] * ((double)resolution.Numerator / resolution.Denominator);
        Console.WriteLine($"Value: {samples[count - 1]}, Domain: {domainValue}{unitSymbol}");
    }
}

Running the example, we can see very high numbers for the domain values. This is due to them being relative to the domain signal’s origin. Above, we read and output the domain signal origin, noting that it equates to the UNIX epoch of "1970-01-01T00:00:00Z". The domain values read are thus relative to the UNIX epoch.

Using a Time Reader

To read time-domain signal data, a Time Reader can be used to perform the conversion from ticks to system wall-clock time.

As making the conversion from ticks to an actual domain unit manually can be cumbersome when the domain is time and the origin is an epoch specified in ISO-8601 format a Time Reader can be used to perform the conversion automatically.

Reading with Time Reader
  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // From here on the reader returns system-clock time-points for the domain values
    auto timeReader = daq::TimeReader(reader);

    // Allocate buffer for reading domain samples
    std::chrono::system_clock::time_point timeStamps[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        timeReader.readWithDomain(samples, timeStamps, &count);
        if (count > 0)
            std::cout << "Value: " << samples[count - 1] << ", Time: " << timeStamps[count - 1] << std::endl;
    }

    return 0;
}
# ...

# Create a Time Stream Reader that outputs domain values in the datetime format
time_reader = opendaq.TimeStreamReader(reader)

for i in range (0, 40):
    time.sleep(0.025)
    # Read up to 100 samples and print the last one
    samples, time_stamps = time_reader.read_with_timestamps(100)
    if len(samples) > 0:
        print(f'Value: {samples[-1]}, Domain: {time_stamps[-1]}')
//TimeReader currently not available in .NET Bindings

Function Blocks

Instead of printing Signal data to the standard terminal output, the openDAQ™ package provides a simple renderer Function Block that displays a graph, visualizing the data.

The openDAQ™ Function Blocks are data processing objects. They receive data through Signals connected to the Function Block’s Input Ports, process the data, and output processed data as new Signals. An example of such a Function Block is an statistics Function Block that averages input Signal data over the last n samples, outputting the average as a new Signal.

Not all Function Blocks are required to have Input Ports or output Signals, however. For example, a function generator Function Block might only output generated Signals, without requiring any input data. The Channels of our simulated Device are another such example - they do not receive any input data but still produce output Signals.

Conversely, a file writer Function Block has no output Signals, but only receives input data, and writes it to a file on a hard drive. Another example of the latter is the renderer Function Block that is provided by one of the Modules within the openDAQ™ binaries. It provides an Input Port to which a Signal can be connected. Once connected, the renderer draws a graph that visualizes the Signal data over time. The Function Block can be added to our openDAQ™ Instance using its "RefFBModuleRenderer" unique ID.

Function Blocks
Figure 1. Function Blocks with different combinations of Input Ports and output Signals
As with Devices, we can list the metadata of all Function Blocks made available by loaded Modules by getting all available Function Blocks. Doing so we can obtain a list of Function Block information objects, providing metadata, as well as the IDs of the Function Blocks.
  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // Create an instance of the renderer function block
    daq::FunctionBlockPtr renderer = instance.addFunctionBlock("RefFBModuleRenderer");

    // Connect the first output signal of the device to the renderer
    renderer.getInputPorts()[0].connect(signal);

    std::this_thread::sleep_for(5000ms);
    return 0;
}
# ...

# Create an instance of the renderer function block
renderer = instance.add_function_block('RefFBModuleRenderer')
# Connect the first output signal of the device to the renderer
renderer.input_ports[0].connect(signal)

time.sleep(5)
// ...

// Create an instance of the renderer function block
var renderer = instance.AddFunctionBlock("RefFBModuleRenderer");

// Connect the first output signal of the device to the renderer
renderer.GetInputPorts()[0].Connect(signal);

Try running the above code snippet. You should see a new window pop-up, displaying the sine wave Device Signal, similar to the window shown in the image below.

image
Figure 2. Image of the renderer drawing a signal graph

The data path

As mentioned, the renderer is a Function Block that receives input data but produces no output Signals. However, the loaded reference Modules also provide another Function Block - the statistics. The statistics takes an input Signal, averages its data over the last n samples, and outputs the averaged data as an output Signal.

Such Function Blocks can form a longer Data Path, where multiple Function Blocks are chained together, each using the output of the previous block as its input data. In the next part of our example, we connect the output Signal of the simulated Device’s first Channel through the statistics and into the renderer, forming the following data path:

image
Figure 3. Image of the data path from the Channel through the statistics and into the renderer

We extend our code to add and connect the statistics Function Block:

  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // Create an instance of the statistics function block
    daq::FunctionBlockPtr statistics = instance.addFunctionBlock("RefFBModuleStatistics");

    // Connect the first output signal of the device to the statistics
    statistics.getInputPorts()[0].connect(signal);

    // Connect the first output signal of the statistics to the renderer
    renderer.getInputPorts()[1].connect(statistics.getSignals()[0]);

    std::this_thread::sleep_for(5000ms);
    return 0;
}
# ...

# Create an instance of the statistics function block
statistics = instance.add_function_block('RefFBModuleStatistics')
# Connect the first output signal of the device to the statistics
statistics.input_ports[0].connect(signal)
# Connect the first output signal of the statistics to the renderer
renderer.input_ports[1].connect(statistics.signals[0])

time.sleep(5)
// ...

// Create an instance of the statistics function block
var statistics = instance.AddFunctionBlock("RefFBModuleStatistics");

// Connect the first output signal of the device to the statistics
statistics.GetInputPorts()[0].Connect(signal);

// Connect the first output signal of the statistics to the renderer
renderer.GetInputPorts()[1].Connect(statistics.GetSignals()[0]);
We now connected the statistics Signal to the 2nd Input Port of the renderer. Both the renderer and the statistics Function Blocks are designed to always have an available Input Port. Whenever a Signal is connected to one of its ports, a new Input Port is created.

When running the above example, we should be able to see the renderer display two Signals - the original sine wave, and the averaged Signal below.

Configuring properties

The openDAQ™ Devices, Function Blocks, and Channels (which are a specialization of Function Blocks) are Property Objects. Property Objects allow for configuring a set of Properties associated with the Device. Each Property contains a set of metadata that describes the Property, and a corresponding value.

For example, the reference Device’s Channel has the Properties "Amplitude" and "Frequency" that control the amplitude and frequency of the sine wave it outputs. Their metadata defines their default, as well as a minimum and maximum values. These Properties represent the settings that Devices, Channels, and Function Blocks allow users to configure.

With the below code snippet, we extend our application example to list the Property names of the first Channel of the simulated Device. We adjust its frequency and noise level, and modulate the amplitude at a set interval.

  • Cpp

  • Python

  • C#

int main(int argc, const char* argv[])
{
    // ...

    // List the names of all properties
    for (const daq::PropertyPtr& prop : channel.getVisibleProperties())
        std::cout << prop.getName() << std::endl;

    // Set the frequency to 5 Hz
    channel.setPropertyValue("Frequency", 5);
    // Set the noise amplitude to 0.75
    channel.setPropertyValue("NoiseAmplitude", 0.75);

    // Modulate the signal amplitude by a step of 0.1 every 25 ms.
    double amplStep = 0.1;
    for (int i = 0; i < 200; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(25));
        const double ampl = channel.getPropertyValue("Amplitude");
        if (9.95 < ampl || ampl < 1.05)
            amplStep *= -1;
        channel.setPropertyValue("Amplitude", ampl + amplStep);
    }

    return 0;
}
# ...

# List the names of all properties
for prop in channel.visible_properties:
    print(prop.name)

# Set the frequency to 5 Hz
channel.set_property_value('Frequency', 5)
# Set the noise amplitude to 0.75
channel.set_property_value('NoiseAmplitude', 0.75)

# Modulate the signal amplitude by a step of 0.1 every 25 ms.
amplitude_step = 0.1
for i in range (0, 200):
    time.sleep(0.025)
    amplitude = channel.get_property_value('Amplitude')
    if not (1.05 <= amplitude <= 9.95):
        amplitude_step = -amplitude_step
    channel.set_property_value('Amplitude', amplitude + amplitude_step)
// ...

// List the names of all properties
foreach (var prop in channel.VisibleProperties)
    Console.WriteLine(prop.Name);

// Set the frequency to 5 Hz
channel.SetPropertyValue("Frequency", 5);
// Set the noise amplitude to 0.75
channel.SetPropertyValue("NoiseAmplitude", 0.75d);

// Modulate the signal amplitude by a step of 0.1 every 25 ms.
double amplStep = 0.1d;
for (int i = 0; i < 200; i++)
{
    Thread.Sleep(25);
    double ampl = channel.GetPropertyValue("Amplitude");
    if (9.95d < ampl || ampl < 1.05d)
        amplStep *= -1d;
    channel.SetPropertyValue("Amplitude", ampl + amplStep);
}

The rendered output now displays a noisy Signal with a modulating amplitude. Below it, it shows the averaged Signal, drawing a smoother sine wave.

Full example code

  • Cpp

  • Python

  • C#

#include <opendaq/opendaq.h>
#include <iostream>
#include <chrono>
#include <thread>

using namespace std::literals::chrono_literals;
using namespace date;

int main(int /*argc*/, const char* /*argv*/[])
{
    // Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
    daq::InstancePtr instance = daq::Instance(MODULE_PATH);

    // Find and connect to a simulator device
    const auto availableDevices = instance.getAvailableDevices();
    daq::DevicePtr device;
    for (const auto& deviceInfo : availableDevices)
    {
        if (deviceInfo.getName() == "Reference device simulator")
        {
            device = instance.addDevice(deviceInfo.getConnectionString());
            break;
        }
    }

    // Exit if no device is found
    if (!device.assigned())
        return 0;

    // Output the name of the added device
    std::cout << device.getInfo().getName() << std::endl;

    // Get the first signal of the first device's channel
    daq::ChannelPtr channel = device.getChannels()[0];
    daq::SignalPtr signal = channel.getSignals()[0];

    // Print out the last value of the signal
    std::cout << signal.getLastValue() << std::endl;

	// Output 40 samples using reader
    daq::StreamReaderPtr reader = daq::StreamReader<double, uint64_t>(signal);

    // Allocate buffer for reading double samples
    double samples[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        reader.read(samples, &count);
        if (count > 0)
            std::cout << samples[count - 1] << std::endl;
    }

    // Get the resolution and origin
    daq::DataDescriptorPtr descriptor = signal.getDomainSignal().getDescriptor();
    daq::RatioPtr resolution = descriptor.getTickResolution();
    daq::StringPtr origin = descriptor.getOrigin();
    daq::StringPtr unitSymbol = descriptor.getUnit().getSymbol();

    std::cout << "Origin: " << origin << std::endl;

    // Allocate buffer for reading domain samples
    uint64_t domainSamples[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        reader.readWithDomain(samples, domainSamples, &count);
        if (count > 0)
        {
            // Scale the domain value to the Signal unit (seconds)
            daq::Float domainValue = (daq::Int) domainSamples[count - 1] * resolution;
            std::cout << "Value: " << samples[count - 1] << ", Domain: " << domainValue << unitSymbol << std::endl;
        }
    }

    // From here on the reader returns system-clock time-points for the domain values
    auto timeReader = daq::TimeReader(reader);

    // Allocate buffer for reading domain samples
    std::chrono::system_clock::time_point timeStamps[100];

    for (int i = 0; i < 40; ++i)
    {
        std::this_thread::sleep_for(25ms);

        // Read up to 100 samples, storing the amount read into `count`
        daq::SizeT count = 100;
        timeReader.readWithDomain(samples, timeStamps, &count);
        if (count > 0)
            std::cout << "Value: " << samples[count - 1] << ", Time: " << timeStamps[count - 1] << std::endl;
    }

    // Create an instance of the renderer function block
    daq::FunctionBlockPtr renderer = instance.addFunctionBlock("RefFBModuleRenderer");

    // Connect the first output signal of the device to the renderer
    renderer.getInputPorts()[0].connect(signal);

    // Create an instance of the statistics function block
    daq::FunctionBlockPtr statistics = instance.addFunctionBlock("RefFBModuleStatistics");

    // Connect the first output signal of the device to the statistics
    statistics.getInputPorts()[0].connect(signal);

    // Connect the first output signal of the statistics to the renderer
    renderer.getInputPorts()[1].connect(statistics.getSignals()[0]);

    // List the names of all properties
    for (const daq::PropertyPtr& prop : channel.getVisibleProperties())
        std::cout << prop.getName() << std::endl;

    // Set the frequency to 5 Hz
    channel.setPropertyValue("Frequency", 5);
    // Set the noise amplitude to 0.75
    channel.setPropertyValue("NoiseAmplitude", 0.75);

    // Modulate the signal amplitude by a step of 0.1 every 25 ms.
    double amplStep = 0.1;
    for (int i = 0; i < 200; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(25));
        const double ampl = channel.getPropertyValue("Amplitude");
        if (9.95 < ampl || ampl < 1.05)
            amplStep *= -1;
        channel.setPropertyValue("Amplitude", ampl + amplStep);
    }

    return 0;
}
import opendaq
import time

# Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
instance = opendaq.Instance()

# Find and connect to a simulator device
for device_info in instance.available_devices:
    if device_info.name == 'Reference device simulator':
        device = instance.add_device(device_info.connection_string)
        break
else:
    # Exit if no device is found
    exit(0)

# Output the name of the added device
print(device.info.name)

# Get the first signal of the first device's channel
channel = device.channels[0]
signal = channel.signals[0]

# Print out the last value of the signal
print(signal.last_value)

reader = opendaq.StreamReader(signal, value_type=opendaq.SampleType.Float64)

# Output 40 samples using reader
for cnt in range (0, 40):
    time.sleep(0.025)
    # Read up to 100 samples and print the last one
    samples = reader.read(100)
    if len(samples) > 0:
        print(samples[-1])

# Get the resolution, origin, and unit
descriptor = signal.domain_signal.descriptor
resolution = descriptor.tick_resolution
origin = descriptor.origin
unit_symbol = descriptor.unit.symbol

print('Origin:', origin)

for i in range (0, 40):
    time.sleep(0.025)

    # Read up to 100 samples
    samples, domain_samples = reader.read_with_domain(100)

    # Scale the domain values to the Signal unit (seconds)
    domain_values = domain_samples * float(resolution)
    if len(samples) > 0:
        print('Value:', samples[-1], ', Domain:', domain_values[-1], unit_symbol)

# Create a Time Stream Reader that outputs domain values in the datetime format
time_reader = opendaq.TimeStreamReader(reader)

for i in range (0, 40):
    time.sleep(0.025)
    # Read up to 100 samples and print the last one
    samples, time_stamps = time_reader.read_with_timestamps(100)
    if len(samples) > 0:
        print(f'Value: {samples[-1]}, Domain: {time_stamps[-1]}')

# Create an instance of the renderer function block
renderer = instance.add_function_block('RefFBModuleRenderer')
# Connect the first output signal of the device to the renderer
renderer.input_ports[0].connect(signal)

# Create an instance of the statistics function block
statistics = instance.add_function_block('RefFBModuleStatistics')
# Connect the first output signal of the device to the statistics
statistics.input_ports[0].connect(signal)
# Connect the first output signal of the statistics to the renderer
renderer.input_ports[1].connect(statistics.signals[0])

# List the names of all properties
for prop in channel.visible_properties:
    print(prop.name)

# Set the frequency to 5 Hz
channel.set_property_value('Frequency', 5)
# Set the noise amplitude to 0.75
channel.set_property_value('NoiseAmplitude', 0.75)

# Modulate the signal amplitude by a step of 0.1 every 25 ms.
amplitude_step = 0.1
for i in range (0, 200):
    time.sleep(0.025)
    amplitude = channel.get_property_value('Amplitude')
    if not (1.05 <= amplitude <= 9.95):
        amplitude_step = -amplitude_step
    channel.set_property_value('Amplitude', amplitude + amplitude_step)
using Daq.Core.OpenDAQ;

// Create a fresh openDAQ(TM) instance that we will use for all the interactions with the openDAQ(TM) SDK
var instance = OpenDAQFactory.Instance();

// Find and connect to a simulator device
Device device = null;
foreach (var deviceInfo in instance.AvailableDevices)
{
    if (deviceInfo.Name.Equals("Reference device simulator"))
    {
        device = instance.AddDevice(deviceInfo.ConnectionString);
        break;
    }
}

if (device == null)
{
    // Exit if no device is found
    return 0;
}

// Output the name of the added device
Console.WriteLine(device.Info.Name);

// Get the first signal of the first device's channel
var channel = device.GetChannels()[0];
var signal = channel.GetSignals()[0];

// Print out the last value of the signal
Console.WriteLine(signal.LastValue);

// Output 40 samples using reader
var reader = OpenDAQFactory.CreateStreamReader(signal); //defaults to CreateStreamReader<double, long>

// Allocate buffer for reading double samples
double[] samples = new double[100];
for (int i = 0; i < 40; i++)
{
    Thread.Sleep(25);

    // Read up to 100 samples, storing the amount read into `count`
    nuint count = 100;
    reader.Read(samples, ref count);
    if (count > 0)
        Console.WriteLine(samples[count - 1]);
}

// Get the resolution, origin, and unit
var descriptor = signal.DomainSignal.Descriptor;
var resolution = descriptor.TickResolution;
var origin = descriptor.Origin;
var unitSymbol = descriptor.Unit.Symbol;

Console.WriteLine($"Origin: {origin}");

// Allocate buffer for reading domain samples

long[] domainSamples = new long[100];
for (int i = 0; i < 40; i++)
{
    Thread.Sleep(100);

    // Read up to 100 samples, storing the amount read into `count`
    nuint count = 100;
    reader.ReadWithDomain(samples, domainSamples, ref count);
    if (count > 0)
    {
        // Scale the domain value to the Signal unit (seconds)
        double domainValue = (double)domainSamples[count - 1] * ((double)resolution.Numerator / resolution.Denominator);
        Console.WriteLine($"Value: {samples[count - 1]}, Domain: {domainValue}{unitSymbol}");
    }
}

// Create an instance of the renderer function block
var renderer = instance.AddFunctionBlock("RefFBModuleRenderer");

// Connect the first output signal of the device to the renderer
renderer.GetInputPorts()[0].Connect(signal);

// Create an instance of the statistics function block
var statistics = instance.AddFunctionBlock("RefFBModuleStatistics");

// Connect the first output signal of the device to the statistics
statistics.GetInputPorts()[0].Connect(signal);

// Connect the first output signal of the statistics to the renderer
renderer.GetInputPorts()[1].Connect(statistics.GetSignals()[0]);

// List the names of all properties
foreach (var prop in channel.VisibleProperties)
    Console.WriteLine(prop.Name);

// Set the frequency to 5 Hz
channel.SetPropertyValue("Frequency", 5);
// Set the noise amplitude to 0.75
channel.SetPropertyValue("NoiseAmplitude", 0.75d);

// Modulate the signal amplitude by a step of 0.1 every 25 ms.
double amplStep = 0.1d;
for (int i = 0; i < 200; i++)
{
    Thread.Sleep(25);
    double ampl = channel.GetPropertyValue("Amplitude");
    if (9.95d < ampl || ampl < 1.05d)
        amplStep *= -1d;
    channel.SetPropertyValue("Amplitude", ampl + amplStep);
}