SObjectizer
5.7
|
Agent in SObjectizer is a finite-state machine.
The behaviour of an agent depends on the current state of the agent and the received message. An agent can receive and process different messages in each state. In other words an agent can receive a message in one state but ignore it in another state. Or, if an agent receives the same message in several states, it can handle the message differently in each state.
Let’s imagine a simple agent which controls LED indicator on some device.
It receives just one signal turn_on_off. When LED indicator is off this signal should turn indicator on. If LED indicator is on this signal should turn indicator off.
This logic can be represented by simple statechart:
It's easy to see that led_indicator agent requires two states: off
and on
. They can be directly expressed in C++ code via usage of so_5::state_t
class. The definition of new state for an agent means creation of new instance of state_t.
States are usually represented as members of agent’s class:
A state can have a textual name:
It could be useful for debugging and logging.
There are several ways of changing agent’s state:
The current state can be obtained via so_current_state()
method:
Method so_is_active_state()
can be used for checking of state activity:
Every agent already has one state: the default one. The default state can be accessed via so_default_state()
method:
Even ad-hoc agents have the default state. But it is the only one state they have. Because there is no user-defined class for an ad-hoc agent then there is no possibility to define new states for ad-hoc agent. Thus there is no possibility to change state of ad-hoc agent.
The most important part of usage of agent’s states is subscription to a message with respect to a specific state. The simple usage of so_subscribe() and so_subscribe_self() methods leads to subscription only for the default agent’s state. It means that:
is the equivalent of:
To make subscription to a message for a specific state it is necessary to use in() method in a subscription chain:
The in() methods can be chained if the same event handler is used in several states:
There is another way to make subscription for a specific state:
There are also on_enter/on_exit methods in state_t class.
Method on_enter sets up an enter handler. Enter handler is automatically called when the state is activated. Contrary on_exit method sets an exit handler. Exit handler is automatically called when the state is activated. Enter and exit handler can be a lambda-function or a pointer to member method of agent's class.
Note: enter and exit handlers must be noexcept
functions.
In the led_indicator example enter and exit handlers are necessary for on
state:
And now we can write the full code of led_indicator example:
The led_indicator example demonstrates simple finite-state machine. Since v.5.5.15 SObjectizer supports more advanced features of agents' states like composite states, shallow- and deep-history, time limitations and so on. These advanced features allow to implement agents as hierarchical state machines.
Let's see an ability to create hierarchical state machines on a slightly complex example: an agent which implements blinking of LED indicator. This agent receives turn_on_off
signal for turning blinking on and off. When blinking is turned on then agent switches LED indicator on for 1s then switches it off for 1s then switches on again and so on until the agent receives next turn_on_off
signal. A statechart for that agent can be represented as (note that this statechart represent yet more complex case which will be described later):
Agent blinking_led wll have two top level states: off
and blinking
. State blinking
is a composite state with two substates: blink_on
and blink_off
. In C++ code this can be expressed this way:
Substate blink_on
is marked as initial substate of composite state blinking. It means that when state blinking
is activated then substate blink_on
is activated too.
Moreover so_current_state()
method will return a reference to blink_on
, not to blinking
. It is because so_current_state()
always returns a reference to a leaf state in agent's state tree.
Every composite state must have the initial substate.
It means that exactly one substate must be declared by using initial_substate_of
indicator:
An attempt to activate a composite state without the initial substate defined will lead to an error at run-time.
Definition of behaviour for composite states and substates is done usual way:
It's easy to see that many events look like:
This is very typical case in complex statecharts.
There is a special method just_switch_to()
which can simplify such cases. By using this method we can rewrite states behaviour that way:
Note that reaction to turn_on_off
signal is defined only in off
and blinking
states. There are no such handles in substates blink_on
and blink_off
. It is not necessary because substates inherit event handlers from their parent states. Inheritance of event handlers means that event handler will be searched in the current state, then in its parent state, then in its parent state and so on...
In blinking_led agent an event handler for turn_on_off
signal will be searched in blink_on
and blink_off
states and then in blinking
state. That is why we don't need to create subscription for turn_on_off
in blink_on
and blink_off
states.
Now we can write the full code of blinking_led agent:
But what if we have to change the behaviour of our blinking_led agent? Let's imagine that blinking_led agent have to switch LED on for 1.5s and then switch it off for 0.7s. How can we do that?
The obvious way is to use delayed signals with different timeouts in enter handlers for blink_on
and blink_off
states. But this way is not very easy in fact...
Usage of time_limit
feature is much simpler. The time_limit feature dictates SObjectizer to limit time spent in the specific state. A clause like:
Tells the SObjectizer that agent must be automatically switched from SomeState
to AnotherState
after Timeout
spent in SomeState
.
With time_limit
feature the bllinking_led agent's implementation looks simpler and shorter:
An enter/exit handler should not change state of agent.
Class so_5::state_t
is not thread safe. It is designed to be used inside an owner agent only. For example:
Because of that it is necessary to be very careful during manipulation of agent's states outside of agent's event handlers.
There is a limitation for deep of substates in SObjectizer v.5.5.15: there can be at most 16 nested states.
All enter/exit handlers must be noexcept
functions. If an enter/exit handler throws an exception the whole application will be aborted. It is because there is no way to revert changes of agent's state change procedure if an enter/exit throws an exception.
If agent A
is successfully registered and switched to state S
then SObjectizer is guaranteed the call of exit handler for state S
in the following cases:
so_evt_finish()
as result of: deregistration of agent's coop or shutdown of SObjectizer Environment. An exit handler for S
will be called after return from so_evt_finish()
.It means that once agent is registered the SObjectizer guaratees the call of exit handler. But exit handler will not be called if agent is not registered. For example:
In this case an agent of type some_agent
will be created, the initial state for that agent will be S
, but exit handler for S
will not be called because the failure of coop registration.
What does it mean?
It means that if you need to do some important task in exit handler then consider to switch agent to that state only after successful registration of your agent. For example: in so_evt_start()
method:
SObjectizer supports states with shallow- and deep-histories. To define a state with history it is necessary to use a constructor of state_t
with argument of type state_t::history_t
:
There is state_t::clear_history()
method. But S.clear_history()
clears history only for state S
. History of any substate of S
remains intact:
Method state_t::transfer_to_state()
is intended for defering an event to another state. For example:
When agent device in state off
and receives volume_up
signal it automatically switches to state on
and handler for volume_up
is searched in state on
.
Note that transfer_to_state
can change agent state several times:
If agent demo is in state A
and receives do_something
signal it switches from A
to B
and then to C
.
The transfer_to_state
is a powerful but dangerous feature. There is no a limitation for the deep of transfers of event from one state to another. Because of that a user can create an infinite loop of transfering event between states.
Sometimes it is necessary to disable an event handler defined in parent state. It can be done by state_t::suppress
method:
Without suppressing of key_digit
and key_grid
events in dialling
state handlers for them will be automatically found in on
state (because of inheritance of event handlers).
Redefenition of enter or exit handler can be necessary when class inheritance is used. For example:
The state_t::time_limit()
method can be called several times in different places. Usually it is called only once somewhere in constructor or so_define_agent()
, for example:
But sometimes it is necessary to call time_limit
in different places. It is possible and allowed. But there is a speciality: if S.time_limit
is called when agent is already in S
state then time will ticks on from the beginning. For example:
Class state_t
was developed to be used as type of members of some agent class:
It means that lifetime of state_t
objects will be automatically synchronized with lifetime of their owners.
This synchronization of lifetimes significantly simplifies implementation of various aspects of agents in SObjectizer. Because of that the recommended way of agent's state creation is declaration of state_t
object as member of corresponding agent's class.
But it is possible to create and destroy state_t
objects manually. It is hard to imagine a situation were it can be necessary but it is possible to write something like that:
But this approach is very dangerous because raw pointers to state objects are stored in different places (in agent as a pointer to the current state, in subscription storage, in history of parent state(s) and so on). If a pointer to state which must be destroyed manually is not removed from these places a dangling pointer will be created with corresponding consequences.