IState
Interface for actor states that process messages in a state machine pattern. States represent discrete behavioral modes within an actor, each handling specific messages and determining when to transition to other states.
Namespace: ControlBee.Interfaces
Extends: IDisposable
Overview
ControlBee uses message-driven state machines to organize actor behavior. Each state:
- Processes messages independently
- Determines when to transition to other states
- Has lifecycle hooks for entry and exit
- Can manage timers for periodic or delayed operations
- Can be stacked for hierarchical behavior
State Machine Pattern:
Idle → WaitingForMaterial → Processing → Unloading → Idle
Each actor maintains a current state (or state stack), and all incoming messages are routed to the active state's ProcessMessage method.
Methods
ProcessMessage
Processes an incoming message within this state.
bool ProcessMessage(Message message);
Parameters:
message— The message to process, containing a name and optional data payload.
Returns: true if the message was handled by this state; false if the message was not relevant and should propagate to parent states.
Description:
The core method of the state interface. When a message is sent to an actor, it's delivered to the current state's ProcessMessage method. The state examines the message and either:
- Handles it - Processes the message and returns
true - Ignores it - Returns
false, allowing the message to propagate to stacked parent states
Usage Pattern:
public override bool ProcessMessage(Message msg)
{
switch (msg.Name)
{
case "Start":
// Handle the message
DoSomething();
return true; // Message handled
case "Stop":
// Handle and transition
SetState(new IdleState(_actor));
return true; // Message handled
default:
return false; // Not handled, may propagate
}
}
State Class
In practice, you inherit from the State base class which implements IState and provides additional functionality:
public class MyState : State
{
private MyActor Actor => (MyActor)_actor;
public MyState(Actor actor) : base(actor)
{
}
// Lifecycle methods
public override void OnEntry(Message msg) { }
public override bool OnProcess(Message msg) { return false; }
public override void OnExit(Message msg) { }
}
Constructor
public MyState(Actor actor) : base(actor)
All states must receive a reference to their owning actor. Store strongly-typed actor reference:
private MyActor Actor => (MyActor)_actor;
Lifecycle Methods
OnEntry
Called once when transitioning into this state.
public override void OnEntry(Message msg)
{
// Initialize state
Actor.Busy.Value = true;
Actor.StatusLight.TurnOn();
// Start operations
Actor.Motor.Start();
// Set timers
StartTimer("Timeout", 5000); // 5 second timeout
}
Use Cases:
- Initialize state-specific resources
- Start hardware operations
- Set up timers
- Update status variables
- Log state transitions
OnProcess
Called for every message while in this state.
public override bool OnProcess(Message msg)
{
switch (msg.Name)
{
case "SensorDetected":
HandleSensor((bool)msg.Data);
return true;
case "TimerMessage":
HandleTimeout();
return true;
case "Stop":
SetState(new IdleState(_actor));
return true;
default:
return false; // Not handled
}
}
Return Values:
true- Message was handled by this statefalse- Message not relevant, allow propagation
Use Cases:
- Process incoming messages
- Handle sensor events
- Respond to timer expirations
- Manage state transitions
- Coordinate with other actors
OnExit
Called once when leaving this state.
public override void OnExit(Message msg)
{
// Clean up state
Actor.Busy.Value = false;
Actor.StatusLight.TurnOff();
// Stop operations
Actor.Motor.Stop();
// Cancel timers
CancelAllTimers();
// Release resources
Actor.Vacuum.TurnOff();
}
Use Cases:
- Clean up state-specific resources
- Stop hardware operations
- Cancel timers
- Update status variables
- Save state data
Important: Always clean up in OnExit to prevent resource leaks and ensure proper state transitions.
State Transition Methods
SetState
Replace the current state completely:
public override bool OnProcess(Message msg)
{
if (msg.Name == "Start")
{
SetState(new ProcessingState(_actor));
return true;
}
return false;
}
Flow:
- Current state's
OnExit()called - New state's
OnEntry()called - Actor now in new state
Use When:
- Normal state progression
- State is completely finished
- No need to return to current state
PushState
Push a new state onto a stack (current state pauses):
public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
PushState(new EmergencyState(_actor));
return true;
}
return false;
}
Flow:
- New state's
OnEntry()called - Current state suspended (OnExit NOT called)
- Messages go to new state
Use When:
- Temporary interruption
- Need to return to current state later
- Hierarchical behavior (base state + overlay)
PopState
Return to the previous state in the stack:
public override bool OnProcess(Message msg)
{
if (msg.Name == "Resume")
{
PopState();
return true;
}
return false;
}
Flow:
- Current state's
OnExit()called - Previous state resumed (OnEntry NOT called again)
- Messages go to previous state
Use When:
- Finishing temporary interruption
- Returning from sub-operation
- Completing overlay behavior
Timer Management
States can manage timers for periodic or delayed operations:
StartTimer
StartTimer("CheckSensor", 100); // Fire after 100ms
Starts a timer that will send a TimerMessage after the specified interval.
CancelTimer
CancelTimer("CheckSensor");
Cancels a specific timer by name.
CancelAllTimers
CancelAllTimers();
Cancels all timers started by this state. Typically called in OnExit().
Timer Pattern:
public override void OnEntry(Message msg)
{
// Start periodic check
StartTimer("CheckSensor", 100);
}
public override bool OnProcess(Message msg)
{
if (msg.Name == "TimerMessage" && (string)msg.Data == "CheckSensor")
{
if (Actor.MaterialSensor.Value)
{
// Material detected, transition
SetState(new ProcessingState(_actor));
}
else
{
// Keep checking
StartTimer("CheckSensor", 100);
}
return true;
}
return false;
}
public override void OnExit(Message msg)
{
CancelAllTimers();
}
Built-in Messages
StateEntryMessage
Automatically sent when entering a state:
public override bool OnProcess(Message msg)
{
if (msg.Name == "StateEntryMessage")
{
// Perform initialization that needs message context
return true;
}
return false;
}
Note: Usually, initialization is done in OnEntry() instead.
StateExitMessage
Automatically sent 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;
}
Note: Usually, cleanup is done in OnExit() instead.
TimerMessage
Sent when a timer expires:
if (msg.Name == "TimerMessage" && (string)msg.Data == "MyTimer")
{
// Handle timer expiration
return true;
}
Common State Patterns
1. Simple State
One responsibility, clear transitions:
public class IdleState : State
{
public IdleState(Actor actor) : base(actor) { }
public override void OnEntry(Message msg)
{
Console.WriteLine("Ready");
}
public override bool OnProcess(Message msg)
{
if (msg.Name == "Start")
{
SetState(new ProcessingState(_actor));
return true;
}
return false;
}
}
2. Waiting State
Polls condition until met:
public class WaitingState : State
{
public WaitingState(Actor actor) : base(actor) { }
public override void OnEntry(Message msg)
{
StartTimer("CheckCondition", 100);
}
public override bool OnProcess(Message msg)
{
if (msg.Name == "TimerMessage")
{
var actor = (MyActor)_actor;
if (actor.Sensor.Value)
{
SetState(new NextState(_actor));
}
else
{
StartTimer("CheckCondition", 100);
}
return true;
}
return false;
}
public override void OnExit(Message msg)
{
CancelAllTimers();
}
}
3. Async Operation State
Starts async operation and waits for completion:
public class MovingState : State
{
public MovingState(Actor actor) : base(actor) { }
public override void OnEntry(Message msg)
{
var actor = (MyActor)_actor;
actor.X.TrapezoidalMove(100, 50, 100, 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;
}
public override void OnExit(Message msg)
{
CancelAllTimers();
}
}
4. Looping State
Processes multiple items sequentially:
public class ProcessingState : State
{
private int currentIndex = 0;
public ProcessingState(Actor actor) : base(actor) { }
public override void OnEntry(Message msg)
{
currentIndex = 0;
ProcessNext();
}
private void ProcessNext()
{
var actor = (MyActor)_actor;
if (currentIndex >= actor.TotalCount)
{
SetState(new DoneState(_actor));
return;
}
// Process current item
actor.ProcessItem(currentIndex);
}
public override bool OnProcess(Message msg)
{
if (msg.Name == "ItemComplete")
{
currentIndex++;
ProcessNext();
return true;
}
return false;
}
}
5. Error Handling State
Handles errors and provides recovery:
public class ErrorState : State
{
private string errorMessage;
public ErrorState(Actor actor, string error) : base(actor)
{
errorMessage = error;
}
public override void OnEntry(Message msg)
{
var actor = (MyActor)_actor;
// Stop all motion
actor.X.Stop();
// Alert user
Console.WriteLine($"ERROR: {errorMessage}");
actor.ErrorLight.TurnOn();
}
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 = (MyActor)_actor;
actor.ErrorLight.TurnOff();
}
}
Message Propagation
When using state stacks (PushState), unhandled messages propagate to parent states:
// State stack: [BaseState, OverlayState]
// Current: OverlayState
public class OverlayState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "OverlaySpecific")
{
// Handle overlay-specific message
return true;
}
// Return false - message propagates to BaseState
return false;
}
}
public class BaseState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
// Handles emergency even when OverlayState is active
SetState(new EmergencyState(_actor));
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 { }
2. Always Clean Up in OnExit
// ✅ Good
public override void OnEntry(Message msg)
{
StartTimer("Monitor", 100);
Actor.Motor.Start();
}
public override void OnExit(Message msg)
{
CancelAllTimers();
Actor.Motor.Stop();
}
// ❌ Bad - No cleanup
public override void OnEntry(Message msg)
{
StartTimer("Monitor", 100);
Actor.Motor.Start();
}
// Motor stays on, timer keeps running!
3. Use Descriptive Names
public class WaitingForVacuumState : State { } // ✅ Clear
public class MovingToLoadPositionState : State { } // ✅ Clear
public class State1 : State { } // ❌ Unclear
4. 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 == "TimerMessage")
{
SetState(new ErrorState(_actor, "Timeout"));
return true;
}
return false;
}
5. Use State Stack for Interruptions
// ✅ Good - Preserves context
if (msg.Name == "PauseForInspection")
{
PushState(new InspectionState(_actor));
// Can return to current state
}
// ❌ Bad - Loses context
if (msg.Name == "PauseForInspection")
{
SetState(new InspectionState(_actor));
// Lost where we were
}
See Also
- State Management Guide - Complete guide to state patterns
- Message Passing Guide - Actor communication
- Actor System Guide - Actor development fundamentals