⬆️ ⬇️

libuniset2 is a library for creating ACS. It is better to see once ... Part 1

Once upon a time ... I wrote an article “Introduction to libuniset - a library for creating an automated control system” , there were plans to write a sequel, but it didn’t work out. Since then, the library has significantly “grown up” and even version 2.0 has already been released, in which many new features have appeared: remote viewing of logs and program variables, support for various useful and not-so-useful protocols and databases, there is even a “time machine” if this comes to that ...



In general, I gathered my strength and decided that it was better to “see it once” with a specific example.



Therefore, who else is interested, please.



Several articles are planned on the stages of work:



')

Unfortunately, it is difficult to create an example of a real multi-site network project (at least you will need a network of virtual machines, installation of packages, deployment, etc.). Therefore, I will consider a simple example that can be compiled and run locally and show the work of the basic uniset-concepts on it.



As an example, take the following "spherical" puzzle:

It is necessary to write a level management process in the “tank”



The process fills the tank (turns on the pump), and as soon as the level reaches the specified upper threshold (95%), it gives a command to the pump “emptying” the tank. As soon as the level reaches the lower threshold (5%), it starts filling again. And so in a circle.



Work must begin after the arrival of (1) the special command “Start work” and end when the “Start work” command is removed (0).

Since I myself mainly work with ALTLinux , then all the examples will be considered under this OS.

I 'm sitting on Sisyphus , but examples will work on the P7 Platform

So...



Preparatory step - creating a project, installing packages



Install the necessary packages into the system

apt-get install libuniset2-devel libuniset2-extensions-devel libuniset2-utils libomniORB-names git gitk git-gui etersoft-build-utils ccache 


Now you can lay our project ...



Important: libuniset2 requires a compiler with C ++ 11 support.



In order not to suffer too much, I have prepared a “framework”, just download the http://github.com/Etersoft/uniset2-example master branch.



So we have the following directory structure (of course it can be any, this structure is just a “habit”)



/ Utilities - various helper utilities (scripts) that the project usually acquires

/ conf - project configuration files

/ docs - documentation (and how without it in a good project)

/ include - place for common project header files

/ src - source code itself

/ lib - location for the common project library (common functions code)

configure.ac is the project build file itself (yes, I use autotools)

autogen.sh - helper script to generate Makefile.in



and other files, which of course should be in the "beautiful" project.



Run
 autogen.sh && configure && make 


By the way, I used to use jmake - this is such a wrapper over make from the etersoft-build-utils package, which takes into account the presence of ccache in the system and the number of processors.



If everything compiles, then you can follow further ... In general, we still have nothing and nothing.



Step One - Configuring a Uniset Project





In uniset projects, the entire system configuration is stored (usually in a single) xml file. Again, out of habit, it is called configure.xml and put in “conf /”. I will not paint the format of this file, the goal is “to launch something as soon as possible and see the work,” but there is a description in the documentation ...



To begin to fill the configuration file, we need to understand from the description of the task which “sensors” we need in our project. In general, the following list is obtained:



All this is entered into the sensors section (we assign an id as we want, just to be unique).

The result will be something like this:

 <sensors name="Sensors" ...> <item id="100" name="OnControl_S" iotype="DI" textname="  (1 - , 0 -  )"/> <item id="101" name="Level_AS" iotype="AI" textname="   "/> <item id="102" name="CmdLoad_C" iotype="DO" textname="    ''"/> <item id="103" name="CmdUnload_C" iotype="DO" textname="    ''"/> </sensors> 


You can see that when you add sensors, we each set a unique identifier ( id ), a unique name ( name ) and still have textname = ".." - this text can be used later for GUI or other applications.

Each sensor has one of four types ( iotype ). Here is the link to the documentation.



In general, the division into such types is a bit conditional, as long as there is no work with real I / O. But the types themselves determine what a particular sensor is from the point of view of the designed system. “Inputs” is what “enters the system”, “exits” - this is what “leaves” (that is, what the system forms).



With the sensors previously figured out ...



Step two - create a simulator





In fact, in order for us to debug the work of our management process, we need to also write a small imitator. In essence, the same management process, only imitating the “necessary equipment”. Therefore, we start with it, as a simpler process ( with a stretch, we assume that we are TDD fans ).

As a result, we have two more directories in the project.



src / Algorithms / Controller - process control (solving the task)

