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.

Info

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

  1. Axis Manager
  2. CNC Manager
  3. Spindle

The states we need to handle are

  1. Bootup
  2. Resetting
  3. Starting
  4. Execute
  5. Completing
  6. Stopping
  7. 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

  1. that all 3 systems are initialized and ready.

During Resetting we will prepare the system for motion by

  1. appling power to the axes
  2. homing the axes if it is required
  3. moving the axes to a parked position if they have been homed

During Starting we will prepare for the process by

  1. Writing custom CNC parameters
  2. Starting the spindle running
  3. Starting the CNC program

During Execute we will run the process by

  1. Waiting for the CNC program to finish

During Completing we will do a controlled process breakdown by

  1. Turning off the spindle

During Stopping we will do a controlled stop by

  1. stopping any motion
  2. 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)

  1. quick stop AND turn the spindle off
  2. 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
Warning

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:

  1. We should look at our TODO list that we made up front to ensure we have caught all the cases we need to handle.

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

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

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