OPC Adapter

The documentation contains both general design information and specific information on a couple of subjects under "Design issues" later on. They are both essential reading to understand the details of the OPC Adapter. Take the time to read this before you modify the code - it will be worth the effort.

A substantial part of this document contains tips and ideas how the OPC Adapter can be changed or extended. If you are about to make a modification, check the final section of this chapter if such a change have already been foreseen and there are tips availble.

General design

This section explains the general design of the OPC Adapter. It is quite straight-forward and should be easy to understand but it contains a lot of information. The OPC Adapter Reference Manual might be handy to have at the side, to quickly look up the documentation for a class.

Class diagram

The following figure (next page) shows the most important classes in the OPC Adapter, and how the relate to each other (inheritance and has-a).

There are a few other classes, and it might be a good idea to quickly browse through the OPC Adapter Reference Documentation, which contains descriptions of all of them.

overview.gif

Classes explained

OPCAdapter: This is the subclass of cmwfwDeviceAdapter, and all that the SFW sees. The SFW will call methods of the OPCAdapter, the adapter will do its job and return the result. The OPCAdapter should be prepared to handle that the SFW might call its methods from several thread "at once", meaning that shared data must be protected and race conditions avoided. For get/set and poll operations, the only shared data is read-only, so no synchronization is needed there. For monitor-on and monitor-off, the OPCAdapter serializes the requests by immediately acquiring a mutex.

DataConverterInterface-BasicTypesConverter: The DataConverterInterface is the abstract base class for all data converting objects. The BasicTypesConverter is one concrete implementation, which encapsulates the data conversion for most basic data types.

LoggingManager: The LoggingManager receives log messages from almost all other classes and puts them in the log file, including a note about when the message was output and by which thread.

GatewayConfiguration: The GatewayConfiguration is initialized with the command-line arguments, and it reads the configuration file (explicitly specified or the default file) and interprets it. Other classes can access the settings by asking the only instance (Singleton) of the GatewayConfiguration.

OPCConfiguration-TestOPCConfiguration: The OPCConfiguration is the abstract base class for retrieving information about the OPC settings and how OPC items should map to IOPoints in the CMW model. TestOPCConfiguration is one concrete implementation, providing hard-coded such settings. A more useful class, for example called DatabaseOPCConfiguration, should retrieve the location of a database by asking GatewayConfiguration and then retrieve the information from there. Such a class is however not available, but should be a piece of cake to add.

OPCItemInterface-OPCItem-MappedOPCItem: The OPCItemInterface is the abstract base class for accessing individual OPC items. It is implemented by the OPCItem, which invokes protected methods of the OPCServer, after acquiring the pointer to it by asking the item's OPCGroup. The MappedOPCItem is a subclass that also contains a cmwfwIOPoint, which is the corresponding data source in the CMW model. This allows the MappedIOPoint to immediately forward an OPC callback to the SFW with the correct IOPoint as argument.

OPCGroupInterface-OPCGroup: The OPCGroupInterface is the abstract base class for accessing OPC groups. It is implemented by the OPCGroup. The group needs to be able to count how many items it contains that should be enabled for callbacks. If there are no callback-enabled items, the group should set its status to inactive, to save the OPC server the trouble of invoking the callbacks.

OPCServerInterface-OPCServer/TestOPCServer: The OPCServerInterface is the abstract base class for accessing an OPC server. The OPCServer class implements this by making the required COM calls to the actual out-of-process OPC server. The TestOPCServer is another implementation, but it does not do any COM calls and only simulates a real OPC server. All operations will just return, without ever trying to connect to the OPC server. This class is useful to test other components, without requiring that an OPC server is available.

Callbackreceiver_OPC2: This is the COM object that receives callbacks from the OPC server. It implements a standard set of methods, first to be able to act as a COM object (IUnknown) and second to be able to receive the callbacks. The "2" signifies that the callback mechanism used is that of OPC Data Access 2.0.

PointDatabaseInterface-PointDatabase: The PointDatabaseInterface is the abstract base class for the look-up from cmwfwIOPoints to MappedOPCItems. This is implemented by the PointDatabase, which is constructed with an OPCConfiguration object as parameter.

