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:
- “GET STARTED” command - DI sensor
- current level in the tank - AI sensor
- command to turn on the “filling pump” - DO sensor
- command to turn on the “pump of emptying” - DO sensor
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.
- DI - Digital Input
- DO - Digital Output
- AI - Analog Input
- AO - Analog Output
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):
- Upon the command “fill” imitate the filling of the tank (increase in the value of the analog level sensor in the tank).
- At the command "empty" imitate the emptying of the tank (reducing the value of the analog level sensor in the tank).
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:
- “Inputs” and “outputs” are essentially sensors that your process “receives” (inputs) or “exposes” (outputs)
- what is the “entrance” for one process, it may well be the “exit” for another.
- Inputs / outputs are not the sensors themselves, but simply class fields, which later (during the start) are tied to specific sensors.
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"?> <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
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_
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:
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);
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 )
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 :
- it's still not realtime and therefore the timers guarantee only that they will not work “earlier” than the specified time.
- 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.
- 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
- Created (filled) the project file configure.xml
- We created a file describing our simulator imitator.src.xml and generated a “skeleton” of the class
- Wrote the implementation of our logic ( only two functions )
- Wrote ( almost stereotyped ) main ()
- Configured our simulator ( tied specific sensors )
And that's all ...
It remains now to try to run.
This will be in the
next part ...
For those who are interested: