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 statefalse- 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:
IdleState.OnExit()calledProcessingState.OnEntry()called- 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:
EmergencyState.OnEntry()calledProcessingStatesuspended (OnExit NOT called)- 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:
EmergencyState.OnExit()calledProcessingStateresumed (OnEntry NOT called again)- 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;
}