open image in lightbox

How To cinnamon Part 2: CycleManager

1 Introduction

Programmable logic controllers (PLCs) are inherently driven by state machines and step-wise, sequential instructions. In industrial automation, representing these flows clearly and deterministically is essential for predictable behavior and maintainable systems.

cinnamon intentionally embraces a text-only programming model based on IEC Structured Text, rather than graphical notations such as Sequential Function Chart (SFC). Textual code is easier to review, diff, and merge; it fits modern development practices and enables straightforward CI/CD automation without the impedance mismatches of diagram-based artifacts.

At the core of cinnamon are CycleManagers. They orchestrate flow control, transitions, and state management, allowing you to implement step sequences and process flows with minimal boilerplate. Instead of hand-coding control logic, you declare intent and let CycleManagers drive execution reliably from state to state.

CycleManagers are configurable at both the step and sequence levels. This lets teams fine-tune timing, transition criteria, error handling, and recovery behavior per step while also shaping the overall execution strategy for the entire process.

States and returns

The CNM_ReturnTypes library defines two standard types for single- and multi-cycle methods/state machines/step chains, aligned with PLCopen-compliant function blocks. SingleExecutionState is used for multi-cycle operations that are edge triggered, while SingleExecutionResult applies to single-cycle executions. The shared fields ERROR, SUCCESS, and ABORTED intentionally use the same numeric values. Because the two enums are non-strict, this design makes them conditionally compatible.

TYPE SingleExecutionResult :
(
	(* Execution was finished with error *)
	ERROR	:= -1,
	(* Execution was finished successfully *)
	SUCCESS	:= 0,
	(* Execution was aborted *) 
	ABORTED	:= 3 
)INT;
END_TYPE

TYPE SingleExecutionState :
(
	(* Execution was finished with error *)
	ERROR	:= -1,
	(* Execution was finished successfully *)
	SUCCESS	:= 0,
	(* There is no execution *) 
	IDLE	:= 1,
	(* Execution is still active *)
	BUSY	:= 2,
	(* Execution was aborted *) 
	ABORTED	:= 3,
	(* Execution is paused *)
	PAUSED	:= 4
)INT;
END_TYPE

Accordingly, the same library also defines the DefaultSteps for state machines that cinnamon CycleManagers implement by default and from which they derive their current state:

DefaultSteps

VAR_GLOBAL CONSTANT
	(* step for: state machine execution was aborted *)
	ABORT	:DINT := 16#8000_0000;
	(* step for: state machine is in error *)
	ERROR	:DINT := -1;
	(* step for: state machine is sleeping/idling *)
	IDLE	:DINT := 0;
	(* first execution step of state machine *)
	INIT	:DINT := 1;
	(* step for: state machine can be paused *) 
	PAUSE	:DINT := 16#7FFF_FFFE;
	(* execution of state machine was successful *)
	SUCCESS	:DINT := 16#7FFF_FFFF;
END_VAR

2 Usage

cinnamon provides two types of CycleManagers. The SimpleCycleManager can be used anywhere in the code independently of the rest of the framework, but it requires cyclic assignment of control signals (such as stop request or stepping, if needed). It resides in the CNM_CycleManager library. The NodeCycleManager, on the other hand, requires a Node for initialization but can query signals like stop request or stepping from the Node on its own, and therefore reduces cyclic signal assignment. It resides in the CNM_OpmodeHandling library and is the recommended choice for mode step sequences.

Both CycleManagers must be called cyclically with an execute input; a pause input is available but optional. It is critical that they are called exactly once per cycle, since many internal functions rely on edge-based behavior and will malfunction if invoked more than once.

A rising edge on the execute input starts the sequence: the step transitions from IDLE to INIT (step 1) and cycleManager.state changes to BUSY. A falling edge on execute causes cycleManager.state to switch to ABORTED if the sequence has not yet reached SUCCESS — in that case the current step is also set to ABORTED for one cycle. If the sequence has already completed successfully, cycleManager.state remains SUCCESS and the step returns to IDLE.

