본문으로 건너뛰기

IState

상태 머신 패턴으로 메시지를 처리하는 액터 상태를 위한 인터페이스입니다. 상태는 액터 내에서 개별 동작 모드를 나타내며, 각각 특정 메시지를 처리하고 다른 상태로 전환할 시기를 결정합니다.

네임스페이스: ControlBee.Interfaces

상속: IDisposable

개요

ControlBee는 메시지 기반 상태 머신을 사용하여 액터 동작을 구성합니다. 각 상태는:

  • 독립적으로 메시지를 처리합니다
  • 다른 상태로 전환할 시기를 결정합니다
  • 진입 및 종료를 위한 수명 주기 후크를 가집니다
  • 주기적 또는 지연된 작업을 위한 타이머를 관리할 수 있습니다
  • 계층적 동작을 위해 스택될 수 있습니다

상태 머신 패턴:

Idle → WaitingForMaterial → Processing → Unloading → Idle

각 액터는 현재 상태(또는 상태 스택)를 유지하며, 모든 수신 메시지는 활성 상태의 ProcessMessage 메서드로 라우팅됩니다.

메서드

ProcessMessage

이 상태 내에서 수신 메시지를 처리합니다.

bool ProcessMessage(Message message);

매개변수:

  • message — 이름과 선택적 데이터 페이로드를 포함하는 처리할 메시지입니다.

반환값: 이 상태에서 메시지를 처리한 경우 true, 메시지가 관련이 없어 부모 상태로 전파되어야 하는 경우 false입니다.

설명:

상태 인터페이스의 핵심 메서드입니다. 메시지가 액터로 전송되면 현재 상태의 ProcessMessage 메서드로 전달됩니다. 상태는 메시지를 검사하고 다음 중 하나를 수행합니다:

  1. 처리 - 메시지를 처리하고 true를 반환합니다
  2. 무시 - false를 반환하여 메시지가 스택된 부모 상태로 전파되도록 허용합니다

사용 패턴:

public override bool ProcessMessage(Message msg)
{
switch (msg.Name)
{
case "Start":
// 메시지 처리
DoSomething();
return true; // 메시지 처리됨

case "Stop":
// 처리 및 전환
SetState(new IdleState(_actor));
return true; // 메시지 처리됨

default:
return false; // 처리되지 않음, 전파될 수 있음
}
}

State 클래스

실제로는 IState를 구현하고 추가 기능을 제공하는 State 기본 클래스를 상속합니다:

public class MyState : State
{
private MyActor Actor => (MyActor)_actor;

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

// 수명 주기 메서드
public override void OnEntry(Message msg) { }
public override bool OnProcess(Message msg) { return false; }
public override void OnExit(Message msg) { }
}

생성자

public MyState(Actor actor) : base(actor)

모든 상태는 소유 액터에 대한 참조를 받아야 합니다. 강력한 타입의 액터 참조를 저장합니다:

private MyActor Actor => (MyActor)_actor;

수명 주기 메서드

OnEntry

이 상태로 전환할 때 한 번 호출됩니다.

public override void OnEntry(Message msg)
{
// 상태 초기화
Actor.Busy.Value = true;
Actor.StatusLight.TurnOn();

// 작업 시작
Actor.Motor.Start();

// 타이머 설정
StartTimer("Timeout", 5000); // 5초 타임아웃
}

사용 사례:

  • 상태별 리소스 초기화
  • 하드웨어 작업 시작
  • 타이머 설정
  • 상태 변수 업데이트
  • 상태 전환 로그

OnProcess

이 상태에 있는 동안 모든 메시지에 대해 호출됩니다.

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; // 처리되지 않음
}
}

반환값:

  • true - 이 상태에서 메시지를 처리했습니다
  • false - 메시지가 관련이 없습니다, 전파를 허용합니다

사용 사례:

  • 수신 메시지 처리
  • 센서 이벤트 처리
  • 타이머 만료에 응답
  • 상태 전환 관리
  • 다른 액터와 조정

OnExit

이 상태를 떠날 때 한 번 호출됩니다.

public override void OnExit(Message msg)
{
// 상태 정리
Actor.Busy.Value = false;
Actor.StatusLight.TurnOff();

// 작업 중지
Actor.Motor.Stop();

// 타이머 취소
CancelAllTimers();

// 리소스 해제
Actor.Vacuum.TurnOff();
}

사용 사례:

  • 상태별 리소스 정리
  • 하드웨어 작업 중지
  • 타이머 취소
  • 상태 변수 업데이트
  • 상태 데이터 저장

중요: 리소스 누수를 방지하고 적절한 상태 전환을 보장하려면 항상 OnExit에서 정리하세요.

상태 전환 메서드

SetState

현재 상태를 완전히 교체합니다:

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

흐름:

  1. 현재 상태의 OnExit() 호출
  2. 새 상태의 OnEntry() 호출
  3. 액터가 이제 새 상태에 있음

사용 시기:

  • 정상적인 상태 진행
  • 상태가 완전히 완료됨
  • 현재 상태로 돌아갈 필요가 없음

PushState

스택에 새 상태를 푸시합니다(현재 상태 일시 중지):

public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
PushState(new EmergencyState(_actor));
return true;
}
return false;
}

흐름:

  1. 새 상태의 OnEntry() 호출
  2. 현재 상태 일시 중단(OnExit이 호출되지 않음)
  3. 메시지가 새 상태로 이동

사용 시기:

  • 임시 중단
  • 나중에 현재 상태로 돌아가야 함
  • 계층적 동작(기본 상태 + 오버레이)

PopState

스택의 이전 상태로 돌아갑니다:

public override bool OnProcess(Message msg)
{
if (msg.Name == "Resume")
{
PopState();
return true;
}
return false;
}

흐름:

  1. 현재 상태의 OnExit() 호출
  2. 이전 상태 재개(OnEntry가 다시 호출되지 않음)
  3. 메시지가 이전 상태로 이동

사용 시기:

  • 임시 중단 완료
  • 하위 작업에서 돌아옴
  • 오버레이 동작 완료

타이머 관리

상태는 주기적 또는 지연된 작업을 위한 타이머를 관리할 수 있습니다:

StartTimer

StartTimer("CheckSensor", 100);  // 100ms 후 발동

지정된 간격 후에 TimerMessage를 보낼 타이머를 시작합니다.

CancelTimer

CancelTimer("CheckSensor");

이름으로 특정 타이머를 취소합니다.

CancelAllTimers

CancelAllTimers();

이 상태에서 시작한 모든 타이머를 취소합니다. 일반적으로 OnExit()에서 호출됩니다.

타이머 패턴:

public override void OnEntry(Message msg)
{
// 주기적 확인 시작
StartTimer("CheckSensor", 100);
}

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);
}
return true;
}
return false;
}

public override void OnExit(Message msg)
{
CancelAllTimers();
}

내장 메시지

StateEntryMessage

상태 진입 시 자동으로 전송됩니다:

public override bool OnProcess(Message msg)
{
if (msg.Name == "StateEntryMessage")
{
// 메시지 컨텍스트가 필요한 초기화 수행
return true;
}
return false;
}

참고: 일반적으로 대신 OnEntry()에서 초기화가 수행됩니다.

StateExitMessage

상태를 떠나기 전에 자동으로 전송됩니다:

public override bool OnProcess(Message msg)
{
if (msg.Name == "StateExitMessage")
{
// 떠나기 전에 상태 저장
Actor.LastPosition.Value = Actor.X.CommandPosition;
return true;
}
return false;
}

참고: 일반적으로 대신 OnExit()에서 정리가 수행됩니다.

TimerMessage

타이머가 만료될 때 전송됩니다:

if (msg.Name == "TimerMessage" && (string)msg.Data == "MyTimer")
{
// 타이머 만료 처리
return true;
}

일반적인 상태 패턴

1. 단순 상태

하나의 책임, 명확한 전환:

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

public override void OnEntry(Message msg)
{
Console.WriteLine("준비됨");
}

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

2. 대기 상태

조건이 충족될 때까지 폴링:

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. 비동기 작업 상태

비동기 작업을 시작하고 완료를 기다립니다:

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, "이동 타임아웃"));
return true;
}

return false;
}

public override void OnExit(Message msg)
{
CancelAllTimers();
}
}

4. 루핑 상태

여러 항목을 순차적으로 처리합니다:

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;
}

// 현재 항목 처리
actor.ProcessItem(currentIndex);
}

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

5. 오류 처리 상태

오류를 처리하고 복구를 제공합니다:

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;

// 모든 모션 중지
actor.X.Stop();

// 사용자에게 알림
Console.WriteLine($"오류: {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();
}
}

메시지 전파

상태 스택(PushState)을 사용할 때 처리되지 않은 메시지는 부모 상태로 전파됩니다:

// 상태 스택: [BaseState, OverlayState]
// 현재: OverlayState

public class OverlayState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "OverlaySpecific")
{
// 오버레이 특정 메시지 처리
return true;
}

// false 반환 - 메시지가 BaseState로 전파됨
return false;
}
}

public class BaseState : State
{
public override bool OnProcess(Message msg)
{
if (msg.Name == "EmergencyStop")
{
// OverlayState가 활성화되어 있어도 긴급 상황 처리
SetState(new EmergencyState(_actor));
return true;
}

return false;
}
}

모범 사례

1. 상태당 하나의 책임

// ✅ 좋음 - 각 상태는 명확한 목적을 가짐
public class MovingState : State { }
public class ProcessingState : State { }
public class WaitingState : State { }

// ❌ 나쁨 - 상태가 너무 많은 일을 함
public class DoEverythingState : State { }

2. OnExit에서 항상 정리

// ✅ 좋음
public override void OnEntry(Message msg)
{
StartTimer("Monitor", 100);
Actor.Motor.Start();
}

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

// ❌ 나쁨 - 정리 없음
public override void OnEntry(Message msg)
{
StartTimer("Monitor", 100);
Actor.Motor.Start();
}
// 모터가 계속 켜져 있고, 타이머가 계속 실행됨!

3. 설명적인 이름 사용

public class WaitingForVacuumState : State { }      // ✅ 명확함
public class MovingToLoadPositionState : State { } // ✅ 명확함
public class State1 : State { } // ❌ 불명확함

4. 타임아웃 처리

// ✅ 좋음 - 타임아웃이 중단 방지
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, "타임아웃"));
return true;
}
return false;
}

5. 중단에 상태 스택 사용

// ✅ 좋음 - 컨텍스트 보존
if (msg.Name == "PauseForInspection")
{
PushState(new InspectionState(_actor));
// 현재 상태로 돌아갈 수 있음
}

// ❌ 나쁨 - 컨텍스트 손실
if (msg.Name == "PauseForInspection")
{
SetState(new InspectionState(_actor));
// 어디에 있었는지 손실됨
}

참고