NDS3  1.0.0
API reference manual
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
Getting started

This section will guide you through the development of a simple Device Support based on the NDS3 framework

Here we assume that you already installed the NDS3 library and the NDS3 control system layer of your choice.

NDS3 concepts

Device Supports for the NDS3 framework must declare the device functionalities using a tree-like structure: the tree structure can be composed by:

  • Nodes
  • Process Variables (PVs)

Nodes

A node can contain children nodes or PVs. There are several kind of nodes:

  • plain Node: it is just used to organize the device's component
  • Port: it is a node that is able to communicate with an instance of the underlying control system. All the children of a port will communicate with the control system using their parent port
  • StateMachine: a node that comes with few pre-defined children representing the node's state and supplies few functionalities to manage the states
  • DataAcquisition: a node that comes with few pre-defined children PVs and a state machine useful for controlling data acquisition.

The responsability of defining the device structure falls on the constructor of the class that the developer decides to use as representation of the device: when a specific device support is needed then NDS3 will allocate and construct the class that represents the device and expects that the constructor of the class will specify all the nodes and PVs that are needed.

Process variables

PVs, or Process Variables, are variables that are visible from both the Device Support and from the clients connected to the control system.

NDS3 differentiates between input PVs and output PVs;

  • input PVs are WRITTEN by the Device Support and READ by the control system and its clients
  • output PVs are READ by the Device Suppoer and WRITTEN by the control system and its clients

For instance, a device representing a temperature sensor will have one input PV that it will update regularly with the detected temperature, while a power supply could have an output PV that set the desidered output voltage.

NDS3 provides 2 kinds of input and output PVs:

  • PVVariableIn and PVVariableOut: the Variable PVs are responsible for storing the value written by the Device Support or by the Control System and are able to supply the internal value on demand to both the Device Support and the Control System. This means that the Device Support can just read or write a value into them when it is convenient, without worrying about the mechanism and the timing that the Control System will use to read/write the PV
  • PVDelegateIn and PVDelegateOut: the Delegate PVs will call a predefined Device Support's function when the Control System wants to read or write some data from/to the PV

All the input PVs also support the push model, which allows the Device Support to forcefully push data to the Control System at any moment.

A simple Device Support: a thermometer

The first device that we are going to build is a really simple thermometer: It has a single input PV which supplies the temperature in Kelvin degrees.

The structure of the termomether device is really simple: it provides a root node (a Port, so it can communicate with the Control System) that represents the device and an input PV that the control system will read to get the current temperature.

The source code

Let's put this in C++ form:

#include <nds3/nds.h>
#include <iostream>
// This class declares our device in the contructor and supplies the functionalities to support it
class Thermometer
{
private:
nds::PVDelegateIn<double> m_thermometerPiniPV;
public:
// Constructor. Here we setup the device structure
Thermometer(nds::Factory &factory,
const std::string &deviceName,
const nds::namedParameters_t &parameters) :
m_thermometerPiniPV(nds::PVDelegateIn<double>("TemperaturePINI", std::bind(&Thermometer::getTempRandom,
this,
std::placeholders::_1,
std::placeholders::_2)))
{
nds::Port rootNode(deviceName);
rootNode.addChild(nds::PVDelegateIn<double>("Temperature", std::bind(&Thermometer::getTemperature,
this,
std::placeholders::_1,
std::placeholders::_2)));
m_thermometerPiniPV.processAtInit(1);
rootNode.addChild(m_thermometerPiniPV);
rootNode.initialize(this, factory);
}
void getTemperature(timespec* pTimestamp, double* pValue)
{
*pValue = 10; // It is always cold in here
std::cout << "Temperature #1: " << *pValue << std::endl;
}
void getTempRandom(timespec* pTimestamp, double* pValue)
{
*pValue = 35; // It is always hot right there
std::cout << "Temperature #2 (pini): " << *pValue << std::endl;
}
};
// If we want to load our device support dynamically then we have to compile it
// as a dynamic module and we have to provide the functions to allocate it,
// deallocate it and retrieve its name
NDS_DEFINE_DRIVER(Thermometer, Thermometer)

What did we do with this code?

The constructor takes three parameters passed by NDS3 when it allocates the device:

  • factory: this parameter contains an interface to the Control System that requested the creation of the device
  • deviceName: the name with which the device should be presented to the control system. Usually it is used as name of the root node
  • parameters: a map of named parameters that are specific for each Device Support. The implementor of the Device Support can decide how to use the parameters

We allocate a Port as root node: a Port node holds a reference to the underlying control system. PVs can be added only to Ports or to nodes that have a Port up in the hierarchy.

The variable rootNode contains a shared pointer to the acual implementation of the Port: when we will register rootNode with the Control System then NDS will take care of storing the shared pointer internally and therefore the Port will continue to exists even after rootNode goes out of scope.

Attention
In NDS3, all the device components just hold shared pointers to the actual components implementation.

After we declare the root node then we call nds::Node::addChild() on it to add a child component, in our case a PVDelegateIn that handles double floating point values. PVDelegateIN PVs are not able to store the value internally but rely on a function that is called when the control system wants to read the value. In our case we provide a callback to getTemperature() which always return 10 degrees Kelvin.

Finally we call initialize() which registers the root node and its children with the Control System.

If we are running our Device Support using EPICS as control system then the following PV will be available:

rootNodeName-Temperature

Issuing a dbpf rootNodeName-Temperature.PROC 1 will cause the framework to call getTemperature() and fill the PV with the value 10.

Compiling the source code

The Makefile for a NDS3 device is quite straightforward: it does not depend on EPICS or any other Control System you may be using and you just have to make sure that it includes the shared library nds3 and specify the flag NDS3_DLL (tells the compiler that the NDS3 is being linked dynamically).

An example for our thermometer:

CXX = g++
# We enable the c++0x flag, plus we define the NDS3_DLL preprocessor macro.
CXXFLAGS = -std=c++0x -Wall -Wextra -pedantic -fPIC -pthread -DNDS3_DLL
# Flags passed to gcc during linking
LINK = -shared -fPIC -Wl,-as-needed
# Name of our device library
TARGET = libthermometer.so
# We specify that we depend on NDS3
LIBS = -lnds3
# We assume that all the .cpp files are in the src folder
SRCS = thermometer.cpp
OBJS = $(SRCS:.cpp=.o)
# Rules for building
$(TARGET): $(OBJS)
$(CXX) $(LINK) -o $@ $^ $(LIBS)
.PHONY: clean
clean:
$(RM) $(TARGET) $(OBJS)

This makefile will generate the shared module libthermometer.so. Now you can load the device into your NDS3 control system of choice and run it.

Using the Thermometer device on EPICS

On EPICS you will use the command to load the shared module and the command to allocate and construct the device.

For instance:

epics> ndsLoadDriver path/to/libthermometer.so
epics> ndsCreateDevice Thermometer testDevice
epics> iocInit
epics> dbl
testDevice-Temperature
epics> dbpf testDevice-Temperature.PROC 1
epics> dbgf testDevice-Temperature
10