A rising edge on the pause input places the currently active step (cycleManager.step.current) into the PAUSE state while execution is active. A falling edge on pause triggers a transition back to the last active step, effectively resuming execution where it left off.

2.1 Declaration

The SimpleCycleManager is declared as a plain instance without constructor arguments:

cycleManager :CNM_CycleManager.SimpleCycleManager();

The NodeCycleManager requires an interface pointer to its owning Node instance—typically THIS^, where THIS refers to the current instance of a class derived from AbstractNode:

cycleManager :CNM_OpmodeHandling.NodeCycleManager(THIS^);

SimpleCycleManager Inputs

FUNCTION_BLOCK SimpleCycleManager EXTENDS AbstractCycleManager
VAR_INPUT
	(* control bit to start or abort the cycle manager *)
	execute :BOOL;
	(* used for StopRequest evaluation if configured *)
	stopRequest :BOOL := FALSE;
	(* Trigger pause (requires Pausing Enabled) *)
	pause :BOOL := FALSE;
	(* if true, will acknowledge error and return to previous step *)
	resume :BOOL := FALSE;
	(* must be TRUE if stepping is enabled to proceed steps automatically *)
	stepControl :BOOL := TRUE;
END_VAR

NodeCycleManager Inputs

FUNCTION_BLOCK NodeCycleManager EXTENDS CNM_CycleManager.AbstractCycleManager
VAR_INPUT
	(* control bit to start or abort the cycle manager *)
	execute :BOOL;
	(* Trigger pause (requires Pausing Enabled) *)
	pause :BOOL := FALSE;
	(* if true, will acknowledge error and return to previous step *)
	resume :BOOL := FALSE;
END_VAR

2.2 Transitions

CycleManagers offer clear transition primitives, so you don’t have to manage steps or states manually. Sequences stay lean and focus on intent.

  • proceed() finishes the current step and moves unconditionally to the next one (current + 1 by default).

  • proceedWith(step) finishes the current step and jumps unconditionally to the specified step for branching.

  • evaluate(state) checks a SingleExecutionState: on SUCCESS the sequence advances to the next step; on ERROR it moves to the ERROR step (or to a provided error step with evaluate(state, errorStep)). While the state is BUSY, IDLE, or ABORTED, the sequence stays in the current step.

    Note: evaluate() ignores the state in the first cycle it is called, because SingleExecutionState machines may retain the last state from a previous execution. To reset and restart a SingleExecutionState machine, the boolean flag cycleManager.executeStep can be used which is FALSE in the first cycle of a step and TRUE thereafter until the step ends. Use it to drive nested step chains, for example: cycleManager.evaluate(state := pusher.extend(execute := cycleManager.executeStep));.

  • waitFor(value) moves to the next step as soon as the boolean input becomes TRUE; otherwise, the chain remains in the current step.

  • executeCommand(ICommand) runs a command and interprets its return state with edge handling: in the first cycle the command is called with execute := FALSE (e.g., for pre-checks); in subsequent cycles it is called with execute := TRUE. The ICommand interface exposes executeCommand(execute) : SingleExecutionState which the CycleManager invokes with proper edge handling.

  • Additionally, two single-cycle helpers are available: enter(ISingleAttempt) runs only in the first cycle of a step, and leave(ISingleAttempt) runs once as soon as transition conditions to the next step are met. ISingleAttempt provides invoke() : SingleExecutionResult, that the cycleManager will execute at the right time.

  • Two special methods are provided for error scenarios: acknowledge() can be used to jump back from an error step to the previous step, but only if this step was reached via an error transition. handle(IMessage) was already explained in part 1; the method raises an error, waits until it is cleared, and can then either return to the previous step—if the step was reached via an error using handle—or proceed to the next step if the optional input resumeWithLastStep is set to false.

You can freely combine these methods within a step; a step change occurs only when all used methods report SUCCESS .

If used in the same step, the order/precedence of method calls should be like this:

  • configurations

  • enter

  • executeCommand

  • evaluate

  • waitFor

  • proceed

  • proceedWith

  • acknowledge

  • leave