Interaction between classes

Startup

This is an outline of what happens when the OPC Gateway, of which the OPC Adapter is one part, is started:

1. The NT service and the pipe for monitoring is set up. The GatewayConfiguration is constructed and reads the command-line options and the config file.

2. The SFW sfw_main() method is called.

3. The SFW creates the OPC Adapter object by using the method cmwfwDeviceAdapter* initRoutine(), implemented by the GatewayService (in initroutinte_for_SFW.cpp).

4. The SFW calls the init() method of the OPCAdapter, which creates the OPCConfiguration object and gives it as a parameter to the PointDatabase.

5. The RDA initializes the CORBA system, and registers with the CORBA Name Server.

6. The PointDatabase sets up the OPCServer object(s), and adds OPCGroups and MappedOPCItems to it.

7. If at least one OPCServer was constructed successfully, the OPCAdapter sets it state to "ready-to-serve".

Monitor-on and monitor-off

When a monitor-on or monitor-off call is received by the OPCAdapter, the following happens:

1. The OPCAdapter acquires its only mutex for monitor-on/off requests. This prevents other concurrent calls from causing race conditions.

2. The relevant MappedOPCItem for the cmwfwIOPoint argument is found by looking it up in the PointDatabase.

3. The OPCItem is informed that it has one more/less listener.

4. If this was the first/last listener for the OPCItem, it informs its OPCGroup.

5. If this was the first/last active OPCItem for the group, it informs its OPCServer that the group should now have callbacks enabled/disabled.

6. The OPCAdapter releases the mutex.

Get, set and poll

This is what happens when a get/set or poll call is received by the OPCAdapter. Note that there is no need for a mutex as the shared data is read-only (when the PointDatabase has once been set up, the mapping is static).

1. The relevant MappedOPCItem for the cmwfwIOPoint argument is found by looking it up in the PointDatabase.

2. The get/set/poll is performed by either invoking read or write on the MappedOPCItem.

3. The OPCItem invokes the read/write method of the OPCServer, which performs the COM calls needed.

4. The Variant data is converted to/from a cmwfwData object, using the DataConverterInterface object of the OPCItem. (The design pattern Strategy.)

5. The cmwfwData object is timestamped and returned to the caller. If there is an OPC quality code which indicates that the data is "Bad" or "Uncertain", a string representation of the quality is added to the cmwfwData with a tag of "data_quality".

OPC Callback

When a monitor-on request has been made for a particular OPC item, callbacks will be made from the OPC server when the data source changes value.

1. The data source changes value and the OPC server invokes DataHasChanged on the Callbackreceiver_OPC2.

2. The Callbackreceiver_OPC receives the new value(s) as Variants. It also receives the client-side handles for the items, which has been set to pointers to the OPCItem objects. The handles are cast to OPCItem object pointers.

3. From the OPCItem objects, the corresponding DataConverterInterface objects are retrieved. They are used to convert the Variants to cmwfwData objects.

4. The dataHasChanged method is invoked on all OPCItems which has new data.

5. For OPCItems which are set to be active (more monitor-on than monitor-off requests) the cmwfwData object is forwarded to the SFW using a single shared instance of a subclass of cmwfwForwarder, ConcreteForwarder. OPCItems that are not set to be active simply discards the data.

Design issues

The following subsections describes some constructs that is important to know about. If you read this section once, it will be much easier to understand the code and the explainations later. Also, if you find something you find complicated or peculiar, check back here and see if it is explained.

global_opc.h

This file contains macro definitions, pragma directives and similar things that are useful in every OPC adapter source file. Only truly global statements should be put here, and almost certainly no (more) include files.

Indirect calls via superclasses

The reason for these "access methods" in, for example, OPCServerInterface is that C++ does not provide a "friend-with-class-and-its-subclasses" construct. If a class X wants to declare an abstract base class A as friend, only the abstract class can access X. Subclasses of A must go via A to access X.