src / Algorithms / Imitator - simulator for adjusting the control process



Since there are still a couple of service directories (which will be discussed later), I select the processes in a separate subdirectory "Algorithms" .



So, the simulator task (the simplest "stupid" option):



Creating an xml file describing the simulator



It is time to talk about one auxiliary, but very important uniset-utility: uniset2-codegen .

With the help of it, on the basis of a special xml-description, a "skeleton" of the process is generated, containing the entire "routine part" of work. Then, "inheriting" from the generated class (skeleton), it is enough just to implement the necessary functionality (by redefining the virtual functions).



The xml-description file is a simple xml file which describes the “inputs” and “outputs”

for this process (if we consider it a “black box”). It is important to understand that:



To realize all this, let's get right to the point. For the simulator we have:

Two entrances are commands to fill / empty and one exit is the level actually simulated in the tank. Then its part of the xml file describing the inputs / outputs will look like this:



  <smap> <item name="Level_s" vartype="out" iotype="AI" comment="   "> <item name="cmdLoad_c" vartype="in" iotype="DO" comment="  "> <item name="cmdUload_c" vartype="in" iotype="DO" comment="  «»"> </smap> 


name - sets the name of the “variable” (in the class skeleton); From this name will be generated class fields containing (id of the sensor to which the input / output is attached), as well as a variable containing the current value.

vartype - defines the type of the "input" or "output" variable. The input is that which is “read”, “output” - that which is “written”.

comment - turns into a comment in doxygen style (/ *! < * /)



Accordingly, in general, we can subsequently launch several simulators, each of which will be tied to its sensors ...



For those who are interested in more ..
The full version of the description file looks like this:
 <?xml version="1.0" encoding="utf-8"?> <!-- name -   msgcount -       sleep_msec -       type ==== in -  (  ) out -  () --> <Imitator> <settings> <set name="class-name" val="Imitator"/> <set name="msg-count" val="30"/> <set name="sleep-msec" val="150"/> </settings> <variables> <item name="stepVal" type="long" const="1" default="6" comment="  ()"/> <item name="stepTime" type="long" const="1" default="500" comment="   , "/> </variables> <smap> <item name="Level_s" vartype="out" iotype="AI" comment="   "/> <item name="cmdLoad_c" vartype="in" iotype="DO" comment="  "/> <item name="cmdUload_c" vartype="in" iotype="DO" comment="  ''"/> </smap> <msgmap> </msgmap> </Imitator> 




Writing simulator code



Having formed the inputs / outputs, we can now generate a skeleton. Without going into details, this is done with the command:

 uniset2-codegen -n Imitator --ask --no-main imitator.src.xml 


the --ask parameter says to generate a process based on change notifications (ordering sensors)

--no-main parameter - says not to generate main.cc we will write our own.

The -n parameter is the name of the class for which the skeleton is being generated.

In general, this command is added to
Makefile.am
 bin_PROGRAMS = imitator BUILT_SOURCES = Imitator_SK.h Imitator_SK.h imitator_LDADD = $(top_builddir)/lib/libUniSetExample.la #imitator_CPPFLAGS = imitator_SOURCES = Imitator_SK.cc Imitator.cc imitator-main.cc Imitator_SK.h Imitator_SK.cc: imitator.src.xml @UNISET_CODEGEN@ -n Imitator --topdir $(top_builddir)/ --ask --no-main imitator.src.xml clean-local: rm -rf *_SK.cc *_SK.h *.log 




As a result, two files will be generated:

Imitator_SK.h - header

Imitator_SK.cc - implementation



These are very interesting files, for studying what is being done there, but so far we are not up to them ...

So we are laying our own implementation. We create two files in which our work logic will be implemented.

Imitator.h

Imitator.cc



Let's look at Imitator.h for details.

Imitator.h
 #ifndef Imitator_H_ #define Imitator_H_ // ----------------------------------------------------------------------------- #include <string> #include "Imitator_SK.h" // ----------------------------------------------------------------------------- /*! \page_Imitator     (   ) - \ref sec_imitator_Common \section sec_loadproc_Common      ""(cmdLoad_c)     ( ).   ""(cmdUnload_c)     ( ). */ class Imitator: public Imitator_SK { public: Imitator( UniSetTypes::ObjectId id, xmlNode* cnode, const std::string& prefix = "" ); virtual ~Imitator(); enum Timers { tmStep }; protected: virtual void sensorInfo( const UniSetTypes::SensorMessage* sm ) override; virtual void timerInfo( const UniSetTypes::TimerMessage* tm ) override; private: }; // ----------------------------------------------------------------------------- #endif // Imitator_H_ 




