📜 ⬆️ ⬇️

If the project "Theater", use the actors ...

In this article there will be a story about the experience of using the actor approach in one interesting project of an automated control system for the theater. This is exactly the impression of use, nothing more.


Recently, I was able to participate in one very interesting task - modernization, and in fact - the development of a new automated control system for lifting slots for one of the theaters.


Modern theater (if it is large) is a rather complicated organization. A lot of people, equipment and various systems are involved in it. One of such systems is the control system for “raising and lowering” scenery on the stage. Modern performances, and more operas and ballets, are becoming more and more saturated with technical means every year. It uses a lot of complex scenery and their movement during the action. The scenery is actively used in the director's ideas, expanding the meaning of what is happening and even “playing his own supporting role”). In general, it was very interesting to get acquainted with the behind-the-scenes life of the theater and find out what happens there during the performances. After all, ordinary viewers see only what is happening on the stage.


But this article is still technical and wanted to share the experience of using the actor approach to implement control. And also share the impression of using one of the few C ++ actor frameworks - sobjectizer .


Why precisely he? We looked at him for a long time. There are articles on Habré, he has excellent detailed documentation with examples. The project is quite mature. A quick glance at the examples showed that the developers operate with “familiar” concepts (states, timers, events), i.e. it was not expected big problems with understanding and development, for use in our project. And yes, not least, the developers are adequate and friendly, ready to help with advice (in Russian) . So we decided to try ...


What are we doing?


So, what is our "object of management". The pitch lifts system is 62 pillars (metal pipes) all the way across the width of the stage, hanging above this very stage, approximately every 30-40 cm from the edge of the stage. Standings themselves are suspended on cables and can rise up or fall down onto the stage (vertical movement). In each performance (either opera or ballet), a piece of a footboard is used for scenery. The scenery is suspended on them and moved (if required by the script) during the action. The movement itself is carried out at the command of operators (they have special control panels) using the “engine - cable - counterweight” system (approximately the same way as elevators in houses). The engines are located at the edges of the stage (on several tiers), so that they are not visible to the viewer. All engines are divided into 8 groups and each group has three frequency converters (IF). Three engines can be simultaneously involved in each group, each connecting to its own converter. Total we have a system of 62 engines and 24 converters, which we must manage.


Our task was to develop an operator interface to manage this economy, as well as implement control algorithms. The system includes three control stations. Two control posts are located directly above the stage and one post is located in the engine room (where the control cabinets are located) and is intended to supervise the work for the electrician on duty. Control cabinets are equipped with controllers that execute commands, control PWM, power supply to the motors, tracking the position of the picket. On the two upper panels there are monitors, a system unit, where the control algorithms and the trackball as a “mouse” are spinning. An Ethernet network is used between the control panels. Up to each control cabinet there is an RS485 channel (i.e. 8 channels) from each of the two control panels. The control can be carried out simultaneously from both consoles (which are above the stage), but at the same time only one of the consoles (appointed by the operator as the main operator) is exchanged with the cabinets, the second console is considered redundant at this moment and the exchange is disconnected on it.


And here are the actors


From the point of view of algorithms, the whole system is built on events. Either these are some changes in the sensors, or the actions of the operator, or the onset of a certain time (timers). And the system of actors who process incoming events very well falls on such algorithms, form some kind of response actions and all this depending on their state. In sobjectizer, all these mechanisms go out of the box. The basic principles on which such a system is built can be attributed: interaction between actors takes place through messages, actors can have states and move between them, in each state the actor processes only those messages that interest him at the moment. Interestingly, conceptually in sobjectizer, work with actors is separated from work with workflows. Those. You can describe the actors you need, implement their logic, realize their interaction through messages. But then separately decide the issue of allocation of flows (resources) for their work. This is ensured by the so-called "dispatchers" who are responsible for this or that policy of working with threads. For example, there is a dispatcher who allocates a separate thread for each actor to work, there is a dispatcher that provides a pool of threads (i.e., there can be more actors than streams) with the ability to specify the maximum number of threads, there is a dispatcher that allocates one stream for all. The presence of dispatchers provides a very flexible mechanism for setting the actor system to fit your needs. It is possible to combine groups of actors to work with one of the dispatchers, while changing one type of dispatcher to another is essentially changing one line of code. According to the authors of the framework, writing a unique dispatcher is also not difficult. In our project, this was not necessary, because everything we needed was already in the sobjectizer.