I could not think of a cleaner way of solving the problem than this. Making methods public could result in misuse by unsuspecting users, and other ways of avoiding the problem were not satisfactory for other reasons. In Java, the problem would be solved with packages.

Typdefs for RDA types, in the SFW file cmwfwTypes.h

The typedefs were present to make it easy to switch from previous dummy classes that were used instead of the RDA in the SFW. However, the typdefs stayed, as they gave cmwfw-prefixed names to RDA classes, which in a way was nice.

The typedefs have been repeated, together with prototype declarations of the RDA classes, in the file cmwfwTypesPrototypesOnly.h which unfortunately does not officially belong to the SFW. By including this file, instead of cmwfwTypes.h, one can speed up compilation considerably. Code that requires more information than the prototype definitions will however need to include the complete file, but since that will be in implementation files it will not create avalanche include effects. This is a practice from Effective C++ (Scott Meyers), Item 34.

All OPC access code in OPCServer.cpp

The reason that all OPC access code is in OPCServer.cpp is simple: The code is not very interesting and I wanted to avoid cluttering many classes with COM code. By keeping everything in one place, the COM calls are isolated. However, other implementations are free to choose another method, for example keeping COM code in a subclass of OPCItemInterface and not delegating the responsibility of COM communication to the OPCServer.

OPC handles stored in OPCItem/OPCGroup

Both the OPCGroupInterface and the OPCItemInterface contains setProperties() methods. These are used to delegate the responsibility of keeping the OPC handles for Groups and Items to the actual Groups and Items. In fact, this is not a really strange thing, as the handles are kept hidden from the user of the classes, and at the same time the OPCServer does not need to keep a lookup-table from Items to their OPC handles, for example.

LoggingManager

The LoggingManager is a bit complex, even though it has a simple interface. It is a Singleton class that accepts log messages (as strings or as strstreams) which should be output to the log file.

First, it should be noted that the LoggingManager can not be used before the GatewayConfiguration has been fully initialized. The reason is simple: That Singleton class holds important config info such as the name of the logfile and LoggingManager cannot operate without it.

The LoggingManager keeps a current log level threshold. All log messages that are not marked with a log level at least this high will be discarded. This is to avoid filling up log files quickly, but still being able to receive the messages if one wants to.

A separate thread is used for actually writing the log messages to file. While this has the benefit of a constantly operating OPC Gateway - even if there are temporary problems with the file system and therefore long, blocking write calls - it also introduces the risk that the entire OPC Gateway might crash before the relevant log messages have been flushed to disc. The solution chosen is that "assertion failed" messages, which really should be the only ones that might result in a crashed OPC Gateway, will wait until their message has been written to disc by the writer thread. This could take several seconds (the writer thread wakes up about every 5 seconds), but should not be a problem since that kind of messages should be really rare - and are very serious when they occur.

It is possible to use "conventional" writes, without the extra thread, by changing the configuration for the OPC Gateway. This is described in the OPC Gateway Documentation.

The LoggingManager will start up at one logging level threshold, and after about a minute it will change to another (normally a higher one, to give verbose logging at startup and brief during normal operation). This can also be controlled with the configuration file. However - if the logging level is manually set with the method setLoggingLevel() (can also be done by monitor programs for the OPC Gateway - again, see its documentation), this behaviour will be disabled, and no automatic switching of log level will be made.

The log file will by default be bounded in size (number of messages). One old file is kept, but the current log file always has the same name, and the latest output is always at the end of it.

RDA MessageLogger not fully implemented

The reason that the MessageLogger is not fully implemented in the temporary RDA is that that would create a dependency from the RDA to the OPC Gateway, which is the wrong way.

Instead, the implementation of two methods, rdaMessageLogger::trace and rdaMessageLogger::error is left for the OPC Gateway, which can then choose to send the log output to the LoggingManager. This achieves the same thing as putting this implementation in the RDA package, but without causing troubles for somebody who want to use the RDA, but not the OPC Gateway. (An example of this is the RDA_only.exe test program.)

Programming practices

This section describes which general rules have been followed for the source code.

Naming

The following conventions have been used to simplify reading of the code and identifying different types:

No one-line if/while/do statements

