The Device Control library (DCLIB) is a library of tools and C source code designed to remote control properties of devices and quickly prototype access to registers on an embedded device. The NETPP enhancement of this library is a network variant of the DCLIB where most of the intelligence is outsourced to the device controller, therefore avoiding raw register protocols between the embedded devices.
A NETPP capable device in a network can communicate its properties to a client in a flexible way. A client can talk to devices of completely different architecture, it is up to the client software to check the device's capabilities according to whether a property exists, or according to a user defined device type field.
To keep the overhead at a minimum for embedded targets, properties are defined as a static array of structures. Those structures are not spelled out manually in source code, but generated from an XML description. The XML description supports several data types (INT, BOOL, BUFFER, etc.) as well as different access methods (global variables, handler functions, etc.)
In short, a few more properties of the netpp library:
Note that netpp does not cover 16 bit TOKEN size anymore. Since most compilers support 32 bit values ('long'), this is not a big issue. Optionally, a non-network variant can be built using specific binary protocols.
To outline the functionality of NETPP: A typical application will consist of a server running on the device to be controlled, and a frontend on the client PC to talk to it. This is in general referred to as Master (PC, User) and Slave (responding Device). A Slave will, under normal circumstances, never initiate a connection.
To summarize the terminology:
NETPP provides two rudimentary frontends:
The example program's source code is documented under the Examples section (see master::c)
The API interface uses the standard C convention (CDECL) interface. NETPP is portable among different operating systems such as Microsoft Windows, Linux, QNX, and other Unix versions.
All Master functionality is hinted with the prefix 'dc' in the listed API functions. The Slave functionality is not completely documented, please see Implementing a NETPP server backend on Slave and Implementing a new NETPP driver / server for hints and examples.
The architecture also allows to integrate user defined children types to a data type via structs that allow signalling events to a specific GUI element when a Property was changed, etc.
To keep the API and the library code as compact and efficient as possible, the Property access happens via tokens. The token is obtained by dcProperty_ParseName() via a given name, which is defined in the name space of a certain parent node. The top root node of a Property hierarchy is always the device node which is obtained by dcDevice_GetRoot().
Value passing is done via a runtime data type information capable structure 'DCValue'. This structure contains a data union field and an associated data type. The value is retrieved from the DCValue struct according to the type field. The Property getter and setter functions of the library perform a type check and return an error (DCERR_PROPERTY_TYPE_MATCH) in case of a mismatch.
Strings and buffer structures are mostly owned by the caller and are not allocated statically by NETPP, unless explicitely stated. When obtaining a buffer, the caller always must preinitialize the DCValue with a pointer to the reserved buffer and the length value to prevent buffer overflows. This is done as follows:
When a string or buffer is allocated static (for example, a port descriptor or other local property structures),
v.value.p does not need to be preinitialized. It points to the static buffer location of the requested property after a successful call. So, it is in general not a good idea to assign v.value.p to a malloc() return value directly, as it may be modified within a GetProperty() call.
If the data type is not known a priori, v.type can be set to DC_UNDEFINED. The handler then fills in the data type.
If the buffer size can be dynamic on the device side, it may be desired to query the buffer size beforehand. This is done via
Note: This does not work with buffer properties that are implemented via a handler (non static buffers). This is a current limitation of the library. In case of a handled (dynamic) buffer, v.len contains 0 for a buffer or 1 for a string.
Generally, the first parameter of a function whose name starts with 'dcDevice_' is the current device device handle. The struct members of the device object must NEVER be accessed directly, to preserve future binary compatibility.
If not documented otherwise, the function's return value is
Functions which set a value may return DCWARN_PROPERTY_MODIFIED, meaning that the given parameter was not valid or not according to certain constraints. All 'Event' children of this property should be queried via dcProperty_Select(), because they may have changed. In this case, the user should also read back the value that was passed to dcDevice_SetProperty() which was possibly modified 'in place'.
Error codes are split in library specific and device specific error codes. Typically, an application controlling a backend includes "devlib_error.h" (containing the generic library error codes) and "DEVICE/device_error.h" (device and backend specific error codes). This file is normally found in the device specific directory netpp/devices/DEVICE (where DEVICE is to be replaced by the device implementation name). It is recommended, to set the include paths as follows:
INCLUDES = -I$(NETPP)/include -I$(NETPP)/devices
The function dcGetErrorString() returns the library specific (device independent) error codes. Normally, an application first checks for device errors and then calls this function to get a verbose error message. To create a user defined error handler, you may want to derive an automatic generation from
The property definition happens in XML according to a schema. A property always has a type specification and an access description. To map a device side variable, flag, or buffer into a property, there are several approaches.
For the above, please refer to the XML implementation in devices/example/example.xml
To write the XML file, it is recommended to use an XML editor like XMLMind Xml Editor, see http://www.xmlmind.com. The provided schema files and style sheets are especially customized for this editor. See an example XML file below.
XMLMind XML Editor example
The style sheet is customized for the XMLMind XML Editor (XXE) such that the item hierarchy is visible in coloured blocks. The underlying Schema files guarantee that the syntax is always correct and that the hierarchy is built according to the schema rules.
As of now, the following property types exist:
Properties can be part of a container such as a Struct or Array node. In the XML description, they display like a property, but contain other properties. A Struct node is a very simple container, it is just a local name space for properties. For example, you would access a property inside a struct via the name "VideoStream.Enable". The members of a struct may be restricted by the schema file to a certain number. This limitation can be changed by the user.
Array properties can either contain:
Currently, only 1-dimensional arrays are supported for register addressing mode. In some implementations, two dimensional arrays may be supported via handlers.
The size of an array can always be queried via the special child 'Size', e.g. "Coordinate.Size". The second children of an array node is always the contained object. Its name does not matter, since it is addressed by the index automatically.
Assume, you have a register table with values [X, Y, W, H, Mode] somewhere in your chip address space. You'd want to save these values in a non-volatile space on the chip for fast context switching. So you would have a copy of this table somewhere else in memory. This lists in short, how you wrap this with a struct/array/property hierarchy:
An example is found in the LUTvalues array property in devices/example/example.xml
If the structured register tables are organized contiguously (respectively, using a fixed address spacing between the tables), they can be simply addressed like "Context[index].Member".
The XML device description is translated to source code via the XML processor 'xsltproc', which must be installed separately from netpp. It is part of every standard Linux distribution or Cygwin distribution on Microsoft Windows.
The generation of the source code happens according to XSLT style sheets that are residing in the xml/ sub folder of the netpp distribution.
XML conversion scheme
A makefile will do the following, by running it from the build directory via 'make':
Optionally, the following type of files can be generated from the XML device description:
devices/ for example devices and their makefiles.
On device side, an implementation of a buffer property basically consists of an address and length field. The 'Property Engine' is responsible of tunneling the data through a specific protocol with minimum buffer copy overhead.
When a variable specification is used, the engine knows the address immediately from the specification. The only thing the caller, i.e. remote control host needs to do, is to reserve enough memory for the string or buffer data, see Property concept. With a handler, things can get more complicated. First, the handler needs to do a check on the buffer size and return a DCERR_PROPERTY_SIZE_MATCH, if the callers buffer is too small. Or, adapt to the situation and send data in chunks. The handler must also pass on the buffer address to the engine before the transfer. When the transfer is finished, the handler is called again with a DCValue type field set to DC_COMMAND. This signals, that the buffer was transferred and an update action can take place, for example when an image is to be redrawn. See also example handler::c set_buffer().
This scheme is applying to buffer reads and writes. For a setter, the handler is notified about the transfer termination (all data arrived) Likewise, the getter is informed that all data was transferred and that the owner may release the allocated buffer space.
In many devices, settings of a property can have impact on another. For example, changing the readout width of a optical sensor may affect the timing. Or very simple, issueing a Reset command on the target requires some registers being reread by the client to synchronize with the user interface.
To address this issue, an event node has to be inserted into the property. When the handler returns DCWARN_PROPERTY_MODIFIED, the event children must be polled as well. An event node simply contains a 'id' reference to another node (which currently must be a property).
For more complex, dynamic rebuilds, you have to use your own mechanisms on a higher level within netpp.
Event properties are static and have an 'EVT_' prefix in their property name.
A Hub represents a specific interface respectively transport layer, for example USB or TCP. It is for example possible, to control the same device Slave via different interfaces. Hubs are instanced as dynamic property nodes, similar to the static property lists, so they can have children. A Port again is always a children of a Hub and represents an attached device that can be opened.
Normally, Hubs are only relevant to the master, however, a Slave uses Hubs internally for each interface it listens to. Ports do not have a meaning on the Slave side, so this paragraph applies to Master actions only.
To browse this hierarchy, a special device handle DC_PORT_ROOT exists. Actions on this pseudo device, such as dcProperty_Select() operate on the local port tree structure. See also example master::c for details. To select the first available hub, you have to call dcProperty_Select() as follows:
Whenever this happens, the ports are initialized and probed. Thus, an update of a device list procedure automatically rescans the available hubs for newly attached devices that way.
For implementing a NETPP server, the following steps need to be taken:
For step 1, there are a few different approaches. One scenario is, that you have a set of existing global variables that you want to export to NETPP. Another approach is, to start the property design first and then implement the variables and handlers where necessary.
As as starting point, you may want to look at the server example (see module NETPP server functionality)
Assume, you want to export a variable
x to NETPP, standing for an image width, for example. If the variable is not yet global, it is recommended to create a context structure that is globally instanced once and make this variable a member of this structure.
When wrapping this variable with a property, you will have to create a new property with a child called 'variable'. In this variable field, you enter the name of the globally accessible variable, in this example:
In the 'header' node of the XML file, you must enter the name of the header file include statement where the global struct is defined.
A few comments about data types: There are only a few valid data types that can be wrapped. These are:
If you want to wrap an integer type different from the above, you have to either change the variable to
int or use the handler methods described below to access the variable.
When you implement properties that must cause an action when they are accessed, you would typically use a handler. This is a piece of C code with a uniform Getter/Setter API. When you have created a XML file with a few handlers, run the following command in the devices/<yourdevice> directory to create a handler skeleton template:
Typically, this file is manually copied to handler.c and the handlers are implemented.
When dealing with buffers, make sure you have read Section String and buffer handling about buffer update/release functionality.
VERY IMPORTANT: Make sure that your program does not spend too much time inside the handler. For example, if the handler waits for a buffer for a longer time than the protocol timeout, the protocol breaks and resets to idle state. So make sure, that your handler processes fast enough. If it does not, you must split up the request for a buffer and the actual transfer into separate property commands, or use a specific error code saying that the buffer is not yet ready. For example in case of a very slow camera, you would use a Trigger property to start the process, poll the Trigger property again to check whether the command has completed and then finally read the buffer.
When using a lot of similar devices, it makes no sense to specify properties over and over again. This makes a class of devices hard to handle, when a basic feature is to be changed.
Therefore, NETPP is able to derive from a base class. This is simply done by adding another "device" specification to the XML file and setting the 'baseclass' attribute to the base class device's id field.
See devices/example folder on how this is done.
On target / backend side, you will need to implement a function called local_getroot() that returns the root node of the selected device. Normally, this returns TOKEN 0 when only one device is implemented. It is up to this function to determine what device description instance is to be used from the definition. This allows a certain device class to determine its properties at runtime. The user thus has the freedom to implement a device control in the following ways:
Variables that are both specified in the base device and derived device's name space, are overridden by the definition in the derived class. So, in the above example, property 'Test2' is defined in both classes as INT but with different static value. When accessing 'Test2' by name, you will always see the value from the device "DerivedDevice". However, when looking at the property list, the property 'Test2' will appear twice. So it is possible to still access the old property by its TOKEN, however, this needs to be determined by walking through the hierarchy with dcProperty_Select().
For developing a new driver, check existing sources, such as unixdevice.c Basically, you will have to implement a new struct protocol_methods for the new interface. Also, you will have to define a server init function which initializes a Hub structure. Since the driver model tries to be as symmetrical as possible, there are not separate functions for the master and the slave, unless Master and slave are using a completely different operating system. However, it may help to keep the differences between master and slave into account:
To develop a new NETPP server for a device, you might want to start from existing code such as in
In the main loop of your server (or your own main loop) you will have to repeatedly call the slave_handler() to listen to your interface.
See protocol_methods which methods are relevant for the master only. In ports_init() you will have to initialize a Hub for the new driver and append it to the global PortBay using portbay_append(). See ports.c for example. Note that ports_init() is called internally, the front end must not call it.
Developers who wish to modify, improve or enhance the netpp code should keep the following rules in mind:
If you think there is a reason for changing files in a "no go" area, please consult the netpp author(s). Note that changes in $NETPP/src or $NETPP/common must be published according to the netpp source license.
A few notes about the coding style: