Skip to main content

State Management

ControlBee uses message-driven state machines to organize actor behavior into discrete, manageable states with clear transitions.

State Machine Pattern

Each actor can have multiple states, with only one state active at a time (or a stack of states). States process messages and determine state transitions.

Why Use State Machines?

Benefits:

  • Clear Structure - Each state has a single, well-defined purpose
  • Predictable Behavior - State transitions are explicit and traceable
  • Easier Testing - States can be tested independently
  • Maintainability - Adding new behaviors means adding new states

Example Scenario:

Idle → WaitingForMaterial → Processing → Unloading → Idle

State Class Structure

All states inherit from the State class:

public class IdleState : State
{
private ConveyorActor Actor => (ConveyorActor)_actor;

public IdleState(Actor actor) : base(actor)
{
}

public override void OnEntry(Message msg)
{
// Called once when entering this state
Console.WriteLine("Entering Idle state");
Actor.Busy.Value = false;
Actor.ConveyorMotor.Stop();
}

public override bool OnProcess(Message msg)
{
// Called for every message while in this state
if (msg.Name == "StartProcess")
{
SetState(new ProcessingState(_actor));
return true; // Message handled
}

return false; // Message not handled
}

public override void OnExit(Message msg)
{
// Called once when leaving this state
Console.WriteLine("Exiting Idle state");
}
}

State Lifecycle

1. State Entry

Called once when transitioning into the state:

public override void OnEntry(Message msg)
{
// Initialize state
Actor.Busy.Value = true;
Actor.StatusLight.TurnOn();

// Start timers
StartTimer("Timeout", 5000); // 5 second timeout

// Set up hardware
Actor.Vacuum.TurnOn();
}

2. Message Processing

Called for every message while in this state:

public override bool OnProcess(Message msg)
{
switch (msg.Name)
{
case "SensorDetected":
// Handle sensor input
ProcessSensor((bool)msg.Data);
return true;

case "TimerMessage":
// Handle timeout
Console.WriteLine("Operation timed out");
SetState(new ErrorState(_actor));
return true;

case "Stop":
// Handle stop command
SetState(new IdleState(_actor));
return true;

default:
return false; // Message not handled
}
}

Return Values:

  • true - Message was handled by this state
  • false - Message not relevant to this state (may propagate to parent states)

3. State Exit

Called once when leaving the state:

public override void OnExit(Message msg)
{
// Clean up state
Actor.Busy.Value = false;

// Stop timers
CancelAllTimers();

// Release resources
Actor.Vacuum.TurnOff();
Actor.StatusLight.TurnOff();
}

State Transitions

SetState

Replace the current state completely:

// In IdleState
public override bool OnProcess(Message msg)
{
if (msg.Name == "Start")
{
SetState(new ProcessingState(_actor)); // Idle → Processing
return true;
}
return false;
}

Flow:

  1. IdleState.OnExit() called
  2. ProcessingState.OnEntry() called
  3. Actor now in ProcessingState

PushState

Push a new state onto a stack (current state pauses):

// In ProcessingState, need to pause for error handling
public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
PushState(new EmergencyState(_actor)); // Processing paused, Emergency active
return true;
}
return false;
}

Flow:

  1. EmergencyState.OnEntry() called
  2. ProcessingState suspended (OnExit NOT called)
  3. Messages go to EmergencyState

PopState

Return to the previous state in the stack:

// In EmergencyState
public override bool OnProcess(Message msg)
{
if (msg.Name == "Resume")
{
PopState(); // Return to ProcessingState
return true;
}
return false;
}

Flow:

  1. EmergencyState.OnExit() called
  2. ProcessingState resumed (OnEntry NOT called again)
  3. Messages go to ProcessingState

Built-in Messages

StateEntryMessage

Sent automatically when entering a state:

public override bool OnProcess(Message msg)
{
if (msg.Name == "StateEntryMessage")
{
// Perform one-time initialization that needs message context
CheckSensorStatus();
return true;
}
return false;
}

StateExitMessage

Sent automatically before exiting a state:

public override bool OnProcess(Message msg)
{
if (msg.Name == "StateExitMessage")
{
// Save state before leaving
Actor.LastPosition.Value = Actor.X.CommandPosition;
return true;
}
return false;
}

TimerMessage

Sent when a timer expires:

public override void OnEntry(Message msg)
{
StartTimer("CheckSensor", 100); // Every 100ms
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "TimerMessage" && (string)msg.Data == "CheckSensor")
{
if (Actor.MaterialSensor.Value)
{
SetState(new ProcessingState(_actor));
}
else
{
StartTimer("CheckSensor", 100); // Restart timer
}
return true;
}
return false;
}

Common State Patterns

1. Sequential Workflow

Chain states in a sequence:

public class PickAndPlaceActor : Actor
{
public override void Start()
{
base.Start();
SetState(new IdleState(this));
}
}

// Idle → MoveToPickup → Pickup → MoveToPlace → Place → Idle
public class IdleState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "StartCycle")
{
SetState(new MoveToPickupState(_actor));
return true;
}
return false;
}
}

public class MoveToPickupState : State
{
public override void OnEntry(Message msg)
{
var actor = (PickAndPlaceActor)_actor;
actor.PickupPos.Value.MoveAsync();
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "MoveComplete")
{
SetState(new PickupState(_actor));
return true;
}
return false;
}
}

public class PickupState : State
{
public override void OnEntry(Message msg)
{
var actor = (PickAndPlaceActor)_actor;
actor.Vacuum.TurnOn();
StartTimer("CheckVacuum", 500);
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "TimerMessage")
{
var actor = (PickAndPlaceActor)_actor;
if (actor.VacuumSensor.Value)
{
SetState(new MoveToPlaceState(_actor));
}
else
{
SetState(new ErrorState(_actor));
}
return true;
}
return false;
}
}

2. Looping State

State that processes multiple items:

public class ProcessingState : State
{
private int currentIndex = 0;
private GridActor Actor => (GridActor)_actor;

public override void OnEntry(Message msg)
{
currentIndex = 0;
ProcessNextCell();
}

private void ProcessNextCell()
{
if (currentIndex >= Actor.TotalCells)
{
// All done
SetState(new IdleState(_actor));
return;
}

// Process current cell
var (row, col) = Actor.GetCellPosition(currentIndex);
if (Actor.Tray.Value.CellExists[row, col])
{
Actor.Publish("ProcessCell", new { Row = row, Col = col });
}
else
{
currentIndex++;
ProcessNextCell(); // Skip to next
}
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "CellComplete")
{
currentIndex++;
ProcessNextCell();
return true;
}
return false;
}
}

3. Waiting State

State that waits for a condition:

public class WaitingForMaterialState : State
{
public override void OnEntry(Message msg)
{
Console.WriteLine("Waiting for material...");
StartTimer("CheckSensor", 100); // Poll every 100ms
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "TimerMessage")
{
var actor = (ConveyorActor)_actor;
if (actor.MaterialSensor.Value)
{
// Material detected
SetState(new ProcessingState(_actor));
}
else
{
// Keep waiting
StartTimer("CheckSensor", 100);
}
return true;
}

if (msg.Name == "Cancel")
{
SetState(new IdleState(_actor));
return true;
}

return false;
}
}

4. Error Handling State

State for error recovery:

public class ErrorState : State
{
private string errorMessage;

public ErrorState(Actor actor, string error) : base(actor)
{
errorMessage = error;
}

public override void OnEntry(Message msg)
{
// Stop all motion
var actor = (StageActor)_actor;
actor.X.Stop();
actor.Z.Stop();

// Alert user
actor.ErrorLight.TurnOn();
Console.WriteLine($"ERROR: {errorMessage}");

// Log to database
actor.LogError(errorMessage);
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "Reset")
{
SetState(new IdleState(_actor));
return true;
}

if (msg.Name == "Retry")
{
SetState(new InitializingState(_actor));
return true;
}

return false;
}

public override void OnExit(Message msg)
{
var actor = (StageActor)_actor;
actor.ErrorLight.TurnOff();
}
}

5. Parallel Operations (State Stack)

Handle interruptions without losing context:

public class RunningState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "PauseForVision")
{
// Pause current operation, do vision, then resume
PushState(new VisionCheckState(_actor));
return true;
}

if (msg.Name == "EmergencyStop")
{
// Emergency takes priority
PushState(new EmergencyState(_actor));
return true;
}

// Normal processing...
return false;
}
}