An surprisingly important practice is to always use a block {...} after for example if-statements, even for one line. This avoids errors of this type:

if (xIsTrue())
  doThis();
  doThat();  // later added, was supposed to be covered by the "if"
doSomethingElse();

Casting

Casting, where needed, should be done using the new casting constructs, which mean for example:

SomeType* temp = const_cast< SomeType* >( constTemp );  // to cast away const

The three other constructs (reinterpret_cast, static_cast and dynamic_cast) should also be used where appropriate.

This avoids quite a few problems that might arise due to careless casting.

Pointers

There are quite a few pointers used as parameters. The basic rule is that ownership of the memory is not transferred, unless explicitly stated in the comment for the method.

Methods starting with "create" normally means that a new instance is created, and that the caller assumes responsibility for it. Also, all methods (for consistency reasons) of GatewayConfiguration give away pointers to newly-allocated memory that the caller should call delete on. This is, of course, also described in the documentation for the methods.

In general, parameters by reference should be preferred where possible, but many of the pointer arguments (especially for cmwfwIOPoint and cmwfwData) exist due to historical reasons.

Copy constructor and assignment operator

For classes that are not supposed to be copied or assigned, the copy constructor and the assignment operator (ClassX(const ClassX&) and ClassX& operator=(const ClassX &)) have been declared private (in the header file), but never defined. This is a common practice to avoid errors when classes are incorrectly copied with the default generated methods and there is no reason to allow copying. If somebody tries to copy or assign them, they will be stopped by the compiler or, in worst case, the linker. This is (also) a practice from Effective C++ (Scott Meyers), Item 27.

#include

As previously stated, the number of includes in each header file should be kept as low as possible. If only pointers or references are handled, it is sufficient to declare the relevant class (by "class SomeClass;") in the header file where it is mentioned, and only include the SomeClass.h header file in the implementation file of the user class.

Comments

Comments for classes and methods are given using the JavaDoc style:

/** Multi-line comment
    Second line
    */
or
/// Single-line comment
These comments should be placed before class methods (note the two asterisks and the three slashes). Several tools exist to extract the comments into HTML documentation, similar to JavaDoc. The one used for the OPC Adapter is called Doxygen, and is based on DOC++.

Tips about MSVC++ and other tools

In this section a couple of effective ways to use the MS Visual Studio package are described.

WinDiff

The program WinDiff is very useful to compare (source code) files with each other. It can also compare a whole directory at once, showing which files differ and what the differences are. WinDiff can be found in the Start menu under Visual Studio when installed.

Editing files

I find it easier to use the file browser of MSVC++ instead of the class browser. The class browser is not always working as it should, but the file browser allows quick access to all files. With several projects within a single workspace, it is also easy to edit files in different projects, and to recompile an application including everything it depends on. Another nice thing when editing files is Alt-F8, which reindents the entire selected block according to its context's indentation.

Edit - Find in files

This function, within MSVC++ is very good when you miss the traditional Unix command grep. It is possible to search for a string (or a regular expression) in all (or some) files in a directory and its subfolders.

I strongly recommend using Find-in-files to fix a similar problem in many places - for example when a class is renamed due to design changes.

By clicking in the Find-in-files pane it is possible to go to the instance found (in the relevant file) right away. Also, F4 can be used to step forward, to the next found instance, possibly in another file.

MSDN: Help - Index

MSDN is extremely useful for looking up information about C++, Win32 functions, programming examples, required header files and so on. I recommend using Help-Index in the menus to search for things, instead of the Search function. Make sure that a suitable subset of the documentation is selected - Java documentation is not very interesting when you're programming in C++ and the other way around. Entries that are filtered out will be displayed in a fainter color in the Index list.

Dependency Walker

The utility Dependency Walker, "Depends", also comes with MSVC++. It will display which DLLs are required by a particular executable, and which version. This is very useful for making sure that the correct libraries are used, or to see that all required DLLs are available on the target machines.

Sysinternals.com utils

There are quite a few utilities available on http://www.sysinternals.com . They are free, and sometimes even come with source code. To mention some favorites:

Other useful things