2.3 Common properties

  • cycleManager.step.current is a read-only property that returns the active step of the execution

  • cycleManager.step.last is a read-only property that returns the previous step

  • cycleManager.step.next can be used to set the next step after the current one succeeds

  • cycleManager.executeStep is a boolean property that is false in the first cycle of a new step and then stays true until the next stepchange, intended to be used to start edge triggered operations within a step

  • cycleManager.errors.isInErrorStep is true if the current step was reached by evaluating an error

  • cycleManager.state returns the current state of the execution

2.4 Configuration

cycleManager.configuration offers a fluent API that improves discoverability and IntelliSense support.

You can configure either a single step or the entire sequence:

  • cycleManager.configuration.step applies only to the current step and resets when the step changes.

  • cycleManager.configuration.sequence applies to all steps but can be overridden by step-level settings.

The following options are available for both sequence and step:

  • stepWidth sets the increment for the next step when none is specified; the default is 1.

  • errorStep defines the step to jump to when an ERROR state occurs.

  • timeout limits the maximum execution time of a step. When it elapses, the CycleManager proceeds to timeoutStep.

  • timeoutStep specifies the step to jump to on a timeout.

  • pause.enable() allows pausing. pause.disable() disallows pausing (for example, during non-reversible processes).

  • stepping.enable() aenables stepping so the CycleManager waits until stepControl is TRUE before changing steps. stepping.disable() allows automatic step changes.

  • stopRequest defines the behavior when the stopRequest input becomes TRUE :

    • stopRequest.ignore() continues sequence execution.

    • stopRequest.immediate() stops immediately by setting the current step to SUCCESS .

    • stopRequest.afterSuccessfulStep() waits for the current step to complete, then proceeds to SUCCESS instead of advancing to the next step.

    • stopRequest.onBusy() ends the sequence if the current step has not yet succeeded (for example, while waiting for a handshake with another unit). It does not stop if the handshake succeeds.

  • The following option applies only to sequence configuration: requireSuccessStep is a boolean. If set to TRUE, the CycleManager guarantees execution of operations within the SUCCESS step. The cycle manager's state changes to SUCCESS only if the SUCCESS step succeeds, for example, with proceed or evaluate(SUCCESS).

2.5 Examples

Using the CycleManager to execute multiple commands parallel

METHOD moveToPos : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	(* a rising edge starts the execution *)
	execute :BOOL;
	(* the index of the target xyzr position *)
	position :SupplyPortalPositions;