Another interesting feature is the existence of the notion of “cooperation” of actors. A co-operation is a group of actors that can either completely exist or is completely destroyed (or does not start) if at least one actor in the co-operation could not start work or was completed. I'm not afraid even to bring such an analogy ( even though it is from another "opera" ) that the concept of “cooperation” is like the concept of “pods” in the now fashionable Kubernetes, it only seems in the sobjectizer, it appeared earlier ...


At the time of creation, each actor joins in cooperation (cooperation may consist of one actor), is tied to a particular dispatcher, and is launched into work. At the same time, actors (and cooperations) can (easily) be created dynamically in large numbers and, as developers promise, this is not expensive. All actors exchange among themselves through “ mailboxes ” (mbox). This is also quite an interesting and strong concept in sobjectizer. It provides a very flexible mechanism for handling incoming messages. First, there can be more than one recipient behind the box. It is really very convenient. For example, a box is created in which events from external sensors arrive and each actor subscribes to events of interest to it. It provides work in the style of "publish / subscribe". Secondly, the developers have made it possible to create relatively easily their own mailbox implementations that can pre-process incoming messages (for example, somehow filter them or distribute them in a special way between consumers). In addition, each actor has its own box and can even send a “link” to it in messages to other actors, for example, so that they can send some kind of notification as a return response.


In our project, in order to ensure the independence of the engine groups among themselves, as well as to ensure the “asynchronous” operation of the engines within the group, all control objects were divided into 8 groups (by the number of control cabinets), each of which was allocated three workers flow (because the group can simultaneously operate no more than three engines).
It should also be said that sobjectizer (in the current version 5.5) does not contain interprocess and network interaction mechanisms and leaves this part to the developers. The authors did this quite deliberately , so that the framework was more “easy”. Moreover, the network interaction mechanisms “once” existed in previous versions, but were excluded. However, this does not cause any inconvenience, because the actual network interaction is very much dependent on the tasks to be solved, the exchange protocols used, etc. Here there can be no universal implementation optimal for all cases.


In our case, for network and interprocess communication, we used one of our long-time developments - the library libuniset2 . As a result, the architecture of our system looks like this:



So let me remind you that we have 62 engines. Each motor can be connected to the inverter, the coordinate to which it is necessary to arrive and the speed with which it is necessary to move can be set to the corresponding stanket. In addition, the engine has the following states:



As a result, each “engine” is represented in the system by an actor that implements the logic of state transitions, processing events from sensors and issuing control commands. In sobjectizer, actors are easy to create; you just need to inherit your class from the base class so_5 :: agent_t. In this case, the constructor is obliged to accept the first argument “some” context so_5 :: context_t, the remaining arguments are determined by the need of the developer.


class Drive_A: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); ... } 

Since This article is not a tutorial, so I will not give here the detailed texts of the descriptions of classes or methods. The article just wanted to show how easy (in a few lines) with the help of sobjectizer all this is implemented. Let me remind you that the project has excellent detailed documentation , with a bunch of different examples.


And what is the "state" of these actors? What are we talking about?


The use of states and transitions between them for the ACS is generally a native topic. This “concept” is very good at handling events. In sobjectizer, this concept is supported at the API level. In an actor class, states are fairly easy to declare.


 class Drive_A final: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); virtual ~Drive_A(); // состояния state_t st_base {this}; state_t st_disabled{ initial_substate_of{st_base}, "disabled" }; state_t st_preinit{ substate_of{st_base}, "preinit" }; state_t st_off{ substate_of{st_base}, "off" }; state_t st_connecting{ substate_of{st_base}, "connecting" }; state_t st_disconnecting{ substate_of{st_base}, "disconnecting" }; state_t st_connected{ substate_of{st_base}, "connected" }; ... } 

