Userdefined States
A sequence may be significant or extensive enough to warrant being treated as a distinct state within the state machine. This is referred to as a user-defined state, which may be linked to any Sequence. It follows the same rules as the automatic sequence, which include:
- To start a userdefined state, the statemachine has to be
Idle
(no error, not in init and not stopped) - If an error occurs during execution of the sequence, it will stop and the
Fault Reaction
sequence will be starting resulting inFault
state - If the statemachine is in
Fault
orStopped
, it can only get started again until the system was homed throughHoming
sequence - If this sequence ends normally the state machine will transition to
Idle
again
Add a userdefined state
The following steps must be taken to add a user-defined state:
- Add an enumeration with the desired user defined states, be sure to have the first entry set to
ZApplication.UnitStateMachineState.Last
; In the template there is already an enumeration prepared which is usually called<Unitname>State
and declared in the units subfolder_DataTypes
- Add Begin and End step for this state in the corresponding step enumeration
- Add a sequence function block into the states folder of the unit
- Instantiate the newly added sequence in the unit function block
- In FB_init constructor of the unit add a transition allowed entry to this new state
- Add a boolean variable to the communication request struct, ideally in the same name of the state
- Add an observer at the unit to observe the boolean variable which we created some steps before
Lets go through this step by step by adding a OpenDoor
and CloseDoor
State to a solution. Open solution explorer on the left and navigate to ZApp.Unit.<Unitname>._DataTypes.<UnitName>State
. Delete the default UserdefinedState and enter your new ones. In our example this would be:
{attribute 'qualified_only'}
TYPE GarageState :
(
OpenDoor := ZApplication.UnitStateMachineState.Last+1,
CloseDoor
);
END_TYPE
Open ZApp.Unit.<Unitname>._DataTypes
and open the step enumeration called
{attribute 'qualified_only'}
{attribute 'to_string'}
TYPE GarageStep :
(
Undefined := 0
,BootBegin
,BootEnd
,StopBegin
,StopEnd
,AutomaticBegin
,AutomaticEnd
,GoHomeBegin
,GoHomeEnd
,OpenDoorBegin // < add begin and end step here
,OpenDoorEnd
,CloseDoorBegin // < add begin and end step here
,CloseDoorEnd
,FaultReactionBegin
,FaultReactionEnd
);
END_TYPE
Now lets open ZApp.Unit.<Unitname>._States
. Create a new function block and input the following code fragment. Repeat this step for the CloseDoor state:
FUNCTION_BLOCK GarageSequenceOpenDoor EXTENDS GarageSequence IMPLEMENTS ZCore.ISequence
VAR
_step : ZCore.Step(GarageStep.OpenDoorBegin, GarageStep.OpenDoorEnd);
END_VAR
IF NOT Busy THEN
RETURN;
END_IF
IF OnStart(_step) THEN
; // init custom sequence variables here ...
END_IF
IF OnStop() THEN
RETURN; // external stop was triggered
END_IF
IF OnHalt() THEN
; // Halting := TRUE; // Uncomment to acknowledge that the sequence is halting
END_IF
REPEAT
LogStep();
CASE _step.Index OF
(* -------------------------------------------------------- *)
GarageStep.OpenDoorBegin:
(* -------------------------------------------------------- *)
IF _step.OnEntry()
THEN
;
END_IF
// Await(obj1:=, nextStep:=GarageStep.OpenDoorEnd);
_step.SetNext(GarageStep.OpenDoorEnd);
(* -------------------------------------------------------- *)
GarageStep.OpenDoorEnd:
(* -------------------------------------------------------- *)
SetBusy(FALSE);
ELSE
Abort('sequence contains unhandled step');
END_CASE
UNTIL _step.IsNotRepeatable() OR_ELSE NOT Busy END_REPEAT
Open the unit function block under ZApp.Unit.<Unitname>
and insert an instance of this newly added state function block
FUNCTION_BLOCK GarageUnit EXTENDS ZApplication.Unit IMPLEMENTS IGarage, ZAux.IStateMachineListener
VAR
_equipment : GarageEquipment(THIS^);
_data : GarageData;
_com : REFERENCE TO GarageCom;
_boot : GarageSequenceBoot(THIS^);
_automatic : GarageSequenceAutomatic(THIS^);
_stop : GarageSequenceStop(THIS^);
_gohome : GarageSequenceGoHome(THIS^);
_faultReaction : GarageSequenceFaultReaction(THIS^);
_openDoor : GarageSequenceOpenDoor(THIS^); // < add this line here
_closeDoor : GarageSequenceCloseDoor(THIS^); // < add this line here
_statemachine : ZApplication.UnitStateMachine;
_observeTimer : ZAux.Timer;
END_VAR
Now we have to integrate this new state to the statemachine and allow a transition to this state. Open ZApp.Unit.<Unitname>.FB_init
and add the line as seen in the example below:
_name := name;
// initialize local logger decorator
_loggerDecorator.FB_init(bInitRetains, bInCopyCode, name, ZModuleProgram.Logger);
_alarmingDecorator.FB_init(bInitRetains, bInCopyCode, name, ZModuleProgram.Alarming);
// setup data
_data.Config REF= configdata;
_data.Machine REF= machinedata;
_data.Calibration REF= calibrationdata;
_com REF= com;
// ------- sequence initialization, map sequences of this unit to the statemachine -------
_statemachine.SetSequence(ZApplication.UnitStateMachineState.Boot, _boot);
_statemachine.SetSequence(ZApplication.UnitStateMachineState.FaultReaction, _faultReaction);
_statemachine.SetSequence(ZApplication.UnitStateMachineState.Gohome, _gohome);
_statemachine.SetSequence(ZApplication.UnitStateMachineState.Stop, _stop);
_statemachine.SetSequence(ZApplication.UnitStateMachineState.Automatic, _automatic);
_statemachine.SetUserdefinedStateSequence(GarageState.OpenDoor, 'OpenDoor', _openDoor); // < add this line here
_statemachine.SetUserdefinedStateSequence(GarageState.CloseDoor, 'CloseDoor', _closeDoor); // < add this line here
_statemachine.SetName(_name);
_statemachine.SetListener(THIS^);
_statemachine.SetLogger(_logger);
// set initial state
_statemachine.SetState(ZApplication.UnitStateMachineState.Boot);
_statemachineIntrf := _statemachine;
The newly added state is almost ready for use. In order to be able to invoke this state externally, we need to include some booleans in the publish and subscribe communication structure:
Open ZApp.Unit.<Unitname>._Com.GarageComRequest
and add the following line:
TYPE GarageComRequest :
STRUCT
Start : BOOL;
Stop : BOOL;
Gohome : BOOL;
OpenDoor : BOOL;
CloseDoor : BOOL;
END_STRUCT
END_TYPE
Lastly, open the unit and add an observer to the body to respond to changes in the communication variable for this newly added state.
IF _observeTimer.Done THEN
_observeTimer.WaitAsync(0.1);
// monitor _com for requested actions (e.g. start homing, start automatic mode, ...)
ObserveRequest(_com.Subscribe.Request.Start, ZApplication.UnitStateMachineState.Automatic);
ObserveRequest(_com.Subscribe.Request.Stop, ZApplication.UnitStateMachineState.Stop);
ObserveRequest(_com.Subscribe.Request.GoHome, ZApplication.UnitStateMachineState.GoHome);
ObserveRequest(_com.Subscribe.Request.Halt, ZApplication.UnitStateMachineState.Halt);
ObserveRequest(_com.Subscribe.Request.OpenDoor, GarageState.OpenDoor); // < add this line here
ObserveRequest(_com.Subscribe.Request.CloseDoor, GarageState.CloseDoor); // < add this line here
// provide relevant information via ADS
_com.Publish.Request.Stop := _stateMachine.IsTransitionAllowed(ZApplication.UnitStateMachineState.Stop);
_com.Publish.Request.Start := _stateMachine.IsTransitionAllowed(ZApplication.UnitStateMachineState.Automatic);
_com.Publish.Request.GoHome := _stateMachine.IsTransitionAllowed(ZApplication.UnitStateMachineState.GoHome);
_com.Publish.Request.Halt := _stateMachine.IsTransitionAllowed(ZApplication.UnitStateMachineState.Halt);
_com.Publish.Request.OpenDoor := _stateMachine.IsTransitionAllowed(GarageState.OpenDoor); // < add this line here
_com.Publish.Request.CloseDoor := _stateMachine.IsTransitionAllowed(GarageState.CloseDoor); // <add this line here
_com.Publish.State := _stateMachine.State();
END_IF
Add Start-Method to Interface
Last but not least, if we want to run this new Userdefined-State and start it from another unit (via linked units) we need to add a proper method to the a'la RunAutomaticAsync
to the Unit and of course to the Unit-Interface.
In the solution explorer open the unit FB and add a new method into the UnitInterface
folder of the FB. To separate starting
-methods from other ones in the unit-interface we have a guideline that every starting-method has an Async-suffix added to it.
In our case we have two new states OpenDoor
and CloseDoor
, so we add two new methods OpenDoorAsync
and CloseDoorAsync
.
METHOD OpenDoorAsync
VAR_INPUT
startToken : ZCore.IStartToken; //< (optional) object to check if the method was executed successfully - may be 0
unit : REFERENCE TO Unit; //< unit that wants to take control of the object
END_VAR
RunSequenceAsync(startToken, unit, GarageState.OpenDoor);
METHOD CloseDoorAsync
VAR_INPUT
startToken : ZCore.IStartToken; //< (optional) object to check if the method was executed successfully - may be 0
unit : REFERENCE TO Unit; //< unit that wants to take control of the object
END_VAR
RunSequenceAsync(startToken, unit, GarageState.CloseDoor);
How to try
Now activate your application and start homing procedure to make a transition from Init
to Idle
state for your units.
To test if all is working simply set the request boolean in the communication request structure and finally the newly added state should be called and do its work.