END_VAR
VAR_INST
	(* we need to declare an instance of the cycleManager because cyclic nodes don't have a nodeCycleManager (in opposite to the ModeNodes ) *)
	cycleManager :CNM_CycleManager.SimpleCycleManager;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
	cycleManager.step.next := CNM_ReturnTypes.DefaultSteps.SUCCESS;
	(* the cycleManager will stay in this step until all commands returned SUCCESS or at least one command returns ERROR. 
	When all commands are done, it will proceed to the next step automatically. *)
	cycleManager.executeCommand( THIS^.driveX.commands.moveAbsolute(THIS^.settings.portalPositions[position].X ));
	cycleManager.executeCommand( THIS^.driveY.commands.moveAbsolute(THIS^.settings.portalPositions[position].Y ));
	cycleManager.executeCommand( THIS^.driveZ.commands.moveAbsolute(THIS^.settings.portalPositions[position].Z ));
	cycleManager.executeCommand( THIS^.driveR.commands.moveAbsolute(THIS^.settings.portalPositions[position].Rot ));
END_CASE

moveToPos := cycleManager.state;

Using enter() and leave() to execute a motor for a given time

METHOD runBrushing : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	execute :BOOL;
END_VAR
VAR_INST
	cycleManager :CNM_CycleManager.SimpleCycleManager;
	brushTimer :CNM_CycleManager.WaitCommand;
END_VAR
VAR CONSTANT	
	START_BRUSHING :DINT := CNM_ReturnTypes.DefaultSteps.INIT + 1;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
    (* run the motor only if a part is present, else shortcut to success *)
	cycleManager.step.next := SEL(
		THIS^.glazingSensor.occupied,
		CNM_ReturnTypes.DefaultSteps.SUCCESS,
		START_BRUSHING
	);
	cycleManager.proceed();
START_BRUSHING:
    cycleManager.step.next := CNM_ReturnTypes.DefaultSteps.SUCCESS;
	cycleManager.enter(THIS^.creamBrusher.attempts.start);
	brushTimer.waitTime := T#3S;
	cycleManager.executeCommand(brushTimer);
	cycleManager.leave(THIS^.creamBrusher.attempts.stop);
END_CASE

runBrushing := cycleManager.state;

A looping sequence with handling, processing and handshakes

METHOD runOven :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
	execute :BOOL;
END_VAR
VAR_INST
	cycleManager :CNM_OpModeHandling.NodeCycleManager(THIS^);
	ovenTimer :CNM_CupcakeDevices.WaitCommand;
END_VAR
VAR CONSTANT
	CHECK_SAUCER_UNLOADED :DINT := CNM_ReturnTypes.DefaultSteps.INIT + 1;
	REQUEST_LOADING :DINT := CHECK_SAUCER_UNLOADED + 1;
	WAIT_SAUCER_LOADED :DINT := REQUEST_LOADING +1;
	OPEN_OVEN :DINT := WAIT_SAUCER_LOADED + 1;
	MOVE_TO_OVEN :DINT := OPEN_OVEN + 1;
	CLOSE_OVEN :DINT := MOVE_TO_OVEN + 1;
	WAIT_OVEN :DINT := CLOSE_OVEN + 1;
	OPEN_OVEN_MOVEOUT :DINT := WAIT_OVEN + 1;
	MOVE_TO_LOADING :DINT := OPEN_OVEN_MOVEOUT + 1;
	REQUEST_UNLOAD :DINT := MOVE_TO_LOADING + 1;
END_VAR

cycleManager(execute := execute);
CASE cycleManager.step.current OF
CNM_ReturnTypes.DefaultSteps.INIT:
	cycleManager.configuration.sequence.stepping.enable();
	cycleManager.proceed();
CHECK_SAUCER_UNLOADED:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.step.next := SEL(THIS^.dropOffSensor.free, OPEN_OVEN, REQUEST_LOADING);
	cycleManager.proceed();
REQUEST_LOADING:
	cycleManager.evaluate(THIS^.cupcakeSupply.requestDrop(execute := cycleManager.executeStep));
WAIT_SAUCER_LOADED:
	cycleManager.configuration.step.stopRequest.immediate();
	IF THIS^.dropOffSensor.occupied THEN
		cycleManager.proceed();
	END_IF
OPEN_OVEN:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Extend );
MOVE_TO_OVEN:
	cycleManager.executeCommand(THIS^.flyingSaucer.commands.moveAbsolute(THIS^.settings.saucerPositions[BakingSaucerPosition.OVEN]));
CLOSE_OVEN:
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Retract );
WAIT_OVEN:
	ovenTimer.waitTime := T#5S;
	cycleManager.executeCommand(ovenTimer);
OPEN_OVEN_MOVEOUT:
	cycleManager.executeCommand( THIS^.ovenDoor.commands.Extend );
MOVE_TO_LOADING:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	cycleManager.executeCommand(THIS^.flyingSaucer.commands.moveAbsolute(THIS^.settings.saucerPositions[BakingSaucerPosition.HOME]));
REQUEST_UNLOAD:
	cycleManager.configuration.step.stopRequest.afterSuccessfulStep();
	(* by setting the next step to this we create a looping sequence until a stop request is triggered *)
	cycleManager.step.next := CHECK_SAUCER_UNLOADED;
	cycleManager.evaluate(THIS^.requestPickup(cycleManager.executeStep));
END_CASE

runOven := cycleManager.state;