상태 관리
ControlBee는 메시지 기반 상태 머신을 사용하여 액터 동작을 명확한 전환을 가진 개별적이고 관리 가능한 상태로 구성합니다.
상태 머신 패턴
각 액터는 여러 상태를 가질 수 있으며, 한 번에 하나의 상태만 활성화됩니다(또는 상태 스택). 상태는 메시지를 처리하고 상태 전환을 결정합니다.
왜 상태 머신을 사용하나요?
이점:
- 명확한 구조 - 각 상태는 단일하고 잘 정의된 목적을 가집니다
- 예측 가능한 동작 - 상태 전환이 명시적이고 추적 가능합니다
- 쉬운 테스트 - 상태를 독립적으로 테스트할 수 있습니다
- 유지보수성 - 새로운 동작을 추가하는 것은 새로운 상태를 추가하는 것을 의미합니다
예제 시나리오:
Idle → WaitingForMaterial → Processing → Unloading → Idle
상태 클래스 구조
모든 상태는 State 클래스를 상속합니다:
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");
}
}
상태 수명 주기
1. 상태 진입
상태로 전환할 때 한 번 호출됩니다:
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. 메시지 처리
이 상태에 있는 동안 모든 메시지에 대해 호출됩니다:
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
}
}
반환값:
true- 이 상태에서 메시지가 처리되었습니다false- 이 상태와 관련이 없는 메시지입니다(부모 상태로 전파될 수 있음)
3. 상태 종료
상태를 떠날 때 한 번 호출됩니다:
public override void OnExit(Message msg)
{
// Clean up state
Actor.Busy.Value = false;
// Stop timers
CancelAllTimers();
// Release resources
Actor.Vacuum.TurnOff();
Actor.StatusLight.TurnOff();
}
상태 전환
SetState
현재 상태를 완전히 교체합니다:
// In IdleState
public override bool OnProcess(Message msg)
{
if (msg.Name == "Start")
{
SetState(new ProcessingState(_actor)); // Idle → Processing
return true;
}
return false;
}
흐름:
IdleState.OnExit()호출ProcessingState.OnEntry()호출- 액터가 이제
ProcessingState에 있습니다
PushState
새 상태를 스택에 푸시합니다(현재 상태가 일시 중지됨):
// 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;
}
흐름:
EmergencyState.OnEntry()호출ProcessingState일시 중지(OnExit 호출되지 않음)- 메시지가
EmergencyState로 전달됩니다
PopState
스택의 이전 상태로 돌아갑니다:
// In EmergencyState
public override bool OnProcess(Message msg)
{
if (msg.Name == "Resume")
{
PopState(); // Return to ProcessingState
return true;
}
return false;
}
흐름:
EmergencyState.OnExit()호출ProcessingState재개(OnEntry가 다시 호출되지 않음)- 메시지가
ProcessingState로 전달됩니다
내장 메시지
StateEntryMessage
상태로 진입할 때 자동으로 전송됩니다:
public override bool OnProcess(Message msg)
{
if (msg.Name == "StateEntryMessage")
{
// Perform one-time initialization that needs message context
CheckSensorStatus();
return true;
}
return false;
}
StateExitMessage
상태를 종료하기 전에 자동으로 전송됩니다:
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
타이머가 만료될 때 전송됩니다:
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;
}
일반적인 상태 패턴
1. 순차적 워크플로
상태를 순서대로 연결합니다:
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. 반복 상태
여러 항목을 처리하는 상태입니다:
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. 대기 상태
조건을 기다리는 상태입니다:
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. 오류 처리 상태
오류 복구를 위한 상태입니다:
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. 병렬 작업 (상태 스택)
컨텍스트를 잃지 않고 중단을 처리합니다:
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;
}
}
메시지 전파
상태 스택을 사용할 때 처리되지 않은 메시지가 전파될 수 있습니다:
// 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;
}
}
상태 통신
상태는 다른 액터와 통신할 수 있습니다:
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;
}
}
모범 사례
1. 상태당 하나의 책임
// ✅ 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. 깨끗한 상태 진입/종료
// ✅ 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. 타임아웃 처리
// ✅ 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. 설명적인 상태 이름 사용
public class WaitingForVacuumState : State { } // ✅ Clear
public class MovingToLoadPositionState : State { } // ✅ Clear
public class State1 : State { } // ❌ Unclear
public class TempState : State { } // ❌ Unclear
5. 상태 전환 문서화
/// <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. 중단에 상태 스택 사용
// ✅ 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. 상태 전환 유효성 검사
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;
}