Table of Contents

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 in Fault state
  • If the statemachine is in Fault or Stopped, it can only get started again until the system was homed through Homing 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 Step. Add a begin and end step for each state into this enumeration:

{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.