In a uniset system, every object that wants to receive notifications about sensors and generally interact with the outside world in some way must have a unique identifier within the system. In addition, our process requires inputs / outputs to be associated with specific sensors, for this in configure.xml each process has its own configuration section.

As a result ... in the constructor, we pass the object identifier, as well as a pointer to a specific xml-node with the settings of this process. In addition, there is still prefix (this is for processing command line arguments, it will be shown later how to use it).



Based on the simulator description, we need processing of commands (inputs) and a timer to implement filling / emptying in time. To do this, define (redefine) only two functions:



  //     : virtual void sensorInfo( const UniSetTypes::SensorMessage* sm ) override; //   : virtual void timerInfo( const UniSetTypes::TimerMessage* tm ) override; 


First, let's take a closer look at the implementation of sensorInfo ()



 void Imitator::sensorInfo( const UniSetTypes::SensorMessage* sm ) { if( sm->id == cmdLoad_c ) { if( sm->value ) { myinfo << myname << "(sensorInfo):   ''..." << endl; askTimer(tmStep,stepTime); //     } } else if( sm->id == cmdUnload_c ) { if( sm->value ) { myinfo << myname << "(sensorInfo):   ''..." << endl; askTimer(tmStep,stepTime); //     } } } 


Everything is simple here ... the “fill” command came (cmdLoad_c = 1) - we start the timer ... (you never slept).

The “empty” command came (cmdUnload_c = 1) - we also start the timer. All logic is contained in the timer.

(of course, you can implement this all in a different way ... I just need to somehow demonstrate how to work with timers :))



Let's see the implementation of timerInfo ().

  void Imitator::timerInfo( const UniSetTypes::TimerMessage* tm ) { if( tm->id == tmStep ) { if( in_cmdLoad_c ) //  .. { out_Level_s += stepVal; if( out_Level_s >= maxLevel ) { out_Level_s = maxLevel; askTimer(tmStep,0); //   ( ) } return; } if( in_cmdUnload_c ) //   { out_Level_s -= stepVal; if( out_Level_s <= minLevel ) { out_Level_s = minLevel; askTimer(tmStep,0); //   ( ) } return; } } } 


When the timer tmStep is triggered (we can have as many timers as possible). We see which team we are holding now, if we fill ( in_cmdLoad_c = 1 ), then we increase out_Level_s by an increment step ( stepVal ), if we empty out, we decrease out_Level_s by a step stepVal . At the same time, check max and min.



And now a small analysis of the whole kitchen ...



"From" in the class appeared fields in_cmdLoad_c, in_cmdUnload_c, out_Level_s.


They are actually generated in the skeleton.



For all the "inputs" ( vartype = "in", see the xml-description file), the following fields are generated in the skeleton

name - the field containing the ID of the sensor with which this "input" is associated

in_name - field containing the current sensor value



For all the "outputs" ( vartype = "out", see the xml-description file), the following fields are generated in the skeleton

name - the field containing the identifier of the sensor with which this "output" is associated

out_name - field for setting the sensor.



how does the timer work


In the same skeleton there is a special function askTimer (timerId, msec, count)

timerId - timer identifier (this is some kind of your number so that you can distinguish between timers)

msec is the time for which the timer is set in milliseconds. If set to 0, the timer is disabled.

count - optional parameter how many times the timer will work (by default it will come every msec milliseconds until it is stopped).



When the timer is “started”, the timerInfo function (const UniSetTypes :: TimerMessage * tm) is called every msec milliseconds to which the TimerMessage structure containing the identifier of the activated timer is transmitted.



It is important :

  1. it's still not realtime and therefore the timers guarantee only that they will not work “earlier” than the specified time.
  2. Timers are not asynchronous (!) processing of messages is carried out sequentially, if you get stuck somewhere in the handler ( sensorInfo for example) by calling there some sleep (xxx) then the timer will “linger” for this time.
  3. timers must be a multiple of the minimum “quantum” (step) of the time specified in the xml file in the parameter
     <set name="sleep-msec" val="150"/> 
    Those. if 150 ms is indicated here, then the 50 ms timer will still work after 150 ms.


While I propose not to pay attention to these details, about them later ...



how sensorInfo works


The sensorInfo () function is called whenever the value of an “input” changes. In fact, notification of a change in a particular sensor comes from SharedMemory (if the process works on notifications).



So with the logic decided. It remains to write the actual main ().

Just show the code, and then comment on ...



main () function




main.cc
 #include <UniSetActivator.h> #include "UniSetExampleConfiguration.h" #include "Imitator.h" // ----------------------------------------------------------------------------- using namespace UniSetTypes; using namespace std; // ----------------------------------------------------------------------------- int main( int argc, const char** argv ) { try { auto conf = uniset_init(argc, argv); auto act = UniSetActivator::Instance(); auto im = UniSetExample::make_object<Imitator>("Imitator1", "Imitator"); act->add(im); SystemMessage sm(SystemMessage::StartUp); act->broadcast( sm.transport_msg() ); act->run(false); return 0; } catch( const Exception& ex ) { cerr << "(imitator): " << ex << endl; } catch( const std::exception& ex ) { cerr << "(imitator): " << ex.what() << endl; } catch(...) { cerr << "(imitator): catch(...)" << endl; } return 1; } // ----------------------------------------------------------------------------- 




First you need to download the same project configuration and initialize everything you need to work libuniset. All this is done in the function uniset_init (argc, argv) , which returns a global pointer (shared_ptr) conf (configuration), for all needs. You can also get it anywhere in the program.
 auto conf = uniset_conf(); 


In this example, we are not using it (explicitly, but actually using make_object ).

In uniset_init () , the configure.xml file is downloaded (the file with this name is the default download attempt). It can be overridden either by passing the third argument to
 uniset_init(argc,argc,"myconfigure.xml") 
, or in the command line by setting the parameter --confile myconfile.xml .



All uniset objects must be "activated", after which they will be able to receive notifications and generally interact with the outside world. For this, there is a UniSetActivator in the system (as can be seen from the code, this is singleton ). The activation process is simple: create an object and add it to the activator (more precisely, shared_ptr to the object).



To create an object of our class Imitator , as described above, we need to pass it its unique identifier and pointer to the xml-node with the settings. For convenience and clarity, the UniSetExampleConfiguration declared the template function make_object <> which transmits the text name of the object (name in the section from configure.xml) and the configuration node <XXXNodeName name = “ObectName /> for this object in configure.xml . In the make_object <> function, all the “magic” of getting an ObjectId object by these parameters and finding xmlNode * in configure.xml is already hidden. From the example it can be seen that the object (identifier) ​​with the name "Imitator1" and the configuration section <Imitator name = "Imitator1" must exist in configure.xml ... />



Here’s what it looks like in configure.xml

 <objects name="Objects" section="Objects"> ... <item id="20001" name="Imitator1"/> ... </objects> 


And the setup section is usually created in the settings section.

  <settings> <Imitator name="Imitator1"/> ... </settings> 


At this coding simulator ended.



Simulator Configuration



The configuration process itself is to populate the configure.xml and bind the sensors to the inputs and outputs of the process. To help in this matter, there is a special utility uniset-linkeditor . This is the bindings editor, which allows you to graphically make bindings, as well as edit some other parameters declared in the xml-description file. Uniset-linkeditor itself is written in python. It is installed as a separate package. So we need to install it first.

 apt-get install uniset-configurator 


In the src / Algorithms / Imitator directory there is a special script edit_imitator.sh that launches the binding editor. Actually, we need to connect our teams ( inputs ) with the CmdLoad_C and CmdUnload_C sensors , and tie the level in the tank ( output of our simulator ) to the Level_AS sensor. This can be done manually, there is nothing difficult in it ... As a result, the configuration section for our simulator (in the configure.xml project file) should take the form

 <Imitator name="Imitator1" Level_s="Level_AS" cmdLoad_c="CmdLoad_C" cmdUnload_c="CmdUnload_C"/> 


As you can see, the bindings are easy to create. Each input or output should be installed in accordance with the sensor in the system.



Subtotal



Despite the fact that there were “many letters”, in fact we did quite a bit



And that's all ...



It remains now to try to run.

This will be in the next part ...



For those who are interested:

Source: https://habr.com/ru/post/278535/



All Articles