Two other things I recommend are the "Command prompt here" right-click Explorer context menu (for easy access to a command prompt with the correct current directory), and the "Enable command-line file completion with TAB" registry setting. They should both be easy to find on the web, but are also included in the distribution archive in \tools\Extras.

Configuration

The class GatewayConfiguration is used to retrieve information about where the log output should go. This is really more of a OPC Gateway question than a OPC Adapter question, so this is not covered further here.

The class OPCConfiguration is an abstract base class, providing pure virtual method declarations to be implemented by subclasses. One class, TestOPCConfiguration, implements this interface by supplying configuration for testing purposes. A real and more usable implementation would probably consult a database to be able to provide this information. Since the interface is set and used by the OPC Adapter, it is possible to implement this in any way that is appropriate: file/database/interactive/other configuration.

How to...

There are a couple of modifications or improvements that are likely to be made in the future. This chapter gives some advice for forseen modifications. As the required actions is somewhat dependent on the exact goal of the modification, the descriptions should be seen as suggested solutions. In some cases, another approach might be better.

Add a new datatype

Even if most basic data types are already supported, it might be required to add more complex types or to allow data transfer by reference: BY_REF flag set in the Variant data type.

This is the way it works now: DataConverterInterface specifies an interface for data conversion. This is the kind of object the OPCServer handles: it queries the OPCItem for its data converter, and then the OPCServer converts the Variant to a cmwfwData using the returned object implementing DataConverterInterface.

The basic set of data types are handled by BasicTypesConverter. By specifying a parameter when the object is created, it is possible to choose which kind of cmwfwData objects it should produce and what type of Variant objects it should request from the OPC server.

If a new data type should be added, it should normally not be through modification of the BasicTypesConverter. Instead, create a new class called for example ExtendedTypesConverter, and then use the same structure as the BasicTypesConverter if you wish.

However, if BY_REF should be supported, it might be preferable to modify the existing BasicTypesConverter, and make it adapt to this - requesting BY_REF but accepting either type (the server might not be able to supply BY_REF data).

The benefit of this approach is that several people can create their own extension data types, and nobody needs to know about it - the OPCServer will only see the DataConverterInterface interface, not the class that implements the data conversion. This is the Strategy design pattern.

Add a config file setting

Let us assume that you want to specify an extra parameter in the config file. To do this, you first need to modify GatewayConfiguration so that the parameter is read and interpreted. Look at the code for the other parameters for an example of this. The GatewayConfiguration uses ORBacus config files, for an easy API to the config file contents.

Then you will probably choose between two alternatives: Either provide a method in the GatewayConfiguration which returns the active setting, for example getMyNewSetting(), or make sure that the GatewayConfiguration will itself immediately inform the affected component about the setting. This depends on which way you want the dependencies:

For an example of the former, check (in the source file GatewayConfiguration.cpp) how ooc.orb.service.NameService is handled. For an example of the latter, check how cmw.opcgw.log_level is handled.

Change configuration method

Let us assume that you no longer want to specify configuration with a ORBacus config file. Instead, you want to use some other option: another format of the config file, a database lookup or something similar.

This is easy: Modify the implementation of GatewayConfiguration. The command-line arguments are available, so you are free to use them in any way you wish. Also, you can modify the command-line arguments. This is currently done to make sure that the -ORBconfig file.cfg option is present - which is how the ORBacus reads its settings. If you switch ORB, it will not have this way of configuration and you will need to find another way of specifying settings to the ORB.

Change logging behaviour

If the log output should be done in another way, for example sending the OPC Adapter log output to the RDA MessageLogger (instead of the other way around, which is the way it is now), this is how to do it:

However, note that the solution chosen for the moment, that the RDA MessageLogger sends its output to the LoggingManager is in my eyes the best one. It creates a dependency the wrong way, but on the other hand it allows the logging to be done in a good way for a WindowsNT Service.

Change from a WinNT service

Even if a WinNT service might seem appropriate, there may be reasons to convert the OPC Gateway to a regular command-line or perhaps a GUI application. This is also easy, as a service is (more or less) only a command-line program with some extra startup and handling code. Most of this code is in a separate file, and the rest (the ServiceStart function) will be easy to transform to a regular main() function.