and then for each state, the developer determines the necessary handlers. Often it is required to do some actions at the entrance to the state and at the exit from it. In sobjectizer this is also provided, you just as easily define your handlers for these events (“state entry”, “state exit”). It is felt that the developers in the past have a great automated control system ...


Event handlers


Event handlers are the place where your application logic is implemented. As mentioned above, a subscription is made to a specific mailbox and for a certain state of the actor. If the actor has no states explicitly declared in the code, then it is implicitly in the special state “default_state”. In different states you can define different handlers for the same events. If you did not specify an event handler in this mailbox, it will simply be ignored (that is, it simply will not exist for the actor).


The syntax for defining handlers is very simple. You only need to specify your function. No specification of any types or template arguments is required. Everything is derived automatically from the function definition. For example:


 so_subscribe(drv->so_mbox()) .in(st_base) .event( &Drive_A::on_get_info ) .event( &Drive_A::on_control ) .event( &Drive_A::off_control ); 

Here is an example of subscribing to events in a specific mailbox for the state st_base. What is interesting, in this example, st_base is the base state for other states and, accordingly, this subscription will be valid for all states “inherited” from st_base. This approach allows you to get rid of "copy-paste" to determine the same handlers for different states. At the same time, in a specific state, you can either override the specified handler or “disable” it (suppress).


There is another way to define handlers. This is a direct definition of lambda functions. This is a very convenient way, because often handlers are short functions in a couple of actions, something to send or switch a state to someone.


 so_subscribe(drv->so_mbox()) .in(st_disconnecting) .event([this](const msg_disconnected_t& m) { ... st_off.activate(); }) .event([this]( const msg_failure_t& m ) { ... st_protection.activate(); }); 

At first, this syntax seems complicated. But just a few days of active development you get used to it and he even starts to like it. Because all the logic of the work of an actor in one state or another can fit into a rather short code and it will be all before your eyes. For example, in the example shown, in the “disconnected” state (st_disconnecting), there is either a transition to the “disconnected” (st_off.) State or to the “protected” (st_protection) state if a failure message has been received. Such code is quite easy to read.


By the way, for simple cases when an event just needs to go into some state, there is an even shorter syntax:


 auto mbox = drv->so_mbox(); st_off .just_switch_to<msg_connected_t>(mbox, st_connected) .just_switch_to<msg_failure_t>(mbox, st_protection) .just_switch_to<msg_on_limit_t>(mbox, st_protection) .just_switch_to<msg_on_t>(mbox, st_on); 

Control


How does the management of all this economy. As mentioned above, two remote controls are provided for direct control of the movement. On each console there is a monitor, a manipulator (trackball) and a speed master (besides the “computer” hidden in the console on which everything is spinning and piles of all kinds of converters). The system has several modes of movement control. Manual and "script mode". About the "script mode" will be discussed further, and now a little about the "manual mode". In this mode, the operator selects the rod he needs, prepares it for movement (connects the engine to the inverter), sets the stamp for the stem (target position) and as soon as it sets the speed more than zero, the stem marks begin to move. To set the speed, a special physical master is used, in the form of a “potentiometer with a handle”, but there is also a “screen master” of speed. The more "turned", the louder faster rides. The maximum speed of the movement is limited and equal to 1.5 m / s. Speed ​​knob - one for all. Those. in manual mode, all operator-connected wall stakes move at the same set speed. Although they can move in different directions (depending on where the operator sent them). Of course, it is difficult for a person to keep track of more than two or three pants at the same time, so usually there is not much movement in manual mode. From two stations, operators can simultaneously manage each of their own pillars. In addition, each console (operator) has its own speed controller.


From the point of view of implementation, the manual mode does not contain any particular logic. The command to connect the engine comes from the graphical interface, converted into a message to the corresponding actor, which fulfills it. Passing through the state "off" -> "connecting" -> "connected". The same with setting the position for moving the pillar and setting the speed. All these events arrive to the actor in the form of messages to which he responds. Is it possible to note that the graphical interface and the management process itself are different processes and between them there is an "interprocess" interaction through the "sensors" using libuniset2 .


Script execution mode (again, these actors?)