public class VisionCheckState : State
{
public override void OnEntry(Message msg)
{
var actor = (VisionActor)_actor;
actor.Vision.Trigger();
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "VisionComplete")
{
PopState(); // Return to RunningState
return true;
}
return false;
}
}

Message Propagation

When using state stacks, unhandled messages can propagate:

// State stack: [IdleState, VisionState]
// Current state: VisionState

public class VisionState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "VisionComplete")
{
// Handle vision-specific message
PopState();
return true; // Handled
}

// Return false - message propagates to IdleState
return false;
}
}

public class IdleState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
// Handles emergency even when VisionState is active
SetState(new EmergencyState(_actor));
return true;
}

return false;
}
}

State Communication

States can communicate with other actors:

public class LoadingState : State
{
public override void OnEntry(Message msg)
{
var actor = (LoaderActor)_actor;

// Request material from supplier
actor.SupplierPeer.Publish("RequestMaterial");

// Wait for response
StartTimer("SupplierTimeout", 10000);
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "MaterialReady")
{
CancelTimer("SupplierTimeout");
SetState(new PickupState(_actor));
return true;
}

if (msg.Name == "TimerMessage" && (string)msg.Data == "SupplierTimeout")
{
SetState(new ErrorState(_actor, "Supplier timeout"));
return true;
}

return false;
}
}

Best Practices

1. One Responsibility Per State

// ✅ Good - Each state has clear purpose
public class MovingState : State { }
public class ProcessingState : State { }
public class WaitingState : State { }

// ❌ Bad - State doing too much
public class DoEverythingState : State
{
// Moving, processing, waiting all in one state
}

2. Clean State Entry/Exit

// ✅ Good - Clean up in OnExit
public override void OnEntry(Message msg)
{
Actor.Motor.Start();
StartTimer("Monitor", 100);
}

public override void OnExit(Message msg)
{
Actor.Motor.Stop();
CancelAllTimers();
}

// ❌ Bad - Not cleaning up
public override void OnEntry(Message msg)
{
Actor.Motor.Start();
StartTimer("Monitor", 100);
}
// OnExit not implemented - motor stays on, timer keeps running

3. Handle Timeouts

// ✅ Good - Timeout prevents hanging
public override void OnEntry(Message msg)
{
Actor.X.MoveAsync(100);
StartTimer("MoveTimeout", 5000);
}

public override bool OnProcess(Message msg)
{
if (msg.Name == "MoveComplete")
{
CancelTimer("MoveTimeout");
SetState(new NextState(_actor));
return true;
}

if (msg.Name == "TimerMessage")
{
SetState(new ErrorState(_actor, "Move timeout"));
return true;
}

return false;
}

4. Use Descriptive State Names

public class WaitingForVacuumState : State { }        // ✅ Clear
public class MovingToLoadPositionState : State { } // ✅ Clear
public class State1 : State { } // ❌ Unclear
public class TempState : State { } // ❌ Unclear

5. Document State Transitions

/// <summary>
/// Manages the pick-and-place operation workflow.
///
/// State flow:
/// IdleState → MoveToPickupState → PickupState →
/// MoveToPlaceState → PlaceState → IdleState
///
/// Any state can transition to ErrorState on failure.
/// </summary>
public class PickAndPlaceActor : Actor
{
// ...
}

6. Use State Stack for Interruptions

// ✅ Good - Preserves context with PushState
public override bool OnProcess(Message msg)
{
if (msg.Name == "PauseForInspection")
{
PushState(new InspectionState(_actor)); // Can return to current state
return true;
}
return false;
}

// ❌ Bad - Loses context with SetState
public override bool OnProcess(Message msg)
{
if (msg.Name == "PauseForInspection")
{
SetState(new InspectionState(_actor)); // Lost where we were
return true;
}
return false;
}

7. Validate State Transitions

public override bool OnProcess(Message msg)
{
if (msg.Name == "Start")
{
var actor = (StageActor)_actor;

// Validate before transitioning
if (!actor.IsInitialized)
{
Console.WriteLine("Cannot start - not initialized");
return true;
}

if (actor.EmergencyStop.Value)
{
Console.WriteLine("Cannot start - emergency stop active");
return true;
}

SetState(new RunningState(_actor));
return true;
}
return false;
}

See Also