Piper Substates
I hear you want a sequence
Let me introduce you to Piper Substates
If you consider Piper to implement the PackML State Machine, substates are the mechanism to implement your systems Sequence.
One thing to keep in mind is that Piper is meant to be a machine state system. It helps to facilitate machine modes, and do the build up and breakdown of the processes. Piper ensures that starting and stopping the machine is consistent, predictable and modular.
Piper is not meant to be used for implementing the process. Typically piper does the startup by getting all the systems ready, then hands control to a process manager. This might be a CNC program, custom built sequencer using ATN or higher level plant automation system.
When Piper enters a new transition state, it will initialize the substates to the starting sequence step. From here, subsystems can request steps that will be executed in order. If a subsystem needs to implement a specific step during a state, it will request the step, then wait for piper to call that step. Many subsystems can request steps, as such, piper will select the lowest requested step as it moves through the sequence. Steps that are not requested are skipped.
Once no more steps are requested, and all the subsystems have responded with the current state, the state is considered complete.
An Example
Let’s take a look at an example.
In this example we have a 2 axis CNC machine, that has a spindle.
Here we have 3 subsystems
- Axis Manager
- CNC Manager
- Spindle
The states we need to handle are
- Bootup
- Resetting
- Starting
- Execute
- Completing
- Stopping
- Aborting
States to implement.. a TODO list
The following is a typical breakdown of what is implemented in each state, and in what order (sequence/substate).
During Bootup we will ensure
- that all 3 systems are initialized and ready.
During Resetting we will prepare the system for motion by
- appling power to the axes
- homing the axes if it is required
- moving the axes to a parked position if they have been homed
During Starting we will prepare for the process by
- Writing custom CNC parameters
- Starting the spindle running
- Starting the CNC program
During Execute we will run the process by
- Waiting for the CNC program to finish
During Completing we will do a controlled process breakdown by
- Turning off the spindle
During Stopping we will do a controlled stop by
- stopping any motion
- turning off the spindle
During Aboring we will do a quick stop (may be combined with and help an E-stop but not BE an E-stop)
- quick stop AND turn the spindle off
- depower all axes
Now let’s look at some code.
We will start with a spindle since it is the most basic.
Spindle Control
//Spindle.st
CASE ModuleInterface.PiperState OF
MACH_ST_BOOTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_WAIT_SPINDLE;
SUB_STATE_WAIT_SPINDLE:
//At this point, we must wait for the spindle before moving on
IF Spindle.IsReady THEN
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_STARTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_START_SPINDLE;
SUB_STATE_START_SPINDLE:
Spindle.Enable := TRUE;
IF Spindle.AtSpeed THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_STOPPING,
MACH_ST_COMPLETING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_STOP_SPINDLE;
SUB_STATE_STOP_SPINDLE:
//Stop the spindle after motion is stopped to avoid corrupting the process
Spindle.Enable := FALSE;
IF Spindle.Stopped THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_ABORTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
//Stop the spindle immediately for safety concerns
//(NOTE: This should work WITH your safety system, not BE your safety system)
Spindle.Enable := FALSE;
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_STOP_SPINDLE;
SUB_STATE_STOP_SPINDLE:
Spindle.Enable := FALSE;
IF Spindle.Stopped THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
ELSE
// Immediately respond with done in states that are not handled by this Module
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_CASE
Axis Control
Next we can look at the Servo manager.
//ServoManager.st
CASE ModuleInterface.PiperState OF
MACH_ST_BOOTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the axes
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_WAIT_AXES;
SUB_STATE_WAIT_AXES:
//At this point, we must wait for the axes before moving on
IF Axis1.IsReady AND Axis2.IsReady THEN
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_RESETTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_POWER_AXES;
SUB_STATE_POWER_AXES:
Axis1.Power := TRUE;
Axis2.Power := TRUE;
IF Axis1.isPowered AND Axis1.isPowered THEN
IF Axis1.isHomed AND Axis2.isHomed THEN
//If the axes are already homed, respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
ELSE
//If the axes are not homed, request a homing state
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_HOME_AXES;
END_IF
END_IF
SUB_STATE_HOME_AXES:
Axis1.Home := NOT Axis1.isHomed;
Axis2.Home := NOT Axis2.isHomed;
IF Axis1.isHomed AND Axis2.isHomed THEN
//If the axes are homed, respond let's park them
ModuleInterface.ModuleResponse := SUB_STATE_PARK_AXES;
END_IF
SUB_STATE_PARK_AXES
Axis1.GoToPark := TRUE;
Axis2.GoToPark := TRUE;
IF Axis1.Parked AND Axis2.Parked THEN
//If the axes are at the park position, respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_STOPPING,
MACH_ST_COMPLETING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the axes
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_STOP_AXES;
SUB_STATE_STOP_AXES:
//Stop the axes after after any other process control that needs to happen
Axis1.Stop := TRUE;
Axis2.Stop := TRUE;
IF NOT Axis1.InMotion AND NOT Axis2.InMotion THEN
//Respond that we are finished when motion is done
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_ABORTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
//Stop the axes immediately for safety concerns
//(NOTE: This should work WITH your safety system, not BE your safety system)
Axis1.Stop := TRUE;
Axis2.Stop := TRUE;
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the axes to stop
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_STOP_AXES;
SUB_STATE_STOP_AXES:
//Ensure the state is fully contained
Axis1.Stop := TRUE;
Axis2.Stop := TRUE;
// The stop has already been requested, but we need to wait for all motion to be complete before powering off
IF NOT Axis1.InMotion AND NOT Axis2.InMotion THEN
//Request poweroff as soon as motion is complete,
// but we should wait until they are actually powered off in the next step
Axis1.PowerOff := TRUE;
Axis2.PowerOff := TRUE;
//Request a new substate to power off
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_POWEROFF_AXES;
END_IF
SUB_STATE_POWEROFF_AXES:
//Ensure the state is fully contained
Axis1.PowerOff := TRUE;
Axis2.PowerOff := TRUE;
IF NOT Axis1.isPowered AND NOT Axis1.isPowered THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
ELSE
// Immediately respond with done in states that are not handled by this Module
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_CASE
CNC Control
And Finally the CNC manager
//CNCManager.st
CASE ModuleInterface.PiperState OF
MACH_ST_BOOTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the CNC to initialize
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_WAIT_CNC_INIT;
SUB_STATE_WAIT_CNC_INIT:
//At this point, we must wait for the CNC before moving on
IF CNC.IsReady THEN
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_STARTING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
// Immediately request a substate so that others systems can run their
// substates first, without waiting for the spindle
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_WAIT_CNC_INIT;
SUB_STATE_WAIT_CNC_INIT:
CNC.InitParameter := TRUE;
IF CNC.InitDone THEN
//Request the next state
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_START_CNC;
END_IF
SUB_STATE_START_CNC:
CNC.Start := TRUE;
IF CNC.IsRunning THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
MACH_ST_EXECUTE:
//Wait in Execute until the CNC is finished running
IF NOT CNC.IsRunning THEN
//Respond that we are finished
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
MACH_ST_STOPPING,
MACH_ST_ABORTING,
MACH_ST_COMPLETING:
CASE ModuleInterface.PiperSubState OF
SUB_STATE_INIT:
IF CNC.IsRunning THEN
//Stop Immediately
CNC.Stop := TRUE
Module.ModuleInterface.ModuleSubStateRequest := SUB_STATE_WAIT_CNC_STOP;
ELSE
//CNC is already stopped, we can be done
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
SUB_STATE_WAIT_CNC_STOP:
IF CNC.IsRunning THEN
//Make sure the state is fully contained
CNC.Stop := TRUE
ELSE
//CNC is already stopped, we can be done
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_IF
END_CASE
ELSE
// Immediately respond with done in states that are not handled by this Module
ModuleInterface.ModuleResponse := ModuleInterface.PiperState;
END_CASE
From this example, you should note that the order of the substates is not clear. This is on purpose and allows it to be changed as required. To configure the order of the substates, the application engineer will create a SUB_STATE_ENUM that orders the substates. Reording the enum will change the order that substate are executed.
This is desirable for managing the interactions between subsystems, however you need to ensure that a subsystem does not inadvertently request a substate in the past, as this step will be skipped.
At this point we have implemented the full sequence for a basic CNC machine. Piper does all of the start up of the process, the CNC runs the process, then Piper does the breakdown. It handles the user stopping, and aborting the process, as well as a normal process complete.
Next steps:
-
We should look at our TODO list that we made up front to ensure we have caught all the cases we need to handle.
-
it may be desirable to add Suspending and Holding states from the PackML standard. Since we have implemented our subsystems independently using Piper, adding this functionality should be fairly straight forward. Each subsystem can implement what they need to in order to handle the new system states without any complicated interactions between them.
-
Adding status info or messages to each state and substate will make it clear to the user what might be held up or going wrong. This will easily integrate with the built in messaging system in Piper.
-
We should build in some error handling. From Pipers perspective there may not be anything to do. The error handling could be handled in each piper state/substate or it could be handled outside of the states by messaging directly with gMachine or the local piper manager.