In fact, manual control mode is mainly used only for hanging during rehearsals or in simple cases. The main mode in which the control is in progress is the “script execution mode” or, briefly, the “script mode”. In this mode, each foot moves to its point with the parameters specified in the script (speed and target mark). For the operator, control in this mode consists of two simple commands:



The whole scenario is divided into so-called "agenda". An agenda is some kind of movement of a group of shtanets. Those. each agenda includes a group of stunkets, with the target speed set for each target and brand where it is necessary to arrive. In fact, the script is divided into acts, acts are divided into pictures, pictures are divided into subpoenas, and subpoenas already consist of “goals” for specific pieces. But from the point of view of management, this division is not important, since It is in the agenda that the specific parameters of the movement are indicated.


To implement this mode, again, the system of actors was the best suited. A “script player” was developed that creates a group of special actors and launches them into work. We have developed two types of actors: actors performing for the assignment for a specific stem and a coordinating actor who distributes tasks between performers. Moreover, performing actors are created as needed, if at the time of the next team is not free. The actor coordinator is responsible for creating and maintaining a pool of performing actors. As a result, management looks something like this:



It is worth noting that there are additional parameters on the agenda. For example, start driving with a delay of N seconds or start driving only after a separate special operator command. Therefore, the list of states for each actor-performer is quite large: “ready for the execution of the next command”, “ready for movement”, “movement delay”, “waiting for the operator’s team”, “movement”, “execution completed”, “failure” .


After the stanket has successfully (or not) reached the specified mark, the actor will notify the coordinator about the completed task. The coordinator either gives the command to disable this engine (if it no longer participates in the current agenda) or issues new movement parameters. In turn, the actor-performer receiving a command to turn off the engine, turns it off and goes into a waiting state for new commands, or starts to execute a new command.


Due to the fact that sobjectizer is quite thoughtful and convenient API for working with states, the implementation code turned out to be quite concise. For example, the delay for movement is described in one line:


 st_delay.time_limit( std::chrono::milliseconds{target->delay()}, st_moving ); st_delay.activate(); ... 

The time_limit function sets the time limit for how much you can spend in this state and what state you need to go after the specified time has passed (st_moving).


Protection actors


Certainly during operation may occur failures. The system has requirements for handling these situations. Here, too, there was a place for the use of actors. Consider several similar protections:



It can be seen that all these protections are independent (self-sufficient) from the point of view of implementation, and must work “in parallel”. Those. Any condition can work. In this case, the logic of checking the conditions of operation of each of the protections is different, sometimes a delay (timer) is required for operation, sometimes preliminary processing of several previous measurements, etc. is required. Therefore, it was very convenient to implement each type of protection as a separate small actor. All these actors are launched in addition (in cooperation) to the main actor implementing the control logic. This approach makes it easy to add additional types of protection by simply adding another actor to the group. At the same time, the implementation of such an actor remains fairly easy and understandable, since it implements only one function.


Protection actors also have several states. Basically, they turn on (go into the "on" state) only when the engine is connected or the picket moves. When the conditions for protection are triggered, they publish a notification about the protection triggering (with the protection code and some details for logging), the main actor already responds to this notification, which, if necessary, turns off the engine and goes into protection mode.


As output ..


... of course this article is not some kind of "discovery". The actor approach has been successfully used in many systems for a long time. But for me it was the first experience of consciously using the actor approach to building control system algorithms, in a relatively small project. And the experience was quite successful. I hope I managed to show that the actors superimpose very well on the control algorithms, they found a place literally everywhere.


On the experience of previous projects, it was clear that one way or another we were implementing “something similar” (states, message exchange, flow control, etc.), but this was not a unified approach. With the use of sobjectizer we got a concise, easy development tool that takes on a lot of problems. The (explicit) use of synchronization tools (mutexes, etc.) has ceased to be necessary, there is no explicit work with threads, no implementations of the state machine. All this is in the framework, is logically interrelated and presented in the form of a convenient API, moreover, without losing control over the details. So the experience was interesting. To those who still doubt, I recommend paying attention to the actor approach and to the sobjectizer framework in particular. He leaves positive emotions.


And the actor approach really works! Especially in the theater.



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