Get status info through a socket connection

If you want to check the status of the OPC Gateway, there are currently three ways to do it.

However, you might want to read the status remotely and not able to read it through the pipe. In this case, you need to use some other method, for example allowing monitors to connect via a socket and get updates through it.

The easiest, and recommended, way to achieve this is to write a new program based on GatewayMonitor.exe, which communicates with the OPC Gateway through the pipe and pass the information on through the socket.

Another way would be to modify the OPC Gateway - all of the pipe code is in the Service source files and should be easy to locate and replace. This would require a recompilation of the OPC Gateway, and also no longer allow the GatewayMonitor.exe to retrieve the server status, so it is not a recommended way.

Update the RDA

It is very likely that the RDA will need to be updated. The current version of the RDA is a temporary implementation using ORBacus. It is temporary because it is a quick port of the "true" RDA - the version for LynxOS using ORBExpress.

When the official version of the RDA based on ORBacus is available, it will probably be desirable to update the OPC Gateway. Also, future versions of the RDA might include new features or bug fixes, and that could also be a reason to upgrade the RDA in the OPC Gateway.

The things that are needed from the RDA, and are likely not to be a part of the official RDA yet, are:

It could be a good idea to look at the existing code for the operator functions, available in IOPoint.h, Data.h/cpp and rdaData.h/cpp. The functions can quite likely be used just as they are even in a new version of the RDA.

Since the OPC Gateway is not based on an officially released RDA, this will require a bit of work. It should not be a big problem, though.

The recommended steps are these:

This should be a rough outline how the RDA is updated. As mentioned, the RDA should preferably be an official version which is able to compile and run on WindowsNT, perhaps with preprocessor directives or special include files.

If the RDA is changed to use another ORB than ORBacus, the link settings will of course need to be modified for the OPC Gateway. Also note that the configuration currently uses ORBacus Property files and that a change of ORB will mean that this code has to be modified.

Update the SFW

Updating the Server framework should be much easier, since the SFW used in the OPC Gateway is an official version with only a very minor change.

First, it might be a good idea to compare the new SFW files with the old ones, to get an idea of which changes has been made. This can be done with for example WinDiff. Make sure that the source code files are using the OPC Gateway naming standard: *.cpp for implementation files and *.h for header files. Renaming files from *.cc to *.cpp can easily be done like this:

X:\NewSFWFiles> ren *.cc *.cpp

Then, the next few steps should be easy:

Change or update the ORB, ORBacus

A change would mean a combination of "Change configuration method" and "Update the RDA" above.

An update would just mean recompiling the ORBacus files and the IDL file for the RDA interface, which in turn would require a recompilation of the RDA.

Reuse code for another adapter/gateway

Reusing code could reduce the time needed to create a new application, similar to the OPC Adapter. Strictly, the part about reusing code for a new gateway should be in the OPC Gateway documentation, but due to its nature it is instead placed here.

For writing a new adapter, the WindowsNT-version of the RDA could of course be reused, even though it is a temporary implementation. When the official version of the RDA for WindowsNT and ORBacus is available, this would not be necessary. The official version of the SFW would be used. For the rest, primarily the logging system (the LoggingManager) and the GatewayConfiguration would be interesting.

For writing a new gateway, especially on WindowsNT, it would probably be very useful to just throw out all OPC-related classes, and replace them with the new functionality. This would mean using more or less everything from the current OPC Gateway, including the logging system and the NT service wrapping.

Extract OPC client code

Another scenario is that another OPC client need to be developed. Just by studying code in OPCServer.cpp it should be quite easy to extract the required OPC handling code. All access to the actual OPC server is encapsulated within the OPCServer.


This is the OPC Adapter Technical Documentation, describing how to extend or improve the OPC Adapter.
Also relevant are: the OPC Adapter Reference Manual - HTML (for quick on-line lookups), the OPC Adapter Reference Manual - RTF (suitable for printing) and the OPC Gateway Documentation (how to use/compile).