Skip to main content

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:

  1. Handles it - Processes the message and returns true
  2. 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 state
  • false - 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:

  1. Current state's OnExit() called
  2. New state's OnEntry() called
  3. 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:

  1. New state's OnEntry() called
  2. Current state suspended (OnExit NOT called)
  3. 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:

  1. Current state's OnExit() called
  2. Previous state resumed (OnEntry NOT called again)
  3. 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