- Author: Roman Savochenko
- Sponsored by, for complete revision on 1.8 HD HD[!]: SINGE SOFTWARE
- Initially created: in the old Wiki
This manual is made to help in building the modules for OpenSCADA. The module creation may be required if you wish to add the support for new data source or other extension to OpenSCADA. Since OpenSCADA is an extremely modular system, all interfaces of interaction with the external environment are implemented by expanding it with modules of following types:
- databases;
- communication interfaces, transports;
- protocols of the communication interfaces;
- data sources and data acquisition;
- archives-history (messages and values);
- user interfaces (GUI, TUI, WebGUI, speech, signal ...);
- additional modules, special.
To create modules for OpenSCADA you need to have some experience in C/C++ programming language, the build system AutoTools, as well as basic knowledge of Linux and the distribution you are using.
In order to post the developed module to the OpenSCADA source tree repository, you must do the following and comply with the following requirements:
- be the copyright holder or the author of the module code and distribute it under any free license, GPL preferred;
- prepare and store the module code as a separated archive of the module folder for any module subsystem of OpenSCADA with demands to the content:
- the source texts of the module at the beginning of each file must include correct copyrights information, be written and formatted according to some system where preference should be given to the formatting style of the main OpenSCADA modules;
- the localisation files of the module must be also correct, actual and proper formed.
- write a short information page of the module for placing it to the OpenSCADA Wiki in way like to the other ones there;
- for placing of this module, write a direct request in the forum topic "OpenSCADA development", including proof of functionality from the OpenSCADA developer or a short demonstration video.
Contents
1 Creating a New Module
Modules in OpenSCADA are the shared libraries, which are dynamically connected to the OpenSCADA core at startup or during the program operation. Many of the modules can be disabled, connected and updated during the operation from the module scheduler. Modules can be also builtin-included into the OpenSCADA core during building by an argument -enable-{ModName}=incl to the configure configuration script, what you can learn from the building manual. The OpenSCADA modules can be of seven types according to the presented modular subsystems. For now the modules to OpenSCADA are written in the "C++" programming language, although bindings for other languages may appear in the future.
In the tree of the source texts, in the branch of each subsystem, to simplify the creation of new modules, the folder "=Tmpl=" with the module template of the corresponding subsystem is provided. The developer of the new module can take this folder and copy it with the name of his new module, although he can also always use as a sample any real working module if his new one is close in structure. You can create modules in the OpenSCADA sources tree or as an independent project of the external module for OpenSCADA.
1.1 Creation in the sources tree of the OpenSCADA project
It makes sense to create new modules in the sources tree of OpenSCADA project in case of further plans for the transfer of the new module to the OpenSCADA project. Since the module should not be contrary to the spirit of the open source project and license on the basis of which OpenSCADA is developed and distributed, license of the new module obviously should be one of the free licenses.
The procedure for creating a new module with inclusion in the source text tree based on a template is generally simpler than the procedure for an external module and includes the following steps:
- 1. Get the source tree of the OpenSCADA project for:
- the Work branch:
- svn co svn://oscada.org/trunk/OpenSCADA
- the stable release branch — NOT RECOMMENDED, as only patches are accepted for stable LTS releases and this instruction requires version 0.9 or higher:
- svn co svn://oscada.org/tags/openscada_0.9
- 2. Copy the template folder with the "NewMod" name of the new module, for example, for the "DB" subsystem:
- cd OpenSCADA/src/moduls/bd; cp -r =Tmpl= NewMod; cd NewMod; rm -f configure.ac
- for the "DAQ" subsystem the path is — "OpenSCADA/src/moduls/daq"
- for the "Archive-History" subsystem the path is — "OpenSCADA/src/moduls/arhiv"
- for the "Transport" subsystem the path is — "OpenSCADA/src/moduls/transport"
- for the "Transport protocol" subsystem the path is — "OpenSCADA/src/moduls/protocol"
- for the "UI" subsystem the path is — "OpenSCADA/src/moduls/ui"
- for the "Special" subsystem the path is — "OpenSCADA/src/moduls/special"
- cd OpenSCADA/src/moduls/bd; cp -r =Tmpl= NewMod; cd NewMod; rm -f configure.ac
- 3. Edit the "module.cpp" file for:
- also can do that automatically by: sed -i "s/Tmpl/NewMod/g" *.{cpp,h}
- changing the name of the builtin-inclusion functions of the module according to the name of the new module:
- "TModule::SAt bd_Tmpl_module( int n_mod )" —> "TModule::SAt bd_NewMod_module( int n_mod )"
- "TModule *bd_Tmpl_attach( const TModule::SAt &AtMod, const string &source )" —> "TModule *bd_NewMod_attach( const TModule::SAt &AtMod, const string &source )"
- information about the module in the "module.cpp" file, namely the section:
//************************************************
//* Modul info! *
#define MOD_ID "NewMod"
#define MOD_NAME _("DB NewMod")
#define MOD_TYPE SDB_ID
#define VER_TYPE SDB_VER
#define MOD_VER "0.0.1"
#define AUTHORS _("MyName MyFamily")
#define DESCRIPTION _("BD NewMod description.")
#define MOD_LICENSE "GPL2"
- 4. Edit the module's building configuration in the "Makefile.am" file to:
- also can do that automatically by: sed -i "s/Tmpl/NewMod/g" Makefile.am
EXTRA_DIST = *.h po/* if NewModIncl noinst_LTLIBRARIES = db_NewMod.la db_NewMod_la_CXXFLAGS = -DMOD_INCL -fpic db_NewMod_la_LIBTOOLFLAGS = --tag=disable-shared db_NewMod_la_LDFLAGS = -module else oscd_modul_LTLIBRARIES = db_NewMod.la db_NewMod_la_CXXFLAGS = db_NewMod_la_LIBTOOLFLAGS = --tag=disable-static db_NewMod_la_LDFLAGS = -module -avoid-version $(top_builddir)/src/liboscada.la endif db_NewMod_la_CXXFLAGS += $(NewMod_CFLAGS) db_NewMod_la_LDFLAGS += $(NewMod_LDLAGS) db_NewMod_la_SOURCES = module.cpp I18N_mod = $(oscd_modulpref)NewMod include ../../../../I18N.mk
- 5. Add an entry of the new module to the end of the subsystem's section of the OpenSCADA building system configuration file "OpenSCADA/configure.ac":
- to the section "DB modules" end for the "DB" subsystem:
AX_MOD_DB_EN(NewMod, [disable or enable[=incl] compilation module DB.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "DAQ modules" end for the "DAQ" subsystem:
AX_MOD_DAQ_EN(NewMod, [disable or enable[=incl] compilation module DAQ.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "Archive modules" end for the "Archive-History" subsystem:
AX_MOD_Archive_EN(NewMod, [disable or enable[=incl] compilation module Archive.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "Transport modules" end for the "Transport" subsystem:
AX_MOD_Transport_EN(NewMod, [disable or enable[=incl] compilation module Transport.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "Transport protocol modules" end for the "Transport protocol" subsystem:
AX_MOD_TrProt_EN(NewMod, [disable or enable[=incl] compilation module Protocol.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "UI modules" end for the "UI" subsystem:
AX_MOD_UI_EN(NewMod, [disable or enable[=incl] compilation module UI.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- to the section "Special modules" end for the "Special" subsystem:
AX_MOD_Special_EN(NewMod, [disable or enable[=incl] compilation module Special.NewMod], disable, incl, [ # The code for external libraries of the module check ])
- 6. Now the new module can be built in OpenSCADA after the reorganisation of the building system:
- autoreconf -if; ./configure --enable-NewMod; make
- 7. Publication — form a patch with your module and send it to the OpenSCADA developers:
- cd OpenSCADA; make distclean; rm -rf src/moduls/bd/NewMod/{Makefile.in,.deps}
- svn add src/moduls/bd/NewMod; svn diff > NewMod.patch
1.2 Creation an external module to OpenSCADA
Creation an external module to OpenSCADA may make sense in the case of the development the integration interface with business (commercial) systems that require proprietary interaction code, as well as in the case of other commercial interfaces implementations in which the module for the OpenSCADA acquire the status of the separate project, that is distributed and maintained independently often in the form of binary buildings for a specific platform and version of OpenSCADA. The license of such modules, respectively, can be arbitrary.
The procedure for creation a new external module based on the template is largely similar to the previous procedure and includes the following steps:
- 1. Get the source texts of the OpenSCADA project — for an external module as a source of the template you can use any OpenSCADA source files of version more than 0.9, because it is necessary to copy only the "=Tmpl=" directory and several files for build.
- 2. Copy the template directory with the "NewMod" name of the new module, for example, for the "DB" subsystem; and in that directory already create and copy the necessary files of the external module. Further, the information files of the project "COPYING", "NEWS", "README", "AUTHORS" and "ChangeLog" must be filled accordingly to the nature of the new module.
- cp -r OpenSCADA/src/moduls/bd/=Tmpl= NewMod; touch NewMod/{NEWS,README,AUTHORS,ChangeLog}; cp OpenSCADA/I18N.mk NewMod/
- for the "DAQ" subsystem the path is — "OpenSCADA/src/moduls/daq/=Tmpl="
- for the "Archive-History" subsystem the path is — "OpenSCADA/src/moduls/arhiv/=Tmpl="
- for the "Transport" subsystem the path is — "OpenSCADA/src/moduls/transport/=Tmpl="
- for the "Transport protocol" subsystem the path is — "OpenSCADA/src/moduls/protocol/=Tmpl="
- for the "UI" subsystem the path is — "OpenSCADA/src/moduls/ui/=Tmpl="
- for the "Special" subsystem the path is — "OpenSCADA/src/moduls/special/=Tmpl="
- cp -r OpenSCADA/src/moduls/bd/=Tmpl= NewMod; touch NewMod/{NEWS,README,AUTHORS,ChangeLog}; cp OpenSCADA/I18N.mk NewMod/
- 3. Edit the module information in the "module.cpp" file similarly to the appropriate item in the previous section.
- 4. Edit the module building configuration in the "Makefile.am" file similarly to the appropriate item in the previous section, excepts:
- instead "db_NewMod_la_LDFLAGS = -module -avoid-version $(top_builddir)/src/liboscada.la" write "db_NewMod_la_LDFLAGS = -module -avoid-version", that is remove "$(top_builddir)/src/liboscada.la"
- instead "include ../../../../I18N.mk" write "include I18N.mk", that is remove the path "../../../../"
- 5. Edit the build system configuration file "configure.ac" for:
- also can do that automatically by: sed -i "s/Tmpl/NewMod/g" configure.ac
- "AC_INIT([Tmpl],[0.0.1],[my@email.org])" — information about the module: name, version and EMail of the project
- "AM_CONDITIONAL([TmplIncl],[test])" — "AM_CONDITIONAL([NewModIncl],[test])"
- 6. Install the OpenSCADA development package "openscada-dev" or "openscada-devel" — because the module is an external one and the OpenSCADA source files are needed only at the first stage of the module creation, you need to install the OpenSCADA development package, which contains the header files and libraries.
- 7. Now you can build the new module, after formation of the building system
- autoreconf -if; ./configure; make
2 API of the module
OpenSCADA API for the developer of OpenSCADA and modules to it is described in the document "OpenSCADA API", which should always be on hand at development for OpenSCADA. This document focuses on the detailed explanation of the main points of the modular API.
Modules in OpenSCADA are implemented as shared libraries and one such library can contain many modules of the OpenSCADA subsystems, actually acting as a container. Those containers also can be included-builtin in the OpenSCADA Core Library if you build very tightly solutions.
The first step in connecting the shared libraries (SO — Shared Object) is the connection of the initialization functions. These functions should be defined as usual "C" functions to avoid distortion of them names. Usually this is done as follows:
//================== CUT =========================
extern "C"
{
#ifdef MOD_INCL
TModule::SAt bd_Tmpl_module( int n_mod )
#else
TModule::SAt module( int n_mod )
#endif
{
if(n_mod == 0) return TModule::SAt(MOD_ID, MOD_TYPE, VER_TYPE);
return TModule::SAt("");
}
<!--T:348-->
#ifdef MOD_INCL
TModule *bd_Tmpl_attach( const TModule::SAt &AtMod, const string &source )
#else
TModule *attach( const TModule::SAt &AtMod, const string &source )
#endif
{
if(AtMod == TModule::SAt(MOD_ID,MOD_TYPE,VER_TYPE)) return new BDTmpl::BDMod(source);
return NULL;
}
}
//================== CUT =========================
The entry point of any module are the following functions:
- TModule::SAt module( int n_mod ), TModule::SAt {modTp}_{modNm}_module( int n_mod ) — are used to scan the list and information about all modules in the library. The first function is used during the implementation of modules in an external shared library, and the second during their including-embedding in the OpenSCADA core, where modTp corresponds to the type of the module, and modNm is its ID.
- TModule *attach( const TModule::SAt &AtMod, const string &source ), TModule *{modTp}_{modNm}_attach( const TModule::SAt &AtMod, const string &source ) — is used to directly connect-open the selected module by creating a root object of the module inherited from TModule. The first function is used during the implementation of modules in an external shared library, and the second during the including-embedding of them into the OpenSCADA core, where modTp and modNm correspond to the previous function.
Common to all modules is the inheritance of the root object-class of the module from the class of the module subsystem TModule, which indicates the presence of a common part of the module interface, which we will consider further. To get a vision of the architecture of the modules in the context of the overall OpenSCADA architecture, it is strongly recommended to have the overall OpenSCADA class diagram in front of your eyes!
All module interface objects inherit the node class TCntrNode, which provides the control interface mechanism. One task of the mechanism is to provide the object configuration interface in any OpenSCADA configurator.
Common API |
---|
TCntrNode — OpenSCADA Node:
|
TModule — OpenSCADA Module:
|
API of the modules of the "Data Bases (DB)" subsystem |
Intended for the integration of OpenSCADA with a database or DBMS which is implemented by the module. Provides two common approaches in the modules implementation:
|
TTypeBD->TModule — the root module object of the "DB" subsystem:
|
TBD — the database object:
|
TTable — the table object in the database:
|
API of the modules of the "Transports" subsystem |
Provides OpenSCADA communications through the interface, often it is the network one which is implemented by the module. |
TTypeTransport->TModule — the root module object of the "Transports" subsystem:
|
TTransportIn — the input transport object:
|
TTransportOut — the output transport object:
|
API of the modules of the "Transport protocols" subsystem |
Provides OpenSCADA with the protocol layer communications, implemented by the module, for the data access from the external systems and for the OpenSCADA data providing for the external systems. |
TProtocol->TModule — the root module object of the "Transport protocols" subsystem:
|
TProtocolIn — the input object of the transport protocol of the input requests processing from the input transport object TTransportIn. For each session of the input request the object of the associated input protocol is created, which remains alive until completion of the full "request->answer" session. Address of the transport, which opened the protocol instance, is specified in srcTr():
|
API of the modules of the "Data AcQuisition" subsystem |
Provides the realtime data acquisition from the external systems or it formation in the calculators, implemented by the module. That is the main subsystem since SCADA is about the Data Acquisition primarily. As the main subsystem it provides several approaches in the modules implementation, which mostly about the attributes structure formation and storing:
|
TTypeDAQ->TModule — the root module object of the "Data AcQuisition" subsystem:
|
TController — the data source controller object. In the context of the object is usually run a task of the periodic or scheduled polling of the realtime data of one physical controller or physically separated block of data. In the case of data getting by the packages, they are placed directly into the archive associated with the parameter attribute TVAl::arch(), and the current value is set by the TVAl::set() function with the attribute "sys"=TRUE:
|
TParamContr->TValue — the controller parameter object of the data source. Contains attributes with real data in a set defined by physically available data. The values to the attributes come from the polling task of the controller, in the asynchronous mode, or are requested during the access, in the synchronous mode, and through the methods of the inherited type TValue:
|
API of the modules of the "Archives-History" subsystem |
Used for archiving and maintaining the history of messages and realtime values received in the "Data AcQuisition" subsystem, and in the means implemented by the module. |
TTypeArchivator->TModule — the root module object of the "Archives-History" subsystem:
|
TMArchivator — the message archiver object.
|
TVArchivator — the value archiver object.
|
TVArchEl — the element object of the value archiver.
|
API of the modules of the "User Interfaces" subsystem |
The user interface is formed according to the concept and mechanisms of external known standards and libraries. |
TUI->TModule — the root module object of the "User Interfaces" subsystem: |
API of the modules of the "Special" subsystem |
Implements the specific functions that are not included in any of the above subsystems. The specific functions are formed accordingly to their own need and with using all features of the OpenSCADA API. |
TSpecial->TModule — the root module object of the "User Interfaces" subsystem: |
For the convenience of direct addressing to the root object of the module from any object lower in the hierarchy, it is recommended to define the global variable "mod" in the namespace of the module, with its initialization in the constructor of the root object. Also, for transparent translation of the text messages of the module, it is recommended to define the function templates of the module message translation call "_({Message})" and "trS({Message})" as:
#undef _
#define _(mess) mod->I18N(mess).c_str()
#undef trS
#define trS(mess) mod->I18N(mess,mess_PreSave)
In the constructor of the root module's object inherited from the TModule it is necessary to set the main information of the module by call the function void modInfoMainSet({Name}, {Type}, {Version}, {Authors}, {Description}, {License}, {Source}) after init the fast link "mod" to the root object of the module.
Further receiving of the translation template file "po/oscd_NewMod.pot" of the text messages "_({Message})" and "trS({Message})", as well as updating-actualising the files of already existing translations "po/{uk|de|ru|...}.po" is carried out by the command make messages in the module folder.
During solve the tasks of the new module, it may be necessary to expand the configuration parameters, which is carried out in the virtual function void cntrCmdProc( XMLNode *req );. The content of this function, which adds properties, in the SQLite module looks like this:
void MBD::cntrCmdProc( XMLNode *opt )
{
//Getting the page info
if(opt->name() == "info") {
TBD::cntrCmdProc(opt);
ctrMkNode("fld",opt,-1,"/prm/cfg/ADDR",EVAL_STR,enableStat()?R_R___:RWRW__,"root",SDB_ID,3,
"dest","sel_ed","select","/prm/cfg/dbFsList","help",
_("SQLite DB address must be written as: \"{FileDBPath}\".\n"
"Where:\n"
" FileDBPath - full path to DB file (./oscada/Main.db).\n"
" Use the empty path to create a temporary database on the disk.\n"
" Use \":memory:\" to create a temporary database in memory."));
if(reqCnt)
ctrMkNode("comm",opt,-1,"/prm/st/end_tr",_("Close opened transaction"),RWRW__,"root",SDB_ID);
}
//Processing for commands to the page
string a_path = opt->attr("path");
if(a_path == "/prm/cfg/dbFsList" && ctrChkNode(opt)) {
opt->childAdd("el")->setText(":memory:");
TSYS::ctrListFS(opt, addr(), "db;");
}
else if(a_path == "/prm/st/end_tr" && ctrChkNode(opt,"set",RWRW__,"root",SDB_ID,SEC_WR) && reqCnt) transCommit();
else TBD::cntrCmdProc(opt);
}
The first half of this function serves the "info" information requests with the list and properties of the configuration fields. The second half serves all the other commands on getting, setting value, and others. The TBD::cntrCmdProc(opt); call is used to obtain the inherited interface. More details on appointment of the used functions are in the control interface, as well as in the source code of existing modules.
The TCntrNode object, in addition to the control interface function, provides unified control mechanisms for modifying the object's configuration, loading, saving, and deleting configuration duplicates in the storage. You can use the modif() and modifG() functions to set the object data modification flag, and the module-specific loading and saving actions can be placed in virtual functions:
- void load_( TConfig *cfg );, void load_( ); — loading the object from a storage.
- void save_( ); — saving the object to a storage.
Configuration actions typically occur through the TConfig object, which contains a set of defined properties with structure and values. To directly reflect the properties of a module object, it inherits from TConfig, and new properties are added with the command:
fldAdd(new TFld("MOD_PRMS",trS("Module addition parameters"),TFld::String,TFld::FullText|TCfg::NoVal,"100000"));
Loading/saving/deleting of the properties specified in the TConfig object, from/to/in the storage, is made with the following commands:
TBDS::dataGet(fullDB(), owner().nodePath()+tbl(), *this);
TBDS::dataSet(fullDB(), owner().nodePath()+tbl(), *this);
TBDS::dataDel(fullDB(flag&NodeRemoveOnlyStor), owner().nodePath()+tbl(), *this, TBDS::UseAllKeys);
Where:
- fullDB() — the full name of the storage in the common form;
- owner().nodePath()+tbl() — the common path, represented as the table, to the object node in the configuration file;
- *this — this object, inherited from TConfig.
To generate the debug messages according the common debugging concept, you need to use the function mess_debug() with call it conditionally at the program source part:
- the rarely invoked part — direct call to the function mess_debug(...);;
- the often invoked part — conditional call if(mess_lev() == TMess::Debug) mess_debug(...);;
- the critical to performance code part — wrapping to definition OSC_DEBUG:
#ifdef OSC_DEBUG
mess_debug(